mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
feat(dash): flag dv profile 8 tracks from supplementalCodecs and use hdr10+dv as the hybrid
This commit is contained in:
@@ -454,7 +454,8 @@ the API surface.
|
||||
### Dolby Vision and hybrid
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -145,6 +145,21 @@ unshackle dl -v hevc,avc EXAMPLE 81234567
|
||||
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"
|
||||
`-r HYBRID` makes a single hybrid track by injecting the Dolby Vision RPU
|
||||
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,
|
||||
the title fails.
|
||||
|
||||
A DV track that carries HDR10+ (`DV+HDR10P` in `--list`) is already a hybrid. At a resolution
|
||||
that has one, `-r HYBRID` selects the DV+HDR10P track with the highest bitrate (the lowest with
|
||||
`--worst`) and muxes it as it is, with no base track and no dovi_tool step. The other
|
||||
resolutions get a hybrid that unshackle makes.
|
||||
A track that is `DV+HDR10P`, `HDR10+DV` or `HDR10P+DV` in `--list` already carries Dolby Vision and an HDR10
|
||||
or HDR10+ base, so it is already a hybrid. At a resolution that has one, `-r HYBRID` selects it
|
||||
and muxes it as it is, with no base track and no dovi_tool step. A track that carries HDR10+
|
||||
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:
|
||||
`-r HYBRID,HDR10P`. `-r HYBRID` on its own muxes only the merged hybrid.
|
||||
|
||||
@@ -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)
|
||||
@@ -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"}
|
||||
|
||||
|
||||
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:
|
||||
H = Video.Codec.HEVC
|
||||
dv = make_video("dv-1080", range_=Video.Range.DV, height=1080, bitrate=9_000_000, codec=H)
|
||||
|
||||
@@ -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.hybrid import Hybrid
|
||||
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 (
|
||||
apply_original_language,
|
||||
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.
|
||||
|
||||
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]
|
||||
native_heights = {v.height for v in videos if v.native_hybrid}
|
||||
@@ -2715,7 +2716,7 @@ class dl:
|
||||
effective_quality = []
|
||||
else:
|
||||
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))
|
||||
|
||||
@@ -2777,9 +2778,12 @@ class dl:
|
||||
hybrid_selected = []
|
||||
if hybrid_quality:
|
||||
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))
|
||||
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] = []
|
||||
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_):
|
||||
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+")
|
||||
|
||||
if gate_failure:
|
||||
@@ -3674,7 +3680,9 @@ class dl:
|
||||
hybrid_base_ids.update(v.id for v in hybrid_bases)
|
||||
hybrids_made = 0
|
||||
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
|
||||
|
||||
for group in sorted(standalone_groups, key=group_file_size, reverse=True):
|
||||
|
||||
@@ -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,
|
||||
"fps": track.fps if track.fps else None,
|
||||
"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,
|
||||
"closed_captions": list(getattr(track, "closed_captions", None) or []),
|
||||
"dv_compatible_bitstream": bool(getattr(track, "dv_compatible_bitstream", False)),
|
||||
|
||||
@@ -230,6 +230,7 @@ class DASH:
|
||||
# mimeType likely specifies the subtitle codec better than `codecs`
|
||||
codecs = mime_type.split("/")[1]
|
||||
|
||||
video_args: dict[str, Any] = {}
|
||||
if content_type == "video":
|
||||
track_type = Video
|
||||
track_codec = Video.Codec.from_codecs(codecs)
|
||||
@@ -252,6 +253,9 @@ class DASH:
|
||||
fps=track_fps or None,
|
||||
scan_type=scan_type,
|
||||
)
|
||||
video_args["dv_compatible_bitstream"] = Video.is_dv_composite(
|
||||
track_codec, codecs, get("supplementalCodecs")
|
||||
)
|
||||
elif content_type == "audio":
|
||||
track_type = Audio
|
||||
track_codec = Audio.Codec.from_codecs(codecs)
|
||||
@@ -326,6 +330,7 @@ class DASH:
|
||||
}
|
||||
},
|
||||
**track_args,
|
||||
**video_args,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1161,11 +1166,7 @@ class DASH:
|
||||
def get_video_range(
|
||||
codecs: str, all_supplemental_props: list[Element], all_essential_props: list[Element]
|
||||
) -> Video.Range:
|
||||
# TODO: Detect Dolby Vision composite streams in DASH manifests (DV RPU embedded but
|
||||
# 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")):
|
||||
if Video.has_dv_codec(codecs):
|
||||
return Video.Range.DV
|
||||
|
||||
return Video.Range.from_cicp(
|
||||
|
||||
@@ -222,28 +222,19 @@ class HLS:
|
||||
tracks = Tracks()
|
||||
|
||||
supplemental_codecs = self.supplemental_codecs_by_uri()
|
||||
dv_supp_prefixes = ("dva1", "dvav", "dvhe", "dvh1")
|
||||
|
||||
for playlist in self.manifest.playlists:
|
||||
video_codec: Optional[Video.Codec] = None
|
||||
try:
|
||||
# TODO: Any better way to figure out the primary track type?
|
||||
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:
|
||||
primary_track_type = Audio
|
||||
else:
|
||||
primary_track_type = Video
|
||||
|
||||
primary_codecs = (playlist.stream_info.codecs or "").lower()
|
||||
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
|
||||
primary_has_dv = Video.has_dv_codec(playlist.stream_info.codecs)
|
||||
|
||||
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
|
||||
# restore DV signaling post-download. HDR10+ SEI in an HDR10 track is found later by the
|
||||
# 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(
|
||||
primary_track_type(
|
||||
@@ -293,7 +286,9 @@ class HLS:
|
||||
joc = 0
|
||||
if media.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"):
|
||||
joc = int(media.channels.split("/JOC")[0])
|
||||
media.channels = "5.1"
|
||||
@@ -354,11 +349,11 @@ class HLS:
|
||||
try:
|
||||
probe = HLS.probe_ts_info(video.url, self.session)
|
||||
if probe:
|
||||
width, height, codec = probe
|
||||
width, height, probed_codec = probe
|
||||
if not has_resolution:
|
||||
video.width, video.height = width, height
|
||||
if not has_codec:
|
||||
video.codec = codec
|
||||
video.codec = probed_codec
|
||||
# best-effort probe over network + ffprobe; the track stays usable without it
|
||||
except Exception as e:
|
||||
logging.getLogger("HLS").debug(f"TS probe failed for {video.url}: {e!r}")
|
||||
|
||||
@@ -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
|
||||
hvc1, with DV advertised only in SUPPLEMENTAL-CODECS. The fMP4 carries valid DV RPU NALs
|
||||
Some services deliver DV Profile 8.1 in a track whose primary codec is plain
|
||||
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
|
||||
MediaInfo and DV-capable TVs see as plain HDR10/HDR10+.
|
||||
|
||||
|
||||
@@ -445,20 +445,27 @@ class Tracks:
|
||||
if video not in non_hybrid_selected and not video.native_hybrid:
|
||||
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 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
|
||||
natives = {}
|
||||
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:
|
||||
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:
|
||||
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).
|
||||
# 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)
|
||||
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
|
||||
|
||||
def select(x):
|
||||
|
||||
@@ -338,10 +338,45 @@ class Video(Track):
|
||||
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
|
||||
def range_label(self) -> str:
|
||||
"""The range name, with ``+HDR10P`` on a DV track that also carries HDR10+."""
|
||||
return self.range.name + ("+HDR10P" if self.hdr10plus else "")
|
||||
"""The range that ``-r`` selects the track by, then any second format it carries: HDR10+DV, DV+HDR10P."""
|
||||
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:
|
||||
return " | ".join(
|
||||
|
||||
Reference in New Issue
Block a user