mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-25 18:42:27 +02:00
adapt events, recordings, vod to same structure as channels
This commit is contained in:
+91
-68
@@ -4,6 +4,13 @@ Event-specific stream routes.
|
||||
|
||||
Events are temporary/live streams (sports, special broadcasts, etc.). They
|
||||
share the same transport pattern as channels but without catchup support.
|
||||
|
||||
As of the query-param consolidation (mirrors channels.py's
|
||||
_handle_channel_stream), there's a single /stream/index.mpd route rather
|
||||
than separate /stream/sw-drm/, /stream/proxied/, and /stream/proxied/ffmpeg/
|
||||
routes. Those combinations are now reached via drm_variant/client_drm/
|
||||
no_proxy/highest_quality_only query params on the one route, resolved
|
||||
directly through _resolve_stream_unified.
|
||||
"""
|
||||
|
||||
from bottle import HTTPResponse, request, response
|
||||
@@ -16,18 +23,70 @@ def setup_event_routes(app, manager, service, helpers):
|
||||
CONTENT_TYPE_EVENT = helpers["CONTENT_TYPE_EVENT"]
|
||||
_build_drm_header = helpers["_build_drm_header"]
|
||||
_build_stream_headers = helpers["_build_stream_headers"]
|
||||
_resolve_stream = helpers["_resolve_stream"]
|
||||
_resolve_decrypted_stream = helpers["_resolve_decrypted_stream"]
|
||||
_resolve_stream_unified = helpers["_resolve_stream_unified"]
|
||||
_get_drm_configs = helpers["_get_drm_configs"]
|
||||
|
||||
def _handle_event_stream(provider, event_id):
|
||||
"""Single implementation backing /stream/index.mpd — the only event
|
||||
stream route. Replaces the former /stream/sw-drm/, /stream/proxied/,
|
||||
and /stream/proxied/ffmpeg/ routes, each of which used to hardcode
|
||||
one fixed combination of drm_variant/receiver_side/
|
||||
highest_quality_only. Mirrors channels.py's _handle_channel_stream,
|
||||
minus catchup handling — events have no catchup."""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
drm_variant = request.query.get("drm_variant", "auto")
|
||||
no_proxy = request.query.get("no_proxy", "false").lower() == "true"
|
||||
# client_drm is the public query-param name; receiver_side is
|
||||
# what _resolve_stream_unified calls the same axis internally —
|
||||
# translated here at the route boundary, same as channels.py.
|
||||
# Old /stream/index.mpd (via the deprecated _resolve_stream
|
||||
# wrapper) hardcoded receiver_side=True, so client_drm defaults
|
||||
# to "true" here to preserve that for callers that don't pass
|
||||
# it explicitly. This intentionally differs from channels.py's
|
||||
# "false" default — that reflects channels' own prior behavior,
|
||||
# not a shared convention.
|
||||
client_drm = request.query.get("client_drm", "true").lower() == "true"
|
||||
highest_quality_only = request.query.get("highest_quality_only", "false").lower() == "true"
|
||||
|
||||
logger.debug(
|
||||
f"_handle_event_stream: provider={provider} event={event_id} "
|
||||
f"country={country!r} drm_variant={drm_variant} "
|
||||
f"client_drm={client_drm} no_proxy={no_proxy} "
|
||||
f"highest_quality_only={highest_quality_only}"
|
||||
)
|
||||
|
||||
return _resolve_stream_unified(
|
||||
CONTENT_TYPE_EVENT, provider, event_id,
|
||||
country=country,
|
||||
drm_variant=drm_variant,
|
||||
receiver_side=client_drm,
|
||||
no_proxy=no_proxy,
|
||||
highest_quality_only=highest_quality_only,
|
||||
)
|
||||
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"stream error for event {provider}/{event_id}: {e}")
|
||||
response.status = 404
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"stream error for event {provider}/{event_id}: {e}")
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route("/api/providers/<provider>/events/<event_id>/manifest")
|
||||
def get_event_manifest(provider, event_id):
|
||||
"""
|
||||
Returns JSON with a manifest_url pointing to the event stream endpoint.
|
||||
Attaches x-kodi-drm-configs header.
|
||||
Returns JSON with a manifest_url pointing to the single stream
|
||||
endpoint. Attaches x-kodi-drm-configs header.
|
||||
|
||||
Response includes both stream_url (auto DRM) and sw_drm_stream_url
|
||||
(software / ClearKey DRM).
|
||||
sw_drm_manifest_url is kept in the response for backward
|
||||
compatibility with existing clients (pvr.ultimate /
|
||||
plugin.video.ultimate may read this field by name) — it's now just
|
||||
manifest_url with drm_variant=software&client_drm=true appended,
|
||||
rather than a separate route.
|
||||
"""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
@@ -37,9 +96,9 @@ def setup_event_routes(app, manager, service, helpers):
|
||||
f"{base_url}/api/providers/{provider}/events/{event_id}"
|
||||
f"/stream/index.mpd{qs}"
|
||||
)
|
||||
sw_drm_qs_sep = "&" if qs else "?"
|
||||
sw_drm_stream_url = (
|
||||
f"{base_url}/api/providers/{provider}/events/{event_id}"
|
||||
f"/stream/sw-drm/index.mpd{qs}"
|
||||
f"{stream_url}{sw_drm_qs_sep}drm_variant=software&client_drm=true"
|
||||
)
|
||||
|
||||
_build_drm_header(CONTENT_TYPE_EVENT, provider, event_id, country=country)
|
||||
@@ -64,67 +123,31 @@ def setup_event_routes(app, manager, service, helpers):
|
||||
@app.route("/api/providers/<provider>/events/<event_id>/stream/index.mpd")
|
||||
def get_event_stream(provider, event_id):
|
||||
"""
|
||||
Returns HTTP 302 redirect to the event manifest, or a rewritten
|
||||
manifest body when media proxy is active.
|
||||
Single stream endpoint for event playback. Replaces the former
|
||||
/stream/sw-drm/, /stream/proxied/, and /stream/proxied/ffmpeg/
|
||||
routes, which are removed. Every combination those routes used to
|
||||
hardcode is now expressed via independent, freely-combinable query
|
||||
params (see get_channel_stream in channels.py for the full list):
|
||||
|
||||
client_drm=true|false client decrypts ClearKey itself
|
||||
if true; server decrypts to
|
||||
plaintext segments if false.
|
||||
Default false.
|
||||
drm_variant=auto|software which upstream DRM/quality
|
||||
variant to request. Default auto.
|
||||
no_proxy=true|false force redirect/fetch even if the
|
||||
content would normally be
|
||||
proxied. Default false.
|
||||
highest_quality_only=true|false collapse to a single
|
||||
highest-quality representation
|
||||
(e.g. for ffmpeg piping). Default
|
||||
false.
|
||||
country as before.
|
||||
|
||||
Returns an HTTP 302 redirect to the upstream manifest, or a
|
||||
rewritten manifest body when the media proxy is involved.
|
||||
"""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
return _resolve_stream(
|
||||
CONTENT_TYPE_EVENT, provider, event_id, country=country
|
||||
)
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"stream error for event {provider}/{event_id}: {e}")
|
||||
response.status = 404
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"stream error for event {provider}/{event_id}: {e}")
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route("/api/providers/<provider>/events/<event_id>/stream/sw-drm/index.mpd")
|
||||
def get_event_stream_sw_drm(provider, event_id):
|
||||
"""
|
||||
Software-DRM event stream endpoint.
|
||||
|
||||
Identical to the standard event stream endpoint except that
|
||||
drm_variant='software' is passed through to the resolution helpers.
|
||||
"""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
return _resolve_stream(
|
||||
CONTENT_TYPE_EVENT, provider, event_id,
|
||||
country=country, drm_variant="software",
|
||||
)
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"sw-drm stream error for event {provider}/{event_id}: {e}")
|
||||
response.status = 404
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"sw-drm stream error for event {provider}/{event_id}: {e}")
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route(
|
||||
"/api/providers/<provider>/events/<event_id>/stream/proxied/index.mpd"
|
||||
)
|
||||
def get_event_stream_decrypted(provider, event_id):
|
||||
"""Proxied event stream — all quality representations."""
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_EVENT, provider, event_id, highest_quality_only=False
|
||||
)
|
||||
|
||||
@app.route(
|
||||
"/api/providers/<provider>/events/<event_id>/stream/proxied/ffmpeg/index.mpd"
|
||||
)
|
||||
def get_event_stream_decrypted_ffmpeg(provider, event_id):
|
||||
"""Proxied event stream — highest quality only, optimised for ffmpeg."""
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_EVENT, provider, event_id, highest_quality_only=True
|
||||
)
|
||||
return _handle_event_stream(provider, event_id)
|
||||
|
||||
@app.route("/api/providers/<provider>/events/<event_id>/drm")
|
||||
def get_event_drm(provider, event_id):
|
||||
|
||||
@@ -4,8 +4,13 @@ Recording stream routes.
|
||||
|
||||
Recordings are always on-demand (pre-captured), so:
|
||||
- No catchup path (unlike channels)
|
||||
- No ffmpeg variant (not a live/adaptive stream that needs quality pinning)
|
||||
- recording_id is a flat identifier, no path hierarchy needed
|
||||
|
||||
As of the query-param consolidation (mirrors channels.py's
|
||||
_handle_channel_stream), there's a single /stream/index.mpd route rather
|
||||
than a separate /stream/proxied/ route. That combination is now reached via
|
||||
client_drm on the one route, resolved directly through
|
||||
_resolve_stream_unified.
|
||||
"""
|
||||
|
||||
from bottle import HTTPResponse, request, response
|
||||
@@ -18,10 +23,57 @@ def setup_recording_routes(app, manager, service, helpers):
|
||||
CONTENT_TYPE_RECORDING = helpers["CONTENT_TYPE_RECORDING"]
|
||||
_build_drm_header = helpers["_build_drm_header"]
|
||||
_build_stream_headers = helpers["_build_stream_headers"]
|
||||
_resolve_stream = helpers["_resolve_stream"]
|
||||
_resolve_decrypted_stream = helpers["_resolve_decrypted_stream"]
|
||||
_resolve_stream_unified = helpers["_resolve_stream_unified"]
|
||||
_get_drm_configs = helpers["_get_drm_configs"]
|
||||
|
||||
def _handle_recording_stream(provider, recording_id):
|
||||
"""Single implementation backing /stream/index.mpd — the only
|
||||
recording stream route. Replaces the former /stream/proxied/ route,
|
||||
which hardcoded receiver_side=False. Mirrors channels.py's
|
||||
_handle_channel_stream, minus catchup handling — recordings have no
|
||||
catchup. highest_quality_only is accepted for symmetry with the
|
||||
other content types even though no /stream/proxied/ffmpeg/ route
|
||||
ever existed here (recordings are on-demand, not live/adaptive, so
|
||||
there was never a quality-pinning need) — it's a no-op unless a
|
||||
caller opts in."""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
drm_variant = request.query.get("drm_variant", "auto")
|
||||
no_proxy = request.query.get("no_proxy", "false").lower() == "true"
|
||||
# Old /stream/index.mpd (via the deprecated _resolve_stream
|
||||
# wrapper) hardcoded receiver_side=True, so client_drm defaults
|
||||
# to "true" here to preserve that for callers that don't pass
|
||||
# it explicitly — same reasoning as events.py/vod.py.
|
||||
client_drm = request.query.get("client_drm", "true").lower() == "true"
|
||||
highest_quality_only = request.query.get("highest_quality_only", "false").lower() == "true"
|
||||
|
||||
logger.debug(
|
||||
f"_handle_recording_stream: provider={provider} recording={recording_id} "
|
||||
f"country={country!r} drm_variant={drm_variant} "
|
||||
f"client_drm={client_drm} no_proxy={no_proxy} "
|
||||
f"highest_quality_only={highest_quality_only}"
|
||||
)
|
||||
|
||||
return _resolve_stream_unified(
|
||||
CONTENT_TYPE_RECORDING, provider, recording_id,
|
||||
country=country,
|
||||
drm_variant=drm_variant,
|
||||
receiver_side=client_drm,
|
||||
no_proxy=no_proxy,
|
||||
highest_quality_only=highest_quality_only,
|
||||
)
|
||||
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(f"stream error for recording {provider}/{recording_id}: {e}")
|
||||
response.status = 404
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"stream error for recording {provider}/{recording_id}: {e}")
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route("/api/providers/<provider>/recordings/<recording_id>/manifest")
|
||||
def get_recording_manifest(provider, recording_id):
|
||||
"""
|
||||
@@ -67,37 +119,13 @@ def setup_recording_routes(app, manager, service, helpers):
|
||||
@app.route("/api/providers/<provider>/recordings/<recording_id>/stream/index.mpd")
|
||||
def get_recording_stream(provider, recording_id):
|
||||
"""
|
||||
Returns HTTP 302 redirect to the recording manifest, or a rewritten
|
||||
manifest body when media proxy is active.
|
||||
Single stream endpoint for recording playback. Replaces the former
|
||||
/stream/proxied/ route, which is removed. client_drm=false now
|
||||
reaches what that route used to do; see get_channel_stream in
|
||||
channels.py for the full query-param list (client_drm, drm_variant,
|
||||
no_proxy, highest_quality_only, country).
|
||||
"""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
return _resolve_stream(
|
||||
CONTENT_TYPE_RECORDING, provider, recording_id, country=country
|
||||
)
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
f"stream error for recording {provider}/{recording_id}: {e}"
|
||||
)
|
||||
response.status = 404
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"stream error for recording {provider}/{recording_id}: {e}"
|
||||
)
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route(
|
||||
"/api/providers/<provider>/recordings/<recording_id>/stream/proxied/index.mpd"
|
||||
)
|
||||
def get_recording_stream_decrypted(provider, recording_id):
|
||||
"""Proxied recording stream — all quality representations."""
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_RECORDING, provider, recording_id, highest_quality_only=False
|
||||
)
|
||||
return _handle_recording_stream(provider, recording_id)
|
||||
|
||||
@app.route("/api/providers/<provider>/recordings/<recording_id>/drm")
|
||||
def get_recording_drm(provider, recording_id):
|
||||
|
||||
+61
-24
@@ -4,6 +4,13 @@ VOD (Video on Demand) stream routes.
|
||||
|
||||
VOD content uses the same transport pattern as events but with hierarchical
|
||||
IDs (paths like "clip_1417600/stream") and without catchup support.
|
||||
|
||||
As of the query-param consolidation (mirrors channels.py's
|
||||
_handle_channel_stream), there's a single /stream/index.mpd route rather
|
||||
than separate /stream/proxied/ and /stream/proxied/ffmpeg/ routes. Those
|
||||
combinations are now reached via client_drm/no_proxy/highest_quality_only
|
||||
query params on the one route, resolved directly through
|
||||
_resolve_stream_unified.
|
||||
"""
|
||||
|
||||
from bottle import HTTPResponse, request, response
|
||||
@@ -16,21 +23,50 @@ def setup_vod_routes(app, manager, service, helpers):
|
||||
CONTENT_TYPE_VOD = helpers["CONTENT_TYPE_VOD"]
|
||||
_build_drm_header = helpers["_build_drm_header"]
|
||||
_build_stream_headers = helpers["_build_stream_headers"]
|
||||
_resolve_stream = helpers["_resolve_stream"]
|
||||
_resolve_decrypted_stream = helpers["_resolve_decrypted_stream"]
|
||||
_resolve_stream_unified = helpers["_resolve_stream_unified"]
|
||||
_get_drm_configs = helpers["_get_drm_configs"]
|
||||
|
||||
@app.route("/api/providers/<provider>/vod/<path:path>/stream/index.mpd")
|
||||
def get_vod_stream(provider, path):
|
||||
# Extract vod_id as the first segment before any slashes
|
||||
# Example: "clip_1417600/stream" -> "clip_1417600"
|
||||
vod_id = path.split("/")[0]
|
||||
|
||||
def _handle_vod_stream(provider, vod_id):
|
||||
"""Single implementation backing /stream/index.mpd — the only VOD
|
||||
stream route. Replaces the former /stream/proxied/ and
|
||||
/stream/proxied/ffmpeg/ routes, each of which used to hardcode one
|
||||
fixed combination of receiver_side/highest_quality_only. Mirrors
|
||||
channels.py's _handle_channel_stream, minus catchup handling — VOD
|
||||
has no catchup. drm_variant is accepted for symmetry with events/
|
||||
channels even though no VOD provider integration currently uses a
|
||||
software-DRM variant."""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
return _resolve_stream(
|
||||
CONTENT_TYPE_VOD, provider, vod_id, country=country
|
||||
drm_variant = request.query.get("drm_variant", "auto")
|
||||
no_proxy = request.query.get("no_proxy", "false").lower() == "true"
|
||||
# client_drm is the public query-param name; receiver_side is
|
||||
# what _resolve_stream_unified calls the same axis internally —
|
||||
# translated here at the route boundary, same as channels.py.
|
||||
# Old /stream/index.mpd (via the deprecated _resolve_stream
|
||||
# wrapper) hardcoded receiver_side=True, so client_drm defaults
|
||||
# to "true" here to preserve that for callers that don't pass
|
||||
# it explicitly. This intentionally differs from channels.py's
|
||||
# "false" default — that reflects channels' own prior behavior,
|
||||
# not a shared convention.
|
||||
client_drm = request.query.get("client_drm", "true").lower() == "true"
|
||||
highest_quality_only = request.query.get("highest_quality_only", "false").lower() == "true"
|
||||
|
||||
logger.debug(
|
||||
f"_handle_vod_stream: provider={provider} vod_id={vod_id} "
|
||||
f"country={country!r} drm_variant={drm_variant} "
|
||||
f"client_drm={client_drm} no_proxy={no_proxy} "
|
||||
f"highest_quality_only={highest_quality_only}"
|
||||
)
|
||||
|
||||
return _resolve_stream_unified(
|
||||
CONTENT_TYPE_VOD, provider, vod_id,
|
||||
country=country,
|
||||
drm_variant=drm_variant,
|
||||
receiver_side=client_drm,
|
||||
no_proxy=no_proxy,
|
||||
highest_quality_only=highest_quality_only,
|
||||
)
|
||||
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
@@ -42,6 +78,21 @@ def setup_vod_routes(app, manager, service, helpers):
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route("/api/providers/<provider>/vod/<path:path>/stream/index.mpd")
|
||||
def get_vod_stream(provider, path):
|
||||
"""
|
||||
Single stream endpoint for VOD playback. Replaces the former
|
||||
/stream/proxied/ and /stream/proxied/ffmpeg/ routes, which are
|
||||
removed. Every combination those routes used to hardcode is now
|
||||
expressed via independent, freely-combinable query params (see
|
||||
get_channel_stream in channels.py for the full list): client_drm,
|
||||
drm_variant, no_proxy, highest_quality_only, country.
|
||||
"""
|
||||
# Extract vod_id as the first segment before any slashes
|
||||
# Example: "clip_1417600/stream" -> "clip_1417600"
|
||||
vod_id = path.split("/")[0]
|
||||
return _handle_vod_stream(provider, vod_id)
|
||||
|
||||
@app.route("/api/providers/<provider>/vod/<path:path>/manifest")
|
||||
def get_vod_manifest(provider, path):
|
||||
vod_id = path.split("/")[0]
|
||||
@@ -72,20 +123,6 @@ def setup_vod_routes(app, manager, service, helpers):
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@app.route("/api/providers/<provider>/vod/<path:path>/stream/proxied/index.mpd")
|
||||
def get_vod_stream_decrypted(provider, path):
|
||||
vod_id = path.split("/")[0]
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_VOD, provider, vod_id, highest_quality_only=False
|
||||
)
|
||||
|
||||
@app.route("/api/providers/<provider>/vod/<path:path>/stream/proxied/ffmpeg/index.mpd")
|
||||
def get_vod_stream_decrypted_ffmpeg(provider, path):
|
||||
vod_id = path.split("/")[0]
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_VOD, provider, vod_id, highest_quality_only=True
|
||||
)
|
||||
|
||||
@app.route("/api/providers/<provider>/vod/<path:path>/drm")
|
||||
def get_vod_drm(provider, path):
|
||||
vod_id = path.split("/")[0]
|
||||
|
||||
+39
-19
@@ -989,25 +989,7 @@ class UltimateService:
|
||||
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/stream/index.mpd?client_drm=false&highest_quality_only=true"
|
||||
|
||||
# Build ffmpeg pipe command
|
||||
# Map all video (will be just one due to highest_quality_only), all audio, and optional subtitles
|
||||
ffmpeg_cmd = (
|
||||
f'pipe://ffmpeg -loglevel fatal '
|
||||
f'-fflags +genpts+igndts+discardcorrupt '
|
||||
f'-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 '
|
||||
f'-thread_queue_size 2048 ' # Kept large to handle demuxing all 4 audio tracks smoothly
|
||||
f'-re '
|
||||
f'-i "{stream_url}" '
|
||||
f'-map 0:v:0 ' # Maps the 1080p video stream
|
||||
f'-map 0:a? ' # Maps ALL available audio tracks (German/English AAC + AC-3)
|
||||
f'-c copy '
|
||||
f'-max_muxing_queue_size 8192 ' # Kept high to safely interleave the multi-audio timescales
|
||||
f'-f mpegts '
|
||||
f'-muxdelay 0 -muxpreload 0 '
|
||||
f'-mpegts_flags resend_headers '
|
||||
f'-metadata service_name="{channel_name}" '
|
||||
f'-flush_packets 1 '
|
||||
f'pipe:1'
|
||||
)
|
||||
ffmpeg_cmd = self._build_ffmpeg_pipe_command(stream_url, channel_name)
|
||||
|
||||
# Header only (EXTINF + KODIPROP) — this variant is
|
||||
# intentionally live-only, so include_catchup=False
|
||||
@@ -1330,6 +1312,44 @@ class UltimateService:
|
||||
|
||||
return m3u_content
|
||||
|
||||
@staticmethod
|
||||
def _build_ffmpeg_pipe_command(stream_url: str, channel_name: str) -> str:
|
||||
"""
|
||||
Build the ffmpeg pipe:// command used by the ffmpeg-piped M3U variant.
|
||||
|
||||
Extracted from _generate_m3u_proxied_ffmpeg_fast, which was the only
|
||||
caller and still is — pulled out unchanged so it's reusable/testable
|
||||
on its own, not because behavior needed to change.
|
||||
|
||||
- -thread_queue_size 2048: kept large to handle demuxing multiple
|
||||
audio tracks smoothly.
|
||||
- -map 0:v:0: maps the single video stream (only one exists, since
|
||||
the upstream URL is requested with highest_quality_only=true).
|
||||
- -map 0:a?: maps all available audio tracks (channels commonly
|
||||
carry multiple, e.g. German/English AAC + AC-3); "?" makes the
|
||||
map optional so this doesn't fail if a channel has none.
|
||||
- -max_muxing_queue_size 8192: kept high to safely interleave
|
||||
multi-audio timescales.
|
||||
"""
|
||||
return (
|
||||
f'pipe://ffmpeg -loglevel fatal '
|
||||
f'-fflags +genpts+igndts+discardcorrupt '
|
||||
f'-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 '
|
||||
f'-thread_queue_size 2048 '
|
||||
f'-re '
|
||||
f'-i "{stream_url}" '
|
||||
f'-map 0:v:0 '
|
||||
f'-map 0:a? '
|
||||
f'-c copy '
|
||||
f'-max_muxing_queue_size 8192 '
|
||||
f'-f mpegts '
|
||||
f'-muxdelay 0 -muxpreload 0 '
|
||||
f'-mpegts_flags resend_headers '
|
||||
f'-metadata service_name="{channel_name}" '
|
||||
f'-flush_packets 1 '
|
||||
f'pipe:1'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _chno_attr(channel) -> str:
|
||||
"""Return ' tvg-chno="N" ch-number="N"' when channel_number is set, else empty string."""
|
||||
|
||||
Reference in New Issue
Block a user