Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/usage/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ mpv "http://localhost:8888/proxy/hls/manifest.m3u8?d=https://devstreaming-cdn.ap
mpv "http://localhost:8888/proxy/hls/manifest.m3u8?d=https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8&max_res=true&api_password=your_password"
```

### DASH Stream with Audio Selection

```bash
# Select specific audio (es)
mpv "http://localhost:8888/proxy/mpd/manifest.m3u8?d=https://example.com/manifest.mpd&audio_lang=es&api_password=your_password"
```

### HLS/DASH Stream with Segment Skipping (Intro/Outro Skip)

```bash
Expand Down
6 changes: 6 additions & 0 deletions docs/usage/url-params-and-encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Select a specific resolution stream instead of the highest or default.
- **Effect:** Selects the stream matching the specified resolution. Falls back to the closest lower resolution if exact match is not found.
- **Supported Endpoints:** `/proxy/hls/manifest.m3u8`, `/proxy/mpd/manifest.m3u8`

**`&audio_lang=es`**
Select a specific audio language (BCP 47 tag, e.g., 'en', 'es', 'es-AR'). Defaults to 'en'.
- **Usage:** Add `&audio_lang=es` (or `pt`, `es-AR`, etc.) to the proxy URL
- **Effect:** Selects the audio matching the specified language. Falls back to the highest-bandwidth audio if exact match not found.
- **Supported Endpoints:** `/proxy/mpd/manifest.m3u8`

