Complete High-quality profile and add menus to split discs
High profile is now fully automatic instead of relying on the manual "adjust disc usage" button: - compute_high_profile_bitrate() (pure, unit-tested) picks the fewest discs that hold the content down to a quality floor, then fills each disc; a 2h movie lands on one filled disc (~3750k), a 4h movie on two, a 30min clip on one near the 9000k cap. - apply_high_profile_bitrate() applies that rate to every title and forces 2-pass; it runs before split planning so the existing splitter reuses it. Split discs now get a per-disc menu (listing that disc's titles) when the project has a menu enabled, honoring the startup/play-all settings; they still auto-play when menus are off. Fix: _adjust_audio_bitrate capped its table at 384 (an MP2 limit) and silently clamped the High profile's 448k AC3 down to 384; extend the table to AC3 rates (448/512/640) in both the ffmpeg and avconv backends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5fe1013be8
commit
3d1ce504b9
3 changed files with 107 additions and 9 deletions
|
|
@ -538,11 +538,14 @@ class avconv(devedeng.avbase.avbase):
|
|||
self.command_var.append(output_file)
|
||||
|
||||
def _adjust_audio_bitrate(self, bitrate):
|
||||
br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384]
|
||||
# includes AC3 rates above the MP2 ceiling (384) so 448k AC3 isn't
|
||||
# silently clamped down
|
||||
br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
|
||||
448, 512, 640]
|
||||
for a in br:
|
||||
if a >= bitrate:
|
||||
return a
|
||||
return 384
|
||||
return br[-1]
|
||||
|
||||
def _avconv_text_subtitle_filter(self, file_project):
|
||||
""" Build a `subtitles=` filter to burn a text-based embedded subtitle
|
||||
|
|
|
|||
|
|
@ -596,11 +596,14 @@ class ffmpeg(devedeng.executor.executor):
|
|||
print(self.command_var)
|
||||
|
||||
def _adjust_audio_bitrate(self, bitrate):
|
||||
br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384]
|
||||
# includes AC3 rates above the MP2 ceiling (384) so the High profile's
|
||||
# 448k AC3 isn't silently clamped down
|
||||
br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
|
||||
448, 512, 640]
|
||||
for a in br:
|
||||
if a >= bitrate:
|
||||
return a
|
||||
return 384
|
||||
return br[-1]
|
||||
|
||||
@staticmethod
|
||||
def _escape_subtitle_path(path):
|
||||
|
|
|
|||
|
|
@ -43,6 +43,39 @@ import devedeng.help
|
|||
import devedeng.separator
|
||||
|
||||
|
||||
def compute_high_profile_bitrate(total_runtime_s, total_audio_kbits,
|
||||
disc_capacity_kbytes, menu_kbytes=0.0,
|
||||
cap=9000, floor=3000):
|
||||
""" Pure helper for the High-quality profile (no GTK), so it can be tested.
|
||||
|
||||
Strategy ("fewest discs, then fill"): use the fewest discs that can
|
||||
hold the content without dropping below the quality floor (densest
|
||||
packing), then raise the video bitrate to exactly fill those discs
|
||||
(clamped to [floor, cap]). A short movie ends up on one disc near the
|
||||
cap; a long one spreads across the minimum number of discs and fills
|
||||
each, rather than spilling onto extra discs at maximum bitrate.
|
||||
|
||||
Returns (n_discs, video_bitrate_kbps).
|
||||
"""
|
||||
if total_runtime_s <= 0:
|
||||
return (1, cap)
|
||||
disc_kbits = disc_capacity_kbytes * 8.0
|
||||
menu_kbits = menu_kbytes * 8.0
|
||||
audio_kbits = total_audio_kbits
|
||||
# how big is the content at the LOWEST acceptable quality? that gives the
|
||||
# densest packing, i.e. the fewest discs needed
|
||||
content_at_floor_kbits = (floor * total_runtime_s) + audio_kbits + menu_kbits
|
||||
n = max(1, int(math.ceil(content_at_floor_kbits / disc_kbits)))
|
||||
# fill those n discs: spread the available video budget over the runtime
|
||||
video_budget_kbits = (n * disc_kbits) - audio_kbits - menu_kbits
|
||||
video_bitrate = video_budget_kbits / total_runtime_s
|
||||
if video_bitrate > cap:
|
||||
video_bitrate = cap
|
||||
if video_bitrate < floor:
|
||||
video_bitrate = floor
|
||||
return (n, int(video_bitrate))
|
||||
|
||||
|
||||
def _movie_chapter_seconds(movie):
|
||||
""" Returns a sorted list of candidate chapter-cut times (seconds) for a
|
||||
movie: the automatic chapters (every chapter_size minutes) merged with
|
||||
|
|
@ -629,6 +662,46 @@ class devede_project:
|
|||
plan = plan_disc_split(movies, disc_capacity, first_disc_reserved)
|
||||
return plan
|
||||
|
||||
def apply_high_profile_bitrate(self):
|
||||
""" For the High-quality profile: compute the fill-the-disc video
|
||||
bitrate (fewest discs at the cap, then fill) and apply it to every
|
||||
movie, and force 2-pass encoding for accurate VBR. After this the
|
||||
normal split planner produces the right number of discs because the
|
||||
content now fills them. No-op for non-DVD or other profiles. """
|
||||
if self.disc_type != "dvd":
|
||||
return
|
||||
if getattr(self.config, "encode_profile", "compatibility") != "high":
|
||||
return
|
||||
|
||||
movies = [m for m in self.get_all_files()
|
||||
if m.element_type == "file_movie"]
|
||||
if not movies:
|
||||
return
|
||||
|
||||
total_runtime = 0.0
|
||||
total_audio_kbits = 0.0
|
||||
for m in movies:
|
||||
length = float(m.original_length)
|
||||
total_runtime += length
|
||||
audio_streams = max(1, getattr(m, "audio_streams", 1))
|
||||
sub_rate = 8 * len(getattr(m, "subtitles_list", []))
|
||||
# High profile uses AC3 448k audio
|
||||
total_audio_kbits += (448 * audio_streams + sub_rate) * length
|
||||
|
||||
disc_capacity = self.get_dvd_size()[0] * 1000.0 # kBytes
|
||||
menu_kbytes = 0.0
|
||||
if self.wcreate_menu.get_active():
|
||||
menu_kbytes = float(self.menu.get_estimated_size())
|
||||
|
||||
n_discs, video_bitrate = compute_high_profile_bitrate(
|
||||
total_runtime, total_audio_kbits, disc_capacity, menu_kbytes)
|
||||
|
||||
for m in movies:
|
||||
m.set_auto_video_audio_rate(video_bitrate, 448)
|
||||
m.video_rate_automatic = True # use the computed auto rate
|
||||
m.two_pass_encoding = True # VBR accuracy
|
||||
self.refresh_disc_usage()
|
||||
|
||||
def _build_profile_selector(self):
|
||||
""" Build the DVD encode-profile selector (Compatibility / High quality
|
||||
/ Custom) and pack it next to the disc-size combo. When "Custom" is
|
||||
|
|
@ -730,12 +803,19 @@ class devede_project:
|
|||
|
||||
def _build_split_dvd(self, run_window, data, split_plan):
|
||||
""" Author each disc of a split DVD project into its own subfolder and
|
||||
emit one ISO per disc (name-disc1.iso, name-disc2.iso, ...). Split
|
||||
discs are authored without a menu: each plays its segment(s). """
|
||||
emit one ISO per disc (name-disc1.iso, name-disc2.iso, ...).
|
||||
|
||||
If the project has a menu enabled, each disc gets its own menu
|
||||
listing that disc's titles; otherwise each disc auto-plays its
|
||||
segment(s). """
|
||||
cv = devedeng.converter.converter.get_converter()
|
||||
title = self.menu.title_text if self.menu.title_text else ""
|
||||
title = title.strip()
|
||||
menuWide = (self.menu.menu_aspect_ratio == "menu_aspect_16_9")
|
||||
with_menu = (self.disc_type == "dvd") and self.wcreate_menu.get_active()
|
||||
start_with_menu = with_menu and (
|
||||
self.menu.at_startup == "menu_show_at_startup")
|
||||
play_all = self.menu.play_all_c
|
||||
|
||||
last_iso = None
|
||||
for disc_index, segments in enumerate(split_plan):
|
||||
|
|
@ -767,12 +847,21 @@ class devede_project:
|
|||
else:
|
||||
segment_lengths.append(None) # whole, unsplit movie
|
||||
|
||||
# build this disc's own menu from its titles, if enabled
|
||||
menu_entries = None
|
||||
if with_menu:
|
||||
menu_procs, menu_entries = self.menu.create_dvd_menus(
|
||||
seg_movies, disc_path)
|
||||
for mp in menu_procs:
|
||||
run_window.add_process(mp)
|
||||
deps.append(mp)
|
||||
|
||||
dvdauthor = devedeng.dvdauthor_converter.dvdauthor_converter()
|
||||
# no menu on split discs: pass menu_entries=None, start_with_menu=False
|
||||
dvdauthor.create_dvd_project(disc_path,
|
||||
"{:s}-disc{:d}".format(data.name, disc_no),
|
||||
seg_movies, None, False,
|
||||
False, menuWide, segment_lengths)
|
||||
seg_movies, menu_entries,
|
||||
start_with_menu, play_all, menuWide,
|
||||
segment_lengths)
|
||||
for d in deps:
|
||||
dvdauthor.add_dependency(d)
|
||||
run_window.add_process(dvdauthor)
|
||||
|
|
@ -839,6 +928,9 @@ class devede_project:
|
|||
# other formats keep their original single-image behaviour.
|
||||
split_plan = None
|
||||
if self.disc_type == "dvd":
|
||||
# High profile: derive the fill-the-disc bitrate first so the split
|
||||
# planner sees the real per-disc sizes (this also enables 2-pass)
|
||||
self.apply_high_profile_bitrate()
|
||||
plan = self.plan_disc_split()
|
||||
if len(plan) > 1:
|
||||
choice = self._ask_split_choice(len(plan))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue