feat(dash): flag dv profile 8 tracks from supplementalCodecs and use hdr10+dv as the hybrid

This commit is contained in:
imSp4rky
2026-10-18 17:18:11 -06:00
parent 2cc5e44957
commit 1a8e633fdc
11 changed files with 232 additions and 43 deletions
+2 -1
View File
@@ -454,7 +454,8 @@ the API surface.
### Dolby Vision and hybrid ### Dolby Vision and hybrid
Two DV mechanisms sit on top of the track model. **DV fixup** re-exposes DV signalling on Two DV mechanisms sit on top of the track model. **DV fixup** re-exposes DV signalling on
HLS streams that carry a valid DV RPU but advertise plain HEVC (`dv_compatible_bitstream`). HLS and DASH streams that carry a valid DV RPU but advertise plain HEVC, with DV only in the
supplemental codecs (`dv_compatible_bitstream`). `--list` shows such a track as `HDR10+DV` or `HDR10P+DV`.
**Hybrid** processing (`Video.Range.HYBRID`) selects an HDR10(+) base and a DV donor, **Hybrid** processing (`Video.Range.HYBRID`) selects an HDR10(+) base and a DV donor,
extracts the RPU with `dovi_tool`, and injects it onto the base layer, to make a single extracts the RPU with `dovi_tool`, and injects it onto the base layer, to make a single
track that carries both HDR10 metadata and the DV RPU. Both are automatic once the track that carries both HDR10 metadata and the DV RPU. Both are automatic once the
+20 -4
View File
@@ -145,6 +145,21 @@ unshackle dl -v hevc,avc EXAMPLE 81234567
unshackle dl -q 2160 -r HDR10,DV EXAMPLE '...' unshackle dl -q 2160 -r HDR10,DV EXAMPLE '...'
``` ```
`--list` labels a track that carries a second format as `<range>+<format>`:
- The first part is the range that `-r` selects the track by.
- The second part is the format that the track also carries.
- The `+` only joins the two parts. unshackle writes HDR10+ as `HDR10P`, so `HDR10+DV` is HDR10 with
Dolby Vision, not HDR10+ with Dolby Vision.
| Label | Track | Selected by |
| --- | --- | --- |
| `DV+HDR10P` | A Dolby Vision track that also carries HDR10+ | `-r DV` |
| `HDR10+DV` | An HDR10 track whose frames also carry a Dolby Vision profile 8 RPU | `-r HDR10` |
| `HDR10P+DV` | An HDR10+ track whose frames also carry a Dolby Vision profile 8 RPU | `-r HDR10P` |
The output file of a track with a Dolby Vision profile 8 RPU plays as Dolby Vision on a DV screen.
!!! warning "HYBRID requires dovi_tool" !!! warning "HYBRID requires dovi_tool"
`-r HYBRID` makes a single hybrid track by injecting the Dolby Vision RPU `-r HYBRID` makes a single hybrid track by injecting the Dolby Vision RPU
metadata onto an HDR10/HDR10+ base layer with **dovi_tool**. metadata onto an HDR10/HDR10+ base layer with **dovi_tool**.
@@ -157,10 +172,11 @@ unshackle dl -q 2160 -r HDR10,DV EXAMPLE '...'
and goes on with the other resolutions of the title. When no hybrid file at all was made, and goes on with the other resolutions of the title. When no hybrid file at all was made,
the title fails. the title fails.
A DV track that carries HDR10+ (`DV+HDR10P` in `--list`) is already a hybrid. At a resolution A track that is `DV+HDR10P`, `HDR10+DV` or `HDR10P+DV` in `--list` already carries Dolby Vision and an HDR10
that has one, `-r HYBRID` selects the DV+HDR10P track with the highest bitrate (the lowest with or HDR10+ base, so it is already a hybrid. At a resolution that has one, `-r HYBRID` selects it
`--worst`) and muxes it as it is, with no base track and no dovi_tool step. The other and muxes it as it is, with no base track and no dovi_tool step. A track that carries HDR10+
resolutions get a hybrid that unshackle makes. comes first, then the highest bitrate (the lowest with `--worst`). The other resolutions get a
hybrid that unshackle makes.
To keep an HDR10+ deliverable *alongside* the hybrid, request both ranges: To keep an HDR10+ deliverable *alongside* the hybrid, request both ranges:
`-r HYBRID,HDR10P`. `-r HYBRID` on its own muxes only the merged hybrid. `-r HYBRID,HDR10P`. `-r HYBRID` on its own muxes only the merged hybrid.
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
import pytest
from unshackle.core.manifests import DASH
from unshackle.core.tracks import Video
MPD = """<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:scte214="urn:scte:dash:scte214-extensions"
type="static" mediaPresentationDuration="PT4S">
<Period id="p0">
<AdaptationSet contentType="video" mimeType="video/mp4">
<EssentialProperty schemeIdUri="urn:mpeg:mpegB:cicp:ColourPrimaries" value="9"/>
<EssentialProperty schemeIdUri="urn:mpeg:mpegB:cicp:TransferCharacteristics" value="16"/>
<EssentialProperty schemeIdUri="urn:mpeg:mpegB:cicp:MatrixCoefficients" value="9"/>
<Representation id="hdr" codecs="hvc1.2.4.L150.90" width="3840" height="1920" bandwidth="7000000"
supplementalCodecs="dvh1.08.06" supplementalProfiles="db1p">
<BaseURL>hdr.mp4</BaseURL><SegmentBase indexRange="0-99"/>
</Representation>
<Representation id="scte" codecs="hvc1.2.4.L120.90" width="1600" height="800" bandwidth="700000"
scte214:supplementalCodecs="dvh1.08.03">
<BaseURL>scte.mp4</BaseURL><SegmentBase indexRange="0-99"/>
</Representation>
<Representation id="plain" codecs="hvc1.2.4.L90.90" width="1024" height="512" bandwidth="400000">
<BaseURL>plain.mp4</BaseURL><SegmentBase indexRange="0-99"/>
</Representation>
</AdaptationSet>
<AdaptationSet contentType="video" mimeType="video/mp4">
<Representation id="dv" codecs="dvh1.05.06" width="3840" height="1920" bandwidth="6800000"
supplementalCodecs="dvh1.05.06">
<BaseURL>dv.mp4</BaseURL><SegmentBase indexRange="0-99"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>"""
def test_a_dv_codec_in_supplemental_codecs_marks_an_hdr10_track_dv_compatible() -> None:
videos = {
v.data["dash"]["representation_id"]: v
for v in DASH.from_text(MPD, url="https://example.test/m.mpd").to_tracks("en").videos
}
assert {k: (v.range, v.dv_compatible_bitstream) for k, v in videos.items()} == {
"hdr": (Video.Range.HDR10, True),
"scte": (Video.Range.HDR10, True),
"plain": (Video.Range.HDR10, False),
"dv": (Video.Range.DV, False),
}
assert "[H.265, HDR10+DV]" in str(videos["hdr"])
assert "[H.265, HDR10]" in str(videos["plain"])
@pytest.mark.parametrize(
("codecs", "expected"),
[
("dvh1.08.06/db1p", True),
("hvc1.2.4 dvhe.08.06", True),
("hvc1.2.4.L150.90,DVH1.08.06", True),
("cdm4", False),
("hvc1.2.4.L150.90", False),
(None, False),
],
)
def test_has_dv_codec(codecs: str | None, expected: bool) -> None:
assert Video.has_dv_codec(codecs) is expected
@pytest.mark.parametrize(
("codec", "codecs", "supplemental", "expected"),
[
(Video.Codec.HEVC, "hvc1.2.4.L150.90", "dvh1.08.06", True),
(Video.Codec.HEVC, "hev1.2.4.L150.90", "dvhe.08.06/db1p", True),
(Video.Codec.AVC, "avc1.640028", "dvav.09.05/db2g", False),
(Video.Codec.HEVC, "hvc1.2.4.L150.90", "dvav.09.05", False),
(Video.Codec.HEVC, "dvh1.05.06", "dvh1.05.06", False),
(None, None, "dvh1.08.06", False),
],
)
def test_only_an_hevc_track_with_an_hevc_dv_supplemental_codec_is_dv_composite(
codec: Video.Codec | None, codecs: str | None, supplemental: str, expected: bool
) -> None:
assert Video.is_dv_composite(codec, codecs, supplemental) is expected
def test_an_hdr10p_track_with_a_dv_rpu_lists_as_hdr10p_plus_dv() -> None:
video = Video(url="https://example.test/v.mp4", language="en", codec=Video.Codec.HEVC, range_=Video.Range.HDR10P)
video.dv_compatible_bitstream = True
assert "[H.265, HDR10P+DV]" in str(video)
+38
View File
@@ -158,6 +158,44 @@ def test_a_dv_hdr10p_track_used_only_for_its_rpu_stays_an_ingredient() -> None:
assert all(t.hybrid_base_only for t in chosen) and ids(chosen) == {"hdr10-1080", "dvp-360"} assert all(t.hybrid_base_only for t in chosen) and ids(chosen) == {"hdr10-1080", "dvp-360"}
def dv_hdr10(track_id: str, height: int, bitrate: int, range_: Video.Range = Video.Range.HDR10) -> Video:
video = make_video(track_id, range_=range_, height=height, bitrate=bitrate, codec=Video.Codec.HEVC)
video.dv_compatible_bitstream = True
return video
def test_select_hybrid_takes_a_dv_hdr10_track_and_no_profile_5_donor() -> None:
H = Video.Codec.HEVC
tracks = [
dv_hdr10("dvhdr-2160", 2160, 7_000_000),
make_video("dv5-2160", range_=Video.Range.DV, height=2160, bitrate=6_800_000, codec=H),
make_video("dv5-512", range_=Video.Range.DV, height=512, bitrate=400_000, codec=H),
]
chosen = list(filter(Tracks().select_hybrid(tracks, [2160]), tracks))
assert ids(chosen) == {"dvhdr-2160"} and chosen[0].native_hybrid
def test_select_hybrid_prefers_a_finished_hybrid_that_carries_hdr10p() -> None:
tracks = [
dv_hdr10("dvhdr-2160", 2160, 9_000_000),
dv_hdr10("dvhdrp-2160", 2160, 7_000_000, Video.Range.HDR10P),
dv_hdr10p("dvp-2160", 2160, 6_000_000),
]
assert ids(list(filter(Tracks().select_hybrid(tracks, [2160]), tracks))) == {"dvhdrp-2160"}
assert ids(list(filter(Tracks().select_hybrid(tracks, [2160], worst=True), tracks))) == {"dvp-2160"}
def test_select_hybrid_picks_no_base_that_has_no_dv_track_to_take_an_rpu_from() -> None:
H = Video.Codec.HEVC
tracks = [
dv_hdr10("dvhdr-2160", 2160, 7_000_000),
make_video("hdr10p-1080", range_=Video.Range.HDR10P, height=1080, bitrate=4_000_000, codec=H),
]
assert ids(list(filter(Tracks().select_hybrid(tracks, [2160, 1080]), tracks))) == {"dvhdr-2160"}
with_generate = Tracks().select_hybrid(tracks, [2160, 1080], generate_dv=True)
assert ids(list(filter(with_generate, tracks))) == {"dvhdr-2160", "hdr10p-1080"}
def test_merge_dedups_shared_ingredient_and_deliverable() -> None: def test_merge_dedups_shared_ingredient_and_deliverable() -> None:
H = Video.Codec.HEVC H = Video.Codec.HEVC
dv = make_video("dv-1080", range_=Video.Range.DV, height=1080, bitrate=9_000_000, codec=H) dv = make_video("dv-1080", range_=Video.Range.DV, height=1080, bitrate=9_000_000, codec=H)
+13 -5
View File
@@ -80,6 +80,7 @@ from unshackle.core.tracks.attachment import Attachment
from unshackle.core.tracks.dv_fixup import apply_dv_fixup from unshackle.core.tracks.dv_fixup import apply_dv_fixup
from unshackle.core.tracks.hybrid import Hybrid from unshackle.core.tracks.hybrid import Hybrid
from unshackle.core.tracks.track import assert_fragments_decrypted, has_encrypted_sample_entry from unshackle.core.tracks.track import assert_fragments_decrypted, has_encrypted_sample_entry
from unshackle.core.tracks.tracks import at_resolution
from unshackle.core.utilities import ( from unshackle.core.utilities import (
apply_original_language, apply_original_language,
as_requested, as_requested,
@@ -273,7 +274,7 @@ def hybrid_plan(videos: Sequence[Video], generate_dv: bool) -> tuple[list[Video]
"""The hybrid bases and the DV track to pair with them. """The hybrid bases and the DV track to pair with them.
With no DV track, only HDR10+ bases qualify, and only with ``--generate-dv``. A resolution with a With no DV track, only HDR10+ bases qualify, and only with ``--generate-dv``. A resolution with a
DV track that carries HDR10+ gets no base: that track is its hybrid. finished hybrid (``native_hybrid``) gets no base: that track is its hybrid.
""" """
dv_tracks = [v for v in videos if v.range == Video.Range.DV] dv_tracks = [v for v in videos if v.range == Video.Range.DV]
native_heights = {v.height for v in videos if v.native_hybrid} native_heights = {v.height for v in videos if v.native_hybrid}
@@ -2715,7 +2716,7 @@ class dl:
effective_quality = [] effective_quality = []
else: else:
hybrid_filter = title.tracks.select_hybrid( hybrid_filter = title.tracks.select_hybrid(
hybrid_candidate_tracks, quality, worst=worst hybrid_candidate_tracks, quality, worst=worst, generate_dv=generate_dv
) )
hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks)) hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks))
@@ -2777,9 +2778,12 @@ class dl:
hybrid_selected = [] hybrid_selected = []
if hybrid_quality: if hybrid_quality:
hybrid_filter = title.tracks.select_hybrid( hybrid_filter = title.tracks.select_hybrid(
hybrid_candidate_tracks, hybrid_quality, worst=worst hybrid_candidate_tracks, hybrid_quality, worst=worst, generate_dv=generate_dv
) )
hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks)) hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks))
for res in hybrid_quality:
if not any(at_resolution(v, res) for v in hybrid_selected):
self.log.warning(f"Skipping {res}p: nothing at that resolution can make a hybrid")
non_hybrid_selected: list[Video] = [] non_hybrid_selected: list[Video] = []
if non_hybrid_ranges and non_hybrid_tracks: if non_hybrid_ranges and non_hybrid_tracks:
@@ -2840,7 +2844,9 @@ class dl:
if keep_videos and any(r == Video.Range.HYBRID for r in range_): if keep_videos and any(r == Video.Range.HYBRID for r in range_):
gate_failure = hybrid_gate_failure(title.tracks.videos, generate_dv) gate_failure = hybrid_gate_failure(title.tracks.videos, generate_dv)
if not gate_failure and not any(v.range == Video.Range.DV for v in title.tracks.videos): if not gate_failure and not any(
v.range == Video.Range.DV or v.native_hybrid for v in title.tracks.videos
):
self.log.info("No Dolby Vision track: Dolby Vision will be generated from HDR10+") self.log.info("No Dolby Vision track: Dolby Vision will be generated from HDR10+")
if gate_failure: if gate_failure:
@@ -3674,7 +3680,9 @@ class dl:
hybrid_base_ids.update(v.id for v in hybrid_bases) hybrid_base_ids.update(v.id for v in hybrid_bases)
hybrids_made = 0 hybrids_made = 0
for video in (v for v in original_videos if v.native_hybrid): for video in (v for v in original_videos if v.native_hybrid):
self.log.info(f"The {video.height}p DV track carries HDR10+: it is the hybrid") self.log.info(
f"The {video.height}p {video.range_label} track is already the hybrid"
)
hybrids_made += 1 hybrids_made += 1
for group in sorted(standalone_groups, key=group_file_size, reverse=True): for group in sorted(standalone_groups, key=group_file_size, reverse=True):
+1 -1
View File
@@ -1752,7 +1752,7 @@ def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str,
"resolution": f"{track.width}x{track.height}" if track.width and track.height else None, "resolution": f"{track.width}x{track.height}" if track.width and track.height else None,
"fps": track.fps if track.fps else None, "fps": track.fps if track.fps else None,
"range": range_name, "range": range_name,
"range_display": DYNAMIC_RANGE_MAP.get(range_name, range_name) + ("+HDR10P" if track.hdr10plus else ""), "range_display": DYNAMIC_RANGE_MAP.get(track.range_label, track.range_label),
"scan_type": enum_name(track.scan_type) if track.scan_type else None, "scan_type": enum_name(track.scan_type) if track.scan_type else None,
"closed_captions": list(getattr(track, "closed_captions", None) or []), "closed_captions": list(getattr(track, "closed_captions", None) or []),
"dv_compatible_bitstream": bool(getattr(track, "dv_compatible_bitstream", False)), "dv_compatible_bitstream": bool(getattr(track, "dv_compatible_bitstream", False)),
+6 -5
View File
@@ -230,6 +230,7 @@ class DASH:
# mimeType likely specifies the subtitle codec better than `codecs` # mimeType likely specifies the subtitle codec better than `codecs`
codecs = mime_type.split("/")[1] codecs = mime_type.split("/")[1]
video_args: dict[str, Any] = {}
if content_type == "video": if content_type == "video":
track_type = Video track_type = Video
track_codec = Video.Codec.from_codecs(codecs) track_codec = Video.Codec.from_codecs(codecs)
@@ -252,6 +253,9 @@ class DASH:
fps=track_fps or None, fps=track_fps or None,
scan_type=scan_type, scan_type=scan_type,
) )
video_args["dv_compatible_bitstream"] = Video.is_dv_composite(
track_codec, codecs, get("supplementalCodecs")
)
elif content_type == "audio": elif content_type == "audio":
track_type = Audio track_type = Audio
track_codec = Audio.Codec.from_codecs(codecs) track_codec = Audio.Codec.from_codecs(codecs)
@@ -326,6 +330,7 @@ class DASH:
} }
}, },
**track_args, **track_args,
**video_args,
) )
) )
@@ -1161,11 +1166,7 @@ class DASH:
def get_video_range( def get_video_range(
codecs: str, all_supplemental_props: list[Element], all_essential_props: list[Element] codecs: str, all_supplemental_props: list[Element], all_essential_props: list[Element]
) -> Video.Range: ) -> Video.Range:
# TODO: Detect Dolby Vision composite streams in DASH manifests (DV RPU embedded but if Video.has_dv_codec(codecs):
# primary codec is plain hvc1, signaled via a separate AdaptationSet/Representation
# with DV codec strings or DolbyVisionConfigurationBox). When found, mark the track
# with dv_compatible_bitstream=True so DVFixup runs pre-mux. No DASH samples seen yet.
if codecs.startswith(("dva1", "dvav", "dvhe", "dvh1")):
return Video.Range.DV return Video.Range.DV
return Video.Range.from_cicp( return Video.Range.from_cicp(
+11 -16
View File
@@ -222,28 +222,19 @@ class HLS:
tracks = Tracks() tracks = Tracks()
supplemental_codecs = self.supplemental_codecs_by_uri() supplemental_codecs = self.supplemental_codecs_by_uri()
dv_supp_prefixes = ("dva1", "dvav", "dvhe", "dvh1")
for playlist in self.manifest.playlists: for playlist in self.manifest.playlists:
video_codec: Optional[Video.Codec] = None
try: try:
# TODO: Any better way to figure out the primary track type? # TODO: Any better way to figure out the primary track type?
if playlist.stream_info.codecs: if playlist.stream_info.codecs:
Video.Codec.from_codecs(playlist.stream_info.codecs) video_codec = Video.Codec.from_codecs(playlist.stream_info.codecs)
except ValueError: except ValueError:
primary_track_type = Audio primary_track_type = Audio
else: else:
primary_track_type = Video primary_track_type = Video
primary_codecs = (playlist.stream_info.codecs or "").lower() primary_has_dv = Video.has_dv_codec(playlist.stream_info.codecs)
primary_has_dv = any(codec.split(".")[0] in dv_supp_prefixes for codec in primary_codecs.split(","))
supp_codecs_str = supplemental_codecs.get(playlist.uri, "")
supp_dv_codec: Optional[str] = None
for codec in supp_codecs_str.lower().split(","):
token = codec.strip().split("/")[0]
if token.split(".")[0] in dv_supp_prefixes:
supp_dv_codec = token
break
video_range = ( video_range = (
Video.Range.DV if primary_has_dv else Video.Range.from_m3u_range_tag(playlist.stream_info.video_range) Video.Range.DV if primary_has_dv else Video.Range.from_m3u_range_tag(playlist.stream_info.video_range)
@@ -252,7 +243,9 @@ class HLS:
# a DV codec. Range stays whatever VIDEO-RANGE signaled (HDR10/HLG/SDR); DVFixup will # a DV codec. Range stays whatever VIDEO-RANGE signaled (HDR10/HLG/SDR); DVFixup will
# restore DV signaling post-download. HDR10+ SEI in an HDR10 track is found later by the # restore DV signaling post-download. HDR10+ SEI in an HDR10 track is found later by the
# core HDR10+ probe. # core HDR10+ probe.
dv_compatible_bitstream = primary_track_type is Video and not primary_has_dv and supp_dv_codec is not None dv_compatible_bitstream = primary_track_type is Video and Video.is_dv_composite(
video_codec, playlist.stream_info.codecs, supplemental_codecs.get(playlist.uri)
)
tracks.add( tracks.add(
primary_track_type( primary_track_type(
@@ -293,7 +286,9 @@ class HLS:
joc = 0 joc = 0
if media.type == "AUDIO": if media.type == "AUDIO":
track_type = Audio track_type = Audio
codec = HLS.select_audio_codec(audio_codecs_by_group_id.get(media.group_id), media.uri) codec: Union[Audio.Codec, Subtitle.Codec, None] = HLS.select_audio_codec(
audio_codecs_by_group_id.get(media.group_id), media.uri
)
if media.channels and media.channels.endswith("/JOC"): if media.channels and media.channels.endswith("/JOC"):
joc = int(media.channels.split("/JOC")[0]) joc = int(media.channels.split("/JOC")[0])
media.channels = "5.1" media.channels = "5.1"
@@ -354,11 +349,11 @@ class HLS:
try: try:
probe = HLS.probe_ts_info(video.url, self.session) probe = HLS.probe_ts_info(video.url, self.session)
if probe: if probe:
width, height, codec = probe width, height, probed_codec = probe
if not has_resolution: if not has_resolution:
video.width, video.height = width, height video.width, video.height = width, height
if not has_codec: if not has_codec:
video.codec = codec video.codec = probed_codec
# best-effort probe over network + ffprobe; the track stays usable without it # best-effort probe over network + ffprobe; the track stays usable without it
except Exception as e: except Exception as e:
logging.getLogger("HLS").debug(f"TS probe failed for {video.url}: {e!r}") logging.getLogger("HLS").debug(f"TS probe failed for {video.url}: {e!r}")
+3 -3
View File
@@ -1,8 +1,8 @@
""" """
DV exposure for HLS composite HEVC streams. DV exposure for HLS and DASH composite HEVC streams.
Some services deliver DV Profile 8.1 in a track whose primary CODECS is plain Some services deliver DV Profile 8.1 in a track whose primary codec is plain
hvc1, with DV advertised only in SUPPLEMENTAL-CODECS. The fMP4 carries valid DV RPU NALs hvc1, with DV advertised only in HLS SUPPLEMENTAL-CODECS or DASH @supplementalCodecs. The fMP4 carries valid DV RPU NALs
but the container does not signal DV, so muxing the MP4 directly produces an MKV that but the container does not signal DV, so muxing the MP4 directly produces an MKV that
MediaInfo and DV-capable TVs see as plain HDR10/HDR10+. MediaInfo and DV-capable TVs see as plain HDR10/HDR10+.
+12 -6
View File
@@ -445,20 +445,27 @@ class Tracks:
if video not in non_hybrid_selected and not video.native_hybrid: if video not in non_hybrid_selected and not video.native_hybrid:
video.hybrid_base_only = True video.hybrid_base_only = True
def select_hybrid(self, tracks, quality, worst: bool = False): def select_hybrid(self, tracks, quality, worst: bool = False, generate_dv: bool = False):
"""A filter for the hybrid ingredients of each resolution. It sets ``native_hybrid`` on every track. """A filter for the hybrid ingredients of each resolution. It sets ``native_hybrid`` on every track.
A DV track that carries HDR10+ is already a hybrid, so its resolution takes that track and no base. A track that already carries DV and an HDR10 or HDR10+ base (DV+HDR10P, HDR10+DV, HDR10P+DV) is a finished
hybrid, so its resolution takes that track and no base. One that carries HDR10+ comes first. The other
resolutions get a base only when a DV track can give it an RPU, or with ``generate_dv``.
""" """
pick = min if worst else max pick = min if worst else max
natives = {} natives = {}
for res in quality: for res in quality:
candidates = [v for v in tracks if v.range == Video.Range.DV and v.hdr10plus and at_resolution(v, res)] candidates = [v for v in tracks if v.finished_hybrid and at_resolution(v, res)]
if candidates: if candidates:
natives[res] = pick(candidates, key=lambda v: v.bitrate or 0) with_hdr10plus = [v for v in candidates if v.carries_hdr10plus]
natives[res] = pick(with_hdr10plus or candidates, key=lambda v: v.bitrate or 0)
for video in tracks: for video in tracks:
video.native_hybrid = any(video is v for v in natives.values()) video.native_hybrid = any(video is v for v in natives.values())
quality = [res for res in quality if res not in natives] dv_tracks = [v for v in tracks if v.range == Video.Range.DV]
if natives and not dv_tracks and not generate_dv:
quality = [] # no DV track to take an RPU from
else:
quality = [res for res in quality if res not in natives]
# Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata). # Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata).
# Only an HEVC base qualifies, because dovi_tool injects the RPU into an HEVC stream. # Only an HEVC base qualifies, because dovi_tool injects the RPU into an HEVC stream.
@@ -482,7 +489,6 @@ class Tracks:
chosen = pick(candidates, key=lambda v: v.bitrate) chosen = pick(candidates, key=lambda v: v.bitrate)
base_selected.append(chosen) base_selected.append(chosen)
dv_tracks = [v for v in tracks if v.range == Video.Range.DV]
lowest_dv = min(dv_tracks, key=lambda v: v.height) if dv_tracks and (base_selected or not natives) else None lowest_dv = min(dv_tracks, key=lambda v: v.height) if dv_tracks and (base_selected or not natives) else None
def select(x): def select(x):
+37 -2
View File
@@ -338,10 +338,45 @@ class Video(Track):
hdr10plus=data.get("hdr10plus", False), hdr10plus=data.get("hdr10plus", False),
) )
@staticmethod
def dv_fourccs(codecs: Optional[str]) -> set[str]:
"""The Dolby Vision fourccs in a codecs list (comma or space separated, ``/brand`` suffixes allowed)."""
fourccs = {token.split("/")[0].split(".")[0] for token in re.split(r"[\s,]+", (codecs or "").lower())}
return fourccs & {"dva1", "dvav", "dvhe", "dvh1"}
@staticmethod
def has_dv_codec(codecs: Optional[str]) -> bool:
"""Whether a codecs list names a Dolby Vision codec."""
return bool(Video.dv_fourccs(codecs))
@staticmethod
def is_dv_composite(
codec: Optional[Video.Codec], codecs: Optional[str], supplemental_codecs: Optional[str]
) -> bool:
"""Whether an HEVC track names DV only in its supplemental codecs: DV profile 8 on an HDR10, SDR or HLG base."""
return (
codec == Video.Codec.HEVC
and not Video.has_dv_codec(codecs)
and bool(Video.dv_fourccs(supplemental_codecs) & {"dvh1", "dvhe"})
)
@property
def carries_hdr10plus(self) -> bool:
"""Whether the track carries HDR10+, as its range or, on a DV track, found by the HDR10+ probe."""
return self.hdr10plus or self.range == Video.Range.HDR10P
@property
def finished_hybrid(self) -> bool:
"""Whether the track already carries DV and an HDR10 or HDR10+ base, so ``-r HYBRID`` needs no build."""
if self.range == Video.Range.DV:
return self.hdr10plus
return self.dv_compatible_bitstream and self.range in (Video.Range.HDR10, Video.Range.HDR10P)
@property @property
def range_label(self) -> str: def range_label(self) -> str:
"""The range name, with ``+HDR10P`` on a DV track that also carries HDR10+.""" """The range that ``-r`` selects the track by, then any second format it carries: HDR10+DV, DV+HDR10P."""
return self.range.name + ("+HDR10P" if self.hdr10plus else "") label = self.range.name + ("+HDR10P" if self.hdr10plus else "")
return f"{label}+DV" if self.dv_compatible_bitstream and self.range != Video.Range.DV else label
def __str__(self) -> str: def __str__(self) -> str:
return " | ".join( return " | ".join(