**`&no_proxy=true`**
Disables the proxy for the current destination, performing a direct request.
- **Usage:** Add `&no_proxy=true` to the proxy URL
Expand Down
4 changes: 2 additions & 2 deletions mediaflow_proxy/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ async def get_manifest(
if drm_info and not drm_info.get("isDrmProtected"):
# For non-DRM protected MPD, we still create an HLS manifest
return await process_manifest(
request, mpd_dict, proxy_headers, None, None, manifest_params.resolution, skip_segments
request, mpd_dict, proxy_headers, None, None, manifest_params.resolution, skip_segments, manifest_params.audio_lang
)

# Support combined kid:key,kid:key format passed as a single key= param
Expand All @@ -838,7 +838,7 @@ async def get_manifest(
key = _normalize_drm_key_value(key)

return await process_manifest(
request, mpd_dict, proxy_headers, key_id, key, manifest_params.resolution, skip_segments
request, mpd_dict, proxy_headers, key_id, key, manifest_params.resolution, skip_segments, manifest_params.audio_lang
)


Expand Down
14 changes: 10 additions & 4 deletions mediaflow_proxy/mpd_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ async def process_manifest(
key: str = None,
resolution: str = None,
skip_segments: list = None,
audio_lang: str = "en"
) -> Response:
"""
Processes the MPD manifest and converts it to an HLS manifest.
Expand All @@ -132,11 +133,13 @@ async def process_manifest(
key (str, optional): The DRM key. Defaults to None.
resolution (str, optional): Target resolution (e.g., '1080p', '720p'). Defaults to None.
skip_segments (list, optional): List of time segments to skip. Each item should have 'start' and 'end' keys.
audio_lang (str, optional): Select a specific audio language (BCP 47 tag, e.g., 'en', 'es', 'es-AR').
Falls back to the highest-bandwidth audio if exact match not found. Defaults to 'en'.

Returns:
Response: The HLS manifest as an HTTP response.
"""
hls_content = build_hls(mpd_dict, request, key_id, key, resolution, skip_segments)
hls_content = build_hls(mpd_dict, request, key_id, key, resolution, skip_segments, audio_lang)

# Start DASH pre-buffering in background if enabled
if settings.enable_dash_prebuffer:
Expand Down Expand Up @@ -391,6 +394,7 @@ def build_hls(
key: str = None,
resolution: str = None,
skip_segments: list = None,
audio_lang: str = "en"
) -> str:
"""
Builds an HLS manifest from the MPD manifest.
Expand All @@ -402,6 +406,8 @@ def build_hls(
key (str, optional): The DRM key. Defaults to None.
resolution (str, optional): Target resolution (e.g., '1080p', '720p'). Defaults to None.
skip_segments (list, optional): List of time segments to skip. Each item should have 'start' and 'end' keys.
audio_lang (str, optional): Select a specific audio language (BCP 47 tag, e.g., 'en', 'es', 'es-AR').
Falls back to the highest-bandwidth audio if exact match not found. Defaults to 'en'.

Returns:
str: The HLS manifest as a string.
Expand Down Expand Up @@ -479,12 +485,12 @@ def build_hls(
deduped = height_deduped[:MAX_VIDEO_VARIANTS]
video_profiles = {p["id"]: (p, url) for p, url in deduped}

# Determine the default audio (English preferred, else highest bandwidth).
# Determine the default audio (audio_lang preferred, else highest bandwidth).
default_audio_id = None
if audio_profiles:
all_audio = list(audio_profiles.values())
en_audio = [(p, u) for p, u in all_audio if (p.get("lang") or "").startswith("en")]
default_profile, _ = max(en_audio or all_audio, key=lambda pu: pu[0].get("bandwidth", 0))
default_audio = [(p, u) for p, u in all_audio if (p.get("lang") or "").startswith(audio_lang)]
default_profile, _ = max(default_audio or all_audio, key=lambda pu: pu[0].get("bandwidth", 0))
Comment on lines +492 to +493

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Regional language requests can select the wrong default audio.

Line 492 only checks profile_lang.startswith(audio_lang), so audio_lang=es-AR will not match a generic es track and may fall back to a different language by bandwidth. Normalize case and implement hierarchical BCP-47 matching (exact → primary-language compatible → bandwidth fallback).

💡 Suggested fix
-    default_audio = [(p, u) for p, u in all_audio if (p.get("lang") or "").startswith(audio_lang)]
+    req_lang = (audio_lang or "en").strip().lower()
+    req_primary = req_lang.split("-")[0]
+
+    def _lang_match(profile_lang: str) -> tuple[int, int]:
+        # score: 2 exact, 1 primary-language compatible, 0 no match
+        pl = (profile_lang or "").strip().lower()
+        if pl == req_lang:
+            return (2, 0)
+        pl_primary = pl.split("-")[0] if pl else ""
+        if pl and (pl.startswith(req_lang + "-") or req_lang.startswith(pl + "-") or pl_primary == req_primary):
+            return (1, 0)
+        return (0, 0)
+
+    scored = [
+        (p, u, _lang_match(p.get("lang")))
+        for p, u in all_audio
+    ]
+    matched = [(p, u) for p, u, score in scored if score[0] > 0]
+    default_audio = matched
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mediaflow_proxy/mpd_processor.py` around lines 492 - 493, The language
matching logic at line 492 uses a simple startswith comparison that fails to
handle case-insensitive matching and hierarchical BCP-47 language tag matching.
To fix this, normalize both the audio_lang parameter and the profile language
code to lowercase for comparison, then implement a three-tier matching strategy:
first filter default_audio to include profiles that exactly match the normalized
audio_lang, then also include profiles whose primary language component (before
the hyphen) matches the audio_lang primary component, and finally fall back to
bandwidth selection from the combined list. This ensures that a request for
audio_lang like "es-AR" properly matches both exact "es-ar" tracks and generic
"es" tracks before using bandwidth as a tiebreaker.

default_audio_id = default_profile["id"]

# Audio tracks: one entry per unique language, capped at MAX_AUDIO_TRACKS.
Expand Down
7 changes: 7 additions & 0 deletions mediaflow_proxy/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ class MPDManifestParams(GenericParams):
None,
description="Override global REMUX_TO_TS setting per-request. true = force TS remuxing, false = force fMP4 passthrough, omit = use server default.",
)
audio_lang: Optional[str] = Field(
"en",
description=(
"Select a specific audio language (BCP 47 tag, e.g., 'en', 'es', 'es-AR'). "
"Falls back to the highest-bandwidth audio if exact match not found. Defaults to 'en'."
),
)

@field_validator("resolution", mode="before")
@classmethod
Expand Down
12 changes: 12 additions & 0 deletions mediaflow_proxy/static/url_generator.html
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@ <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Resoluti
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Select specific resolution (falls back to closest lower if not available)</p>
</div>

<!-- Audio Language -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Audio</h4>
<input type="text" id="mpd-audio-lang" placeholder="en (BCP 47 language tag)"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Set audio language (English by default, else highest bandwidth)</p>
</div>

<!-- Skip Segments Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
Expand Down Expand Up @@ -2226,6 +2234,10 @@ <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3 flex item
const resolution = document.getElementById('mpd-resolution').value;
if (resolution) params.append('resolution', resolution);

// Audio language
const audioLang = document.getElementById('mpd-audio-lang').value.trim();
if (audioLang) params.append('audio_lang', audioLang);

// Skip segments
const skipSegments = document.getElementById('mpd-skip-segments').value.trim();
if (skipSegments) params.append('skip', skipSegments);
Expand Down