Files
2026-03-06 13:40:57 +01:00

139 lines
5.3 KiB
Python

# streaming_providers/base/utils/video_quality.py
from dataclasses import dataclass
from typing import Optional, Tuple, List
import xml.etree.ElementTree as ET
from .logger import logger
@dataclass
class VideoRepresentation:
"""Represents a video representation with its quality metrics."""
element: ET.Element
adaptation_set: ET.Element
period: ET.Element
bandwidth: int
width: Optional[int] = None
height: Optional[int] = None
frame_rate: Optional[float] = None
representation_id: str = ""
period_id: str = ""
as_id: str = ""
def get_quality_score(self) -> Tuple[int, int, int]:
"""
Returns a tuple for comparison: (resolution_pixels, bandwidth, frame_rate_score)
Higher values = better quality
"""
resolution = (self.width or 0) * (self.height or 0)
frame_rate_score = int((self.frame_rate or 0) * 100)
return resolution, self.bandwidth, frame_rate_score
class VideoQualityFilter:
"""Handles video quality filtering and highest quality selection."""
NAMESPACE = {"mpd": "urn:mpeg:dash:schema:mpd:2011"}
@staticmethod
def is_video_adaptation_set(adaptation_set: ET.Element) -> bool:
"""Determine if an AdaptationSet is video based on mimeType or contentType."""
mime_type = adaptation_set.get("mimeType", "")
content_type = adaptation_set.get("contentType", "")
# Check AdaptationSet level
if mime_type.startswith("video/") or content_type == "video":
return True
# Check Representation level if not specified at AdaptationSet level
for representation in adaptation_set.findall("mpd:Representation", VideoQualityFilter.NAMESPACE):
rep_mime = representation.get("mimeType", "")
if rep_mime.startswith("video/"):
return True
return False
@staticmethod
def parse_video_representation(
representation: ET.Element,
adaptation_set: ET.Element,
period: ET.Element,
period_id: str,
as_id: str
) -> Optional[VideoRepresentation]:
"""Parse a Representation element and extract video quality information."""
try:
# Bandwidth is required
bandwidth = int(representation.get("bandwidth", "0"))
if bandwidth == 0:
return None
# Width and height (can be on Representation or AdaptationSet)
width = representation.get("width") or adaptation_set.get("width")
height = representation.get("height") or adaptation_set.get("height")
width = int(width) if width else None
height = int(height) if height else None
# Frame rate (can be on Representation or AdaptationSet)
frame_rate_str = representation.get("frameRate") or adaptation_set.get("frameRate")
frame_rate = None
if frame_rate_str:
# Handle both "30" and "30000/1001" formats
if "/" in frame_rate_str:
num, denom = frame_rate_str.split("/")
frame_rate = float(num) / float(denom)
else:
frame_rate = float(frame_rate_str)
representation_id = representation.get("id", "")
return VideoRepresentation(
element=representation,
adaptation_set=adaptation_set,
period=period,
bandwidth=bandwidth,
width=width,
height=height,
frame_rate=frame_rate,
representation_id=representation_id,
period_id=period_id,
as_id=as_id
)
except (ValueError, AttributeError) as e:
logger.warning(f"Failed to parse video representation: {e}")
return None
@classmethod
def find_best_video(cls, root: ET.Element) -> Optional[VideoRepresentation]:
"""Find the highest quality video representation across all periods."""
all_video_reps: List[VideoRepresentation] = []
# Scan all periods and adaptation sets to find video representations
for period in root.findall(".//mpd:Period", cls.NAMESPACE):
period_id = period.get("id", "")
for adaptation_set in period.findall("mpd:AdaptationSet", cls.NAMESPACE):
if not cls.is_video_adaptation_set(adaptation_set):
continue
as_id = adaptation_set.get("id", str(id(adaptation_set)))
# Collect all representations from this video adaptation set
for representation in adaptation_set.findall("mpd:Representation", cls.NAMESPACE):
video_rep = cls.parse_video_representation(
representation, adaptation_set, period, period_id, as_id
)
if video_rep:
all_video_reps.append(video_rep)
if not all_video_reps:
logger.warning("No video representations found for filtering")
return None
best_video = max(all_video_reps, key=lambda v: v.get_quality_score())
logger.debug(
f"Found {len(all_video_reps)} video representations, "
f"best: {best_video.width}x{best_video.height} @ {best_video.bandwidth}bps"
)
return best_video