mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-03-10 08:29:00 +00:00
fix(hybrid): accept HDR10+ tracks as valid base layer for HYBRID mode
HYBRID mode previously required a plain HDR10 track, rejecting HDR10+ (HDR10P) even though it's a perfectly valid (and superior) base layer. HDR10+ is now preferred over HDR10 when both are available, preserving dynamic metadata in the final DV Profile 8 output.
This commit is contained in:
@@ -1496,24 +1496,33 @@ class dl:
|
|||||||
|
|
||||||
# validate hybrid mode requirements
|
# validate hybrid mode requirements
|
||||||
if any(r == Video.Range.HYBRID for r in range_):
|
if any(r == Video.Range.HYBRID for r in range_):
|
||||||
hdr10_tracks = [v for v in title.tracks.videos if v.range == Video.Range.HDR10]
|
base_tracks = [
|
||||||
|
v for v in title.tracks.videos
|
||||||
|
if v.range in (Video.Range.HDR10, Video.Range.HDR10P)
|
||||||
|
]
|
||||||
dv_tracks = [v for v in title.tracks.videos if v.range == Video.Range.DV]
|
dv_tracks = [v for v in title.tracks.videos if v.range == Video.Range.DV]
|
||||||
|
|
||||||
if not hdr10_tracks and not dv_tracks:
|
if not base_tracks and not dv_tracks:
|
||||||
available_ranges = sorted(set(v.range.name for v in title.tracks.videos))
|
available_ranges = sorted(set(v.range.name for v in title.tracks.videos))
|
||||||
self.log.error("HYBRID mode requires both HDR10 and DV tracks, but neither is available")
|
self.log.error(
|
||||||
|
"HYBRID mode requires both HDR10/HDR10+ and DV tracks, but neither is available"
|
||||||
|
)
|
||||||
self.log.error(
|
self.log.error(
|
||||||
f"Available ranges: {', '.join(available_ranges) if available_ranges else 'none'}"
|
f"Available ranges: {', '.join(available_ranges) if available_ranges else 'none'}"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not hdr10_tracks:
|
elif not base_tracks:
|
||||||
available_ranges = sorted(set(v.range.name for v in title.tracks.videos))
|
available_ranges = sorted(set(v.range.name for v in title.tracks.videos))
|
||||||
self.log.error("HYBRID mode requires both HDR10 and DV tracks, but only DV is available")
|
self.log.error(
|
||||||
|
"HYBRID mode requires both HDR10/HDR10+ and DV tracks, but only DV is available"
|
||||||
|
)
|
||||||
self.log.error(f"Available ranges: {', '.join(available_ranges)}")
|
self.log.error(f"Available ranges: {', '.join(available_ranges)}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not dv_tracks:
|
elif not dv_tracks:
|
||||||
available_ranges = sorted(set(v.range.name for v in title.tracks.videos))
|
available_ranges = sorted(set(v.range.name for v in title.tracks.videos))
|
||||||
self.log.error("HYBRID mode requires both HDR10 and DV tracks, but only HDR10 is available")
|
self.log.error(
|
||||||
|
"HYBRID mode requires both HDR10/HDR10+ and DV tracks, but only HDR10 is available"
|
||||||
|
)
|
||||||
self.log.error(f"Available ranges: {', '.join(available_ranges)}")
|
self.log.error(f"Available ranges: {', '.join(available_ranges)}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -2038,12 +2047,15 @@ class dl:
|
|||||||
# Hybrid mode: process DV and HDR10 tracks separately for each resolution
|
# Hybrid mode: process DV and HDR10 tracks separately for each resolution
|
||||||
self.log.info("Processing Hybrid HDR10+DV tracks...")
|
self.log.info("Processing Hybrid HDR10+DV tracks...")
|
||||||
|
|
||||||
# Group video tracks by resolution
|
# Group video tracks by resolution (prefer HDR10+ over HDR10 as base)
|
||||||
resolutions_processed = set()
|
resolutions_processed = set()
|
||||||
hdr10_tracks = [v for v in title.tracks.videos if v.range == Video.Range.HDR10]
|
base_tracks_list = [
|
||||||
|
v for v in title.tracks.videos
|
||||||
|
if v.range in (Video.Range.HDR10P, Video.Range.HDR10)
|
||||||
|
]
|
||||||
dv_tracks = [v for v in title.tracks.videos if v.range == Video.Range.DV]
|
dv_tracks = [v for v in title.tracks.videos if v.range == Video.Range.DV]
|
||||||
|
|
||||||
for hdr10_track in hdr10_tracks:
|
for hdr10_track in base_tracks_list:
|
||||||
resolution = hdr10_track.height
|
resolution = hdr10_track.height
|
||||||
if resolution in resolutions_processed:
|
if resolution in resolutions_processed:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ class Hybrid:
|
|||||||
has_hdr10 = any(video.range == Video.Range.HDR10 for video in self.videos)
|
has_hdr10 = any(video.range == Video.Range.HDR10 for video in self.videos)
|
||||||
has_hdr10p = any(video.range == Video.Range.HDR10P for video in self.videos)
|
has_hdr10p = any(video.range == Video.Range.HDR10P for video in self.videos)
|
||||||
|
|
||||||
if not has_hdr10:
|
if not has_hdr10 and not has_hdr10p:
|
||||||
raise ValueError("No HDR10 track available for hybrid processing.")
|
raise ValueError("No HDR10 or HDR10+ track available for hybrid processing.")
|
||||||
|
|
||||||
# If we have HDR10+ but no DV, we can convert HDR10+ to DV
|
# If we have HDR10+ but no DV, we can convert HDR10+ to DV
|
||||||
if not has_dv and has_hdr10p:
|
if not has_dv and has_hdr10p:
|
||||||
@@ -113,10 +113,11 @@ class Hybrid:
|
|||||||
|
|
||||||
# Edit L6 with actual luminance values from RPU, then L5 active area
|
# Edit L6 with actual luminance values from RPU, then L5 active area
|
||||||
self.level_6()
|
self.level_6()
|
||||||
hdr10_video = next((v for v in videos if v.range == Video.Range.HDR10), None)
|
base_video = next(
|
||||||
hdr10_input = hdr10_video.path if hdr10_video else None
|
(v for v in videos if v.range in (Video.Range.HDR10, Video.Range.HDR10P)), None
|
||||||
if hdr10_input:
|
)
|
||||||
self.level_5(hdr10_input)
|
if base_video and base_video.path:
|
||||||
|
self.level_5(base_video.path)
|
||||||
|
|
||||||
self.injecting()
|
self.injecting()
|
||||||
|
|
||||||
@@ -557,12 +558,6 @@ class Hybrid:
|
|||||||
config.directories.temp / self.rpu_file,
|
config.directories.temp / self.rpu_file,
|
||||||
]
|
]
|
||||||
|
|
||||||
# If we converted from HDR10+, optionally remove HDR10+ metadata during injection
|
|
||||||
# Default to removing HDR10+ metadata since we're converting to DV
|
|
||||||
if self.hdr10plus_to_dv:
|
|
||||||
inject_cmd.append("--drop-hdr10plus")
|
|
||||||
console.status("Removing HDR10+ metadata during injection")
|
|
||||||
|
|
||||||
inject_cmd.extend(["-o", config.directories.temp / self.hevc_file])
|
inject_cmd.extend(["-o", config.directories.temp / self.hevc_file])
|
||||||
|
|
||||||
inject = subprocess.run(
|
inject = subprocess.run(
|
||||||
|
|||||||
@@ -278,23 +278,30 @@ class Tracks:
|
|||||||
self.subtitles = list(filter(x, self.subtitles))
|
self.subtitles = list(filter(x, self.subtitles))
|
||||||
|
|
||||||
def select_hybrid(self, tracks, quality):
|
def select_hybrid(self, tracks, quality):
|
||||||
hdr10_tracks = [
|
# Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata)
|
||||||
v
|
base_ranges = (Video.Range.HDR10P, Video.Range.HDR10)
|
||||||
for v in tracks
|
base_tracks = []
|
||||||
if v.range == Video.Range.HDR10 and (v.height in quality or int(v.width * 9 / 16) in quality)
|
for range_type in base_ranges:
|
||||||
]
|
base_tracks = [
|
||||||
hdr10 = []
|
v
|
||||||
|
for v in tracks
|
||||||
|
if v.range == range_type and (v.height in quality or int(v.width * 9 / 16) in quality)
|
||||||
|
]
|
||||||
|
if base_tracks:
|
||||||
|
break
|
||||||
|
|
||||||
|
base_selected = []
|
||||||
for res in quality:
|
for res in quality:
|
||||||
candidates = [v for v in hdr10_tracks if v.height == res or int(v.width * 9 / 16) == res]
|
candidates = [v for v in base_tracks if v.height == res or int(v.width * 9 / 16) == res]
|
||||||
if candidates:
|
if candidates:
|
||||||
best = max(candidates, key=lambda v: v.bitrate) # assumes .bitrate exists
|
best = max(candidates, key=lambda v: v.bitrate)
|
||||||
hdr10.append(best)
|
base_selected.append(best)
|
||||||
|
|
||||||
dv_tracks = [v for v in tracks if v.range == Video.Range.DV]
|
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 else None
|
lowest_dv = min(dv_tracks, key=lambda v: v.height) if dv_tracks else None
|
||||||
|
|
||||||
def select(x):
|
def select(x):
|
||||||
if x in hdr10:
|
if x in base_selected:
|
||||||
return True
|
return True
|
||||||
if lowest_dv and x is lowest_dv:
|
if lowest_dv and x is lowest_dv:
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user