| | import logging |
| | import math |
| | import time |
| | from datetime import datetime, timezone, timedelta |
| |
|
| | from fastapi import Request, Response, HTTPException |
| |
|
| | from mediaflow_proxy.configs import settings |
| | from mediaflow_proxy.drm.decrypter import decrypt_segment |
| | from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url, get_original_scheme |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | async def process_manifest(request: Request, mpd_dict: dict, key_id: str = None, key: str = None) -> Response: |
| | """ |
| | Processes the MPD manifest and converts it to an HLS manifest. |
| | |
| | Args: |
| | request (Request): The incoming HTTP request. |
| | mpd_dict (dict): The MPD manifest data. |
| | key_id (str, optional): The DRM key ID. Defaults to None. |
| | key (str, optional): The DRM key. Defaults to None. |
| | |
| | Returns: |
| | Response: The HLS manifest as an HTTP response. |
| | """ |
| | hls_content = build_hls(mpd_dict, request, key_id, key) |
| | return Response(content=hls_content, media_type="application/vnd.apple.mpegurl") |
| |
|
| |
|
| | async def process_playlist(request: Request, mpd_dict: dict, profile_id: str) -> Response: |
| | """ |
| | Processes the MPD manifest and converts it to an HLS playlist for a specific profile. |
| | |
| | Args: |
| | request (Request): The incoming HTTP request. |
| | mpd_dict (dict): The MPD manifest data. |
| | profile_id (str): The profile ID to generate the playlist for. |
| | |
| | Returns: |
| | Response: The HLS playlist as an HTTP response. |
| | |
| | Raises: |
| | HTTPException: If the profile is not found in the MPD manifest. |
| | """ |
| | matching_profiles = [p for p in mpd_dict["profiles"] if p["id"] == profile_id] |
| | if not matching_profiles: |
| | raise HTTPException(status_code=404, detail="Profile not found") |
| |
|
| | hls_content = build_hls_playlist(mpd_dict, matching_profiles, request) |
| | return Response(content=hls_content, media_type="application/vnd.apple.mpegurl") |
| |
|
| |
|
| | async def process_segment( |
| | init_content: bytes, |
| | segment_content: bytes, |
| | mimetype: str, |
| | key_id: str = None, |
| | key: str = None, |
| | ) -> Response: |
| | """ |
| | Processes and decrypts a media segment. |
| | |
| | Args: |
| | init_content (bytes): The initialization segment content. |
| | segment_content (bytes): The media segment content. |
| | mimetype (str): The MIME type of the segment. |
| | key_id (str, optional): The DRM key ID. Defaults to None. |
| | key (str, optional): The DRM key. Defaults to None. |
| | |
| | Returns: |
| | Response: The decrypted segment as an HTTP response. |
| | """ |
| | if key_id and key: |
| | |
| | now = time.time() |
| | decrypted_content = decrypt_segment(init_content, segment_content, key_id, key) |
| | logger.info(f"Decryption of {mimetype} segment took {time.time() - now:.4f} seconds") |
| | else: |
| | |
| | decrypted_content = init_content + segment_content |
| |
|
| | return Response(content=decrypted_content, media_type=mimetype) |
| |
|
| |
|
| | def build_hls(mpd_dict: dict, request: Request, key_id: str = None, key: str = None) -> str: |
| | """ |
| | Builds an HLS manifest from the MPD manifest. |
| | |
| | Args: |
| | mpd_dict (dict): The MPD manifest data. |
| | request (Request): The incoming HTTP request. |
| | key_id (str, optional): The DRM key ID. Defaults to None. |
| | key (str, optional): The DRM key. Defaults to None. |
| | |
| | Returns: |
| | str: The HLS manifest as a string. |
| | """ |
| | hls = ["#EXTM3U", "#EXT-X-VERSION:6"] |
| | query_params = dict(request.query_params) |
| |
|
| | video_profiles = {} |
| | audio_profiles = {} |
| |
|
| | |
| | proxy_url = request.url_for("playlist_endpoint") |
| | proxy_url = str(proxy_url.replace(scheme=get_original_scheme(request))) |
| |
|
| | for profile in mpd_dict["profiles"]: |
| | query_params.update({"profile_id": profile["id"], "key_id": key_id or "", "key": key or ""}) |
| | playlist_url = encode_mediaflow_proxy_url( |
| | proxy_url, |
| | query_params=query_params, |
| | ) |
| |
|
| | if "video" in profile["mimeType"]: |
| | video_profiles[profile["id"]] = (profile, playlist_url) |
| | elif "audio" in profile["mimeType"]: |
| | audio_profiles[profile["id"]] = (profile, playlist_url) |
| |
|
| | |
| | for i, (profile, playlist_url) in enumerate(audio_profiles.values()): |
| | is_default = "YES" if i == 0 else "NO" |
| | hls.append( |
| | f'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="{profile["id"]}",DEFAULT={is_default},AUTOSELECT={is_default},LANGUAGE="{profile.get("lang", "und")}",URI="{playlist_url}"' |
| | ) |
| |
|
| | |
| | for profile, playlist_url in video_profiles.values(): |
| | hls.append( |
| | f'#EXT-X-STREAM-INF:BANDWIDTH={profile["bandwidth"]},RESOLUTION={profile["width"]}x{profile["height"]},CODECS="{profile["codecs"]}",FRAME-RATE={profile["frameRate"]},AUDIO="audio"' |
| | ) |
| | hls.append(playlist_url) |
| |
|
| | return "\n".join(hls) |
| |
|
| |
|
| | def build_hls_playlist(mpd_dict: dict, profiles: list[dict], request: Request) -> str: |
| | """ |
| | Builds an HLS playlist from the MPD manifest for specific profiles. |
| | |
| | Args: |
| | mpd_dict (dict): The MPD manifest data. |
| | profiles (list[dict]): The profiles to include in the playlist. |
| | request (Request): The incoming HTTP request. |
| | |
| | Returns: |
| | str: The HLS playlist as a string. |
| | """ |
| | hls = ["#EXTM3U", "#EXT-X-VERSION:6"] |
| |
|
| | added_segments = 0 |
| | current_time = datetime.now(timezone.utc) |
| | live_stream_delay = timedelta(seconds=settings.mpd_live_stream_delay) |
| | target_end_time = current_time - live_stream_delay |
| |
|
| | proxy_url = request.url_for("segment_endpoint") |
| | proxy_url = str(proxy_url.replace(scheme=get_original_scheme(request))) |
| |
|
| | for index, profile in enumerate(profiles): |
| | segments = profile["segments"] |
| | if not segments: |
| | logger.warning(f"No segments found for profile {profile['id']}") |
| | continue |
| |
|
| | |
| | if index == 0: |
| | sequence = segments[0]["number"] |
| | extinf_values = [f["extinf"] for f in segments if "extinf" in f] |
| | target_duration = math.ceil(max(extinf_values)) if extinf_values else 3 |
| | hls.extend( |
| | [ |
| | f"#EXT-X-TARGETDURATION:{target_duration}", |
| | f"#EXT-X-MEDIA-SEQUENCE:{sequence}", |
| | ] |
| | ) |
| | if mpd_dict["isLive"]: |
| | hls.append("#EXT-X-PLAYLIST-TYPE:EVENT") |
| | else: |
| | hls.append("#EXT-X-PLAYLIST-TYPE:VOD") |
| |
|
| | init_url = profile["initUrl"] |
| |
|
| | query_params = dict(request.query_params) |
| | query_params.pop("profile_id", None) |
| | query_params.pop("d", None) |
| |
|
| | for segment in segments: |
| | if mpd_dict["isLive"]: |
| | if segment["end_time"] > target_end_time: |
| | continue |
| | hls.append(f"#EXT-X-PROGRAM-DATE-TIME:{segment['program_date_time']}") |
| | hls.append(f'#EXTINF:{segment["extinf"]:.3f},') |
| | query_params.update( |
| | {"init_url": init_url, "segment_url": segment["media"], "mime_type": profile["mimeType"]} |
| | ) |
| | hls.append( |
| | encode_mediaflow_proxy_url( |
| | proxy_url, |
| | query_params=query_params, |
| | ) |
| | ) |
| | added_segments += 1 |
| |
|
| | if not mpd_dict["isLive"]: |
| | hls.append("#EXT-X-ENDLIST") |
| |
|
| | logger.info(f"Added {added_segments} segments to HLS playlist") |
| | return "\n".join(hls) |
| |
|