mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-31 19:22:28 +01:00
Add discovery
This commit is contained in:
@@ -68,7 +68,9 @@ class ManifestParser:
|
||||
Returns:
|
||||
Full URL to an initialization segment, or None if not found
|
||||
"""
|
||||
# Build effective base URL from manifest URL and BaseURL elements
|
||||
# Build effective base URL from manifest URL and MPD/Period-level BaseURL
|
||||
# elements only (Representation-level BaseURLs are relative media paths,
|
||||
# not base URLs, and are intentionally excluded by extract_base_urls).
|
||||
base_urls = ManifestUtils.extract_base_urls(manifest_content)
|
||||
effective_base = URLResolver.build_effective_base_url(manifest_url, base_urls)
|
||||
|
||||
@@ -82,43 +84,75 @@ class ManifestParser:
|
||||
target_sets = video_sets + audio_sets
|
||||
|
||||
for ad_set_info in target_sets:
|
||||
# Extract SegmentTemplate initialization attribute
|
||||
# ------------------------------------------------------------------
|
||||
# Branch A: SegmentTemplate-based manifest (most live/VOD streams)
|
||||
# The init segment URL is expressed as a template attribute.
|
||||
# ------------------------------------------------------------------
|
||||
init_template = ManifestUtils.extract_segment_template_initialization(
|
||||
ad_set_info.content
|
||||
)
|
||||
|
||||
if not init_template:
|
||||
continue
|
||||
if init_template:
|
||||
logger.debug(f"Found init template: {init_template}")
|
||||
|
||||
logger.debug(f"Found init template: {init_template}")
|
||||
# Get first Representation ID from this AdaptationSet
|
||||
rep_id = ManifestUtils.extract_first_representation_id(ad_set_info.content)
|
||||
|
||||
# Get first Representation ID from this AdaptationSet
|
||||
rep_id = ManifestUtils.extract_first_representation_id(ad_set_info.content)
|
||||
if not rep_id:
|
||||
logger.debug("No Representation ID found in AdaptationSet")
|
||||
continue
|
||||
|
||||
if not rep_id:
|
||||
logger.debug("No Representation ID found in AdaptationSet")
|
||||
continue
|
||||
logger.debug(f"Using Representation ID: {rep_id}")
|
||||
|
||||
logger.debug(f"Using Representation ID: {rep_id}")
|
||||
# Substitute template variables with defaults
|
||||
init_url = URLResolver.substitute_template_variables(
|
||||
init_template,
|
||||
representation_id=rep_id,
|
||||
bandwidth="0",
|
||||
time="0",
|
||||
number="1"
|
||||
)
|
||||
|
||||
# Substitute template variables with defaults
|
||||
init_url = URLResolver.substitute_template_variables(
|
||||
init_template,
|
||||
representation_id=rep_id,
|
||||
bandwidth="0",
|
||||
time="0",
|
||||
number="1"
|
||||
)
|
||||
# Construct full URL
|
||||
full_url = URLResolver.construct_full_url(
|
||||
effective_base,
|
||||
init_url,
|
||||
url_encode_filename=True
|
||||
)
|
||||
|
||||
# Construct full URL
|
||||
full_url = URLResolver.construct_full_url(
|
||||
effective_base,
|
||||
init_url,
|
||||
url_encode_filename=True
|
||||
)
|
||||
logger.info(f"Constructed init segment URL (SegmentTemplate): {full_url}")
|
||||
return full_url
|
||||
|
||||
logger.info(f"Constructed init segment URL: {full_url}")
|
||||
return full_url
|
||||
# ------------------------------------------------------------------
|
||||
# Branch B: SegmentBase manifest (on-demand, single-file MP4)
|
||||
# Each Representation has a <BaseURL> pointing to the full MP4 file.
|
||||
# The init segment is the same file, accessed via HTTP Range using
|
||||
# the range from <Initialization range="start-end"/>.
|
||||
# We return the bare MP4 URL; the caller is responsible for issuing
|
||||
# an appropriate Range request if it wants only the init bytes.
|
||||
# ------------------------------------------------------------------
|
||||
segment_base_url = ManifestUtils.extract_segment_base_url(ad_set_info.content)
|
||||
|
||||
if segment_base_url:
|
||||
init_range = ManifestUtils.extract_segment_base_init_range(ad_set_info.content)
|
||||
|
||||
full_url = URLResolver.construct_full_url(
|
||||
effective_base,
|
||||
segment_base_url,
|
||||
url_encode_filename=False # path is already a clean relative URL
|
||||
)
|
||||
|
||||
if init_range:
|
||||
logger.info(
|
||||
f"Constructed init segment URL (SegmentBase): {full_url} "
|
||||
f"[Range: bytes={init_range}]"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Constructed init segment URL (SegmentBase, no range): {full_url}"
|
||||
)
|
||||
|
||||
return full_url
|
||||
|
||||
logger.warning("Could not find init segment URL in manifest")
|
||||
return None
|
||||
|
||||
@@ -131,12 +131,61 @@ class ManifestUtils:
|
||||
@staticmethod
|
||||
def extract_base_urls(manifest_content: str) -> List[str]:
|
||||
"""
|
||||
Extract all BaseURL elements from manifest.
|
||||
Extract only MPD-level and Period-level BaseURL elements from manifest.
|
||||
Representation-level BaseURL values are relative media paths, not base URLs,
|
||||
and must NOT be included here or they corrupt the effective base URL calculation.
|
||||
|
||||
Args:
|
||||
manifest_content: Full manifest XML content
|
||||
|
||||
Returns:
|
||||
List of BaseURL text contents
|
||||
List of BaseURL text contents at MPD or Period scope only
|
||||
"""
|
||||
return re.findall(r"<BaseURL[^>]*>([^<]+)</BaseURL>", manifest_content)
|
||||
base_urls = []
|
||||
|
||||
# Strip everything from the first AdaptationSet onward so we only
|
||||
# see MPD-level and Period-level BaseURL elements.
|
||||
first_as = re.search(r"<AdaptationSet[\s>]", manifest_content)
|
||||
header_content = manifest_content[:first_as.start()] if first_as else manifest_content
|
||||
|
||||
for match in re.finditer(r"<BaseURL[^>]*>([^<]+)</BaseURL>", header_content):
|
||||
base_urls.append(match.group(1))
|
||||
|
||||
return base_urls
|
||||
|
||||
@staticmethod
|
||||
def extract_segment_base_url(ad_set_content: str) -> Optional[str]:
|
||||
"""
|
||||
For SegmentBase manifests, extract the first Representation-level BaseURL
|
||||
within an AdaptationSet. This is the complete (relative or absolute) URL
|
||||
for a self-contained MP4 file; the init segment lives inside it at the byte
|
||||
range given by the <Initialization range="…"/> element.
|
||||
|
||||
Args:
|
||||
ad_set_content: AdaptationSet XML content
|
||||
|
||||
Returns:
|
||||
BaseURL string from the first Representation, or None if not present
|
||||
"""
|
||||
match = re.search(r"<BaseURL[^>]*>([^<]+)</BaseURL>", ad_set_content)
|
||||
return match.group(1).strip() if match else None
|
||||
|
||||
@staticmethod
|
||||
def extract_segment_base_init_range(ad_set_content: str) -> Optional[str]:
|
||||
"""
|
||||
Extract the byte-range for the init segment from a SegmentBase manifest.
|
||||
Returns the value of the 'range' attribute on the <Initialization> element,
|
||||
e.g. "0-686".
|
||||
|
||||
Args:
|
||||
ad_set_content: AdaptationSet XML content
|
||||
|
||||
Returns:
|
||||
Range string ("start-end") or None if not present
|
||||
"""
|
||||
match = re.search(
|
||||
r"<Initialization[^>]*\brange=\"([^\"]+)\"",
|
||||
ad_set_content,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
return match.group(1).strip() if match else None
|
||||
@@ -410,6 +410,35 @@ class MPDRewriter:
|
||||
period.remove(adaptation_set)
|
||||
|
||||
logger.info(f"Removed {removal_count} video representation(s), kept highest quality only")
|
||||
|
||||
# Clean up SupplementalProperty elements that reference removed AdaptationSet IDs.
|
||||
# These are used for codec-switching hints (e.g. AVC↔HEVC) and become dangling
|
||||
# after we discard all but one video AdaptationSet.
|
||||
CODEC_SWITCH_SCHEME = "urn:mpeg:dash:adaptation-set-switching:2016"
|
||||
surviving_as_ids: Set[str] = set()
|
||||
for period in root.findall(".//mpd:Period", self.MPD_NAMESPACE):
|
||||
for adaptation_set in period.findall("mpd:AdaptationSet", self.MPD_NAMESPACE):
|
||||
as_id = adaptation_set.get("id")
|
||||
if as_id:
|
||||
surviving_as_ids.add(as_id)
|
||||
|
||||
for period in root.findall(".//mpd:Period", self.MPD_NAMESPACE):
|
||||
for adaptation_set in period.findall("mpd:AdaptationSet", self.MPD_NAMESPACE):
|
||||
for sp in list(adaptation_set.findall("mpd:SupplementalProperty", self.MPD_NAMESPACE)):
|
||||
if sp.get("schemeIdUri") == CODEC_SWITCH_SCHEME:
|
||||
raw_value = sp.get("value", "")
|
||||
# Keep only IDs that still exist in the tree
|
||||
kept = [
|
||||
v.strip()
|
||||
for v in raw_value.split(",")
|
||||
if v.strip() in surviving_as_ids
|
||||
]
|
||||
if not kept:
|
||||
# No surviving peers — remove the property entirely
|
||||
adaptation_set.remove(sp)
|
||||
else:
|
||||
sp.set("value", ",".join(kept))
|
||||
|
||||
return best_video
|
||||
|
||||
def _is_video_adaptation_set(self, adaptation_set: ET.Element) -> bool:
|
||||
@@ -797,7 +826,62 @@ class MPDRewriter:
|
||||
if element.tag.endswith("Representation"):
|
||||
current_rep_id = element.get("id", "")
|
||||
|
||||
# Rewrite URL attributes
|
||||
# ------------------------------------------------------------------
|
||||
# SegmentBase manifest support (Bug 2 fix):
|
||||
# Representation-level <BaseURL> elements carry the full media URL.
|
||||
# Rewrite their text content through the proxy so all media requests
|
||||
# are routed correctly. We also resolve <SegmentBase> / <Initialization>
|
||||
# range-request URLs here (Bug 3 fix) so the init segment is fetched
|
||||
# via the proxy with proper decryption parameters.
|
||||
# ------------------------------------------------------------------
|
||||
if element.tag.endswith("BaseURL") and element.text:
|
||||
parent_rep_id = current_rep_id # captured when we entered Representation
|
||||
raw_url = element.text.strip()
|
||||
if raw_url:
|
||||
resolved = urljoin(base_url, raw_url)
|
||||
# Rewrite the BaseURL text through the proxy.
|
||||
# We don't know the segment type yet (the SegmentBase child will
|
||||
# handle Initialization specifically), so pass None for seg_type
|
||||
# so the proxy receives both kid+key when in decrypt mode.
|
||||
element.text = self.build_proxy_url(
|
||||
resolved, None, None, current_encrypted, current_kid,
|
||||
representation_id=parent_rep_id,
|
||||
)
|
||||
# Update local base_url so that any sibling/child elements that
|
||||
# use urljoin against it resolve correctly against the proxied URL.
|
||||
# We intentionally do NOT update the outer base_url variable here
|
||||
# — it is passed by value to children via the recursion call below.
|
||||
base_url = resolved # use original resolved URL for child resolution
|
||||
|
||||
# SegmentBase <Initialization range="…"/> — rewrite as a proxy URL that
|
||||
# carries the Range header information so the proxy can pass it through.
|
||||
# The actual media bytes live at the parent Representation's <BaseURL>,
|
||||
# which has already been rewritten above. We rebuild the init URL from
|
||||
# the (original, pre-proxy) base_url so we don't double-encode it.
|
||||
if element.tag.endswith("Initialization") and "range" in element.attrib:
|
||||
byte_range = element.attrib["range"]
|
||||
# base_url at this point is the resolved (original CDN) Representation
|
||||
# URL set just above when we processed the sibling <BaseURL> element.
|
||||
# If for some reason it wasn't set (no <BaseURL> sibling), fall back
|
||||
# to the inherited base_url which is the AdaptationSet/Period base.
|
||||
init_proxy_url = self.build_proxy_url(
|
||||
base_url, None, "initialization", current_encrypted, current_kid,
|
||||
representation_id=current_rep_id,
|
||||
)
|
||||
# Replace the range attribute with a marker the proxy understands,
|
||||
# and store the proxied URL as a sourceURL attribute so the player
|
||||
# uses it for the range request rather than the BaseURL text.
|
||||
# DASH players that understand SegmentBase will issue:
|
||||
# GET <sourceURL> Range: bytes=<range>
|
||||
# Proxies that receive this will forward the Range header to the CDN.
|
||||
element.set("sourceURL", init_proxy_url)
|
||||
# Leave the range attribute intact so the player still knows which
|
||||
# bytes to request.
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Standard SegmentTemplate / SegmentList attribute rewriting
|
||||
# (unchanged from original — handles media/initialization/sourceURL attrs)
|
||||
# ------------------------------------------------------------------
|
||||
attr_map = {
|
||||
"media": "media",
|
||||
"initialization": "initialization",
|
||||
|
||||
Reference in New Issue
Block a user