Add DVD encode profiles and subtitle hardsub burn-in
Introduce three project-level DVD encode profiles (Compatibility 4500k / High quality VBR / Custom) via new config keys, and burn a chosen embedded subtitle track into the picture: - file_movie carries the hardsub selection (index/kind), persists it, and builds a subtitle-picker combobox in its properties page; set_final_rates maps the profile to the final video/audio bitrate. - ffmpeg burns text subs with the subtitles= filter after scaling and image subs with an overlay filtergraph before scaling; NTSC DVD audio is now AC3 for all profiles. A start_offset (-ss) is threaded through for segmenting. - avconv gets the text-sub path and start_offset for parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a158119d41
commit
8b041f0725
4 changed files with 310 additions and 13 deletions
|
|
@ -133,8 +133,9 @@ class ffmpeg(devedeng.executor.executor):
|
|||
self._filter_sep = ","
|
||||
|
||||
|
||||
def convert_file(self, file_project, output_file, video_length, pass2=False):
|
||||
def convert_file(self, file_project, output_file, video_length, pass2=False, start_offset=0):
|
||||
|
||||
self.start_offset = start_offset
|
||||
if file_project.two_pass_encoding:
|
||||
if pass2:
|
||||
self.text = _("Converting %(X)s (pass 2)") % {
|
||||
|
|
@ -144,7 +145,8 @@ class ffmpeg(devedeng.executor.executor):
|
|||
"X": file_project.title_name}
|
||||
# Prepare the converting process for the second pass
|
||||
tmp = devedeng.ffmpeg.ffmpeg()
|
||||
tmp.convert_file(file_project, output_file, video_length, True)
|
||||
tmp.convert_file(file_project, output_file, video_length, True,
|
||||
start_offset)
|
||||
# it deppends of this process
|
||||
tmp.add_dependency(self)
|
||||
# add it as a child process of this one
|
||||
|
|
@ -162,12 +164,24 @@ class ffmpeg(devedeng.executor.executor):
|
|||
# codification
|
||||
second_pass = True
|
||||
|
||||
# whether this encode burns in an image-based (PGS/VOBSUB) subtitle via
|
||||
# an overlay filtergraph; only applies on the (final) encoding pass and
|
||||
# when we are actually re-encoding the video
|
||||
overlay_subs = (second_pass
|
||||
and (not file_project.no_reencode_audio_video)
|
||||
and self._image_overlay_active(file_project))
|
||||
|
||||
if (video_length == 0):
|
||||
self.final_length = file_project.original_length
|
||||
else:
|
||||
self.final_length = video_length
|
||||
self.command_var = []
|
||||
self.command_var.append("ffmpeg")
|
||||
# input seek (for splitting a long movie across discs); placed before
|
||||
# -i so ffmpeg seeks quickly and the output starts at offset 0
|
||||
if start_offset and start_offset > 0:
|
||||
self.command_var.append("-ss")
|
||||
self.command_var.append(str(start_offset))
|
||||
self.command_var.append("-i")
|
||||
self.command_var.append(file_project.file_name)
|
||||
|
||||
|
|
@ -180,10 +194,18 @@ class ffmpeg(devedeng.executor.executor):
|
|||
self.command_var.append("-itsoffset")
|
||||
self.command_var.append(str(file_project.audio_delay))
|
||||
|
||||
if start_offset and start_offset > 0:
|
||||
self.command_var.append("-ss")
|
||||
self.command_var.append(str(start_offset))
|
||||
self.command_var.append("-i")
|
||||
self.command_var.append(file_project.file_name)
|
||||
self.command_var.append("-map")
|
||||
self.command_var.append("1:" + str(file_project.video_list[0]))
|
||||
if overlay_subs:
|
||||
# video comes from the overlay filtergraph output, mapped later
|
||||
self.command_var.append("-map")
|
||||
self.command_var.append("[vsub]")
|
||||
else:
|
||||
self.command_var.append("-map")
|
||||
self.command_var.append("1:" + str(file_project.video_list[0]))
|
||||
if (not file_project.copy_sound) and (not file_project.no_reencode_audio_video):
|
||||
for l in file_project.audio_list:
|
||||
self.command_var.append("-map")
|
||||
|
|
@ -255,9 +277,23 @@ class ffmpeg(devedeng.executor.executor):
|
|||
str(file_project.width_final) + ":" + \
|
||||
str(file_project.height_final)
|
||||
|
||||
if cmd_line != "":
|
||||
self.command_var.append("-vf")
|
||||
self.command_var.append(cmd_line)
|
||||
# --- subtitle burn-in (hardsub) ---------------------------------
|
||||
# Image-based subs (PGS/VOBSUB) must be overlaid via filter_complex
|
||||
# BEFORE scaling, so they stay aligned with the source frame; text
|
||||
# subs are rendered by libass AFTER scaling, as the last -vf entry.
|
||||
if overlay_subs:
|
||||
self._apply_image_overlay(file_project, cmd_line)
|
||||
cmd_line = "" # consumed by filter_complex above
|
||||
else:
|
||||
if second_pass:
|
||||
text_filter = self.get_hardsub_text_filter(file_project)
|
||||
if text_filter is not None:
|
||||
if (cmd_line != ""):
|
||||
cmd_line += self._filter_sep
|
||||
cmd_line += text_filter
|
||||
if cmd_line != "":
|
||||
self.command_var.append("-vf")
|
||||
self.command_var.append(cmd_line)
|
||||
|
||||
self.command_var.append("-y")
|
||||
|
||||
|
|
@ -304,7 +340,13 @@ class ffmpeg(devedeng.executor.executor):
|
|||
self.command_var.append("ac3")
|
||||
else:
|
||||
self.command_var.append("-c:a")
|
||||
if file_project.format_pal:
|
||||
# PAL always uses AC3; for NTSC the original used MP2,
|
||||
# but the DVD-safe profiles use AC3 (matches the known-
|
||||
# good baseline and is more broadly player-compatible)
|
||||
profile = getattr(self.config, "encode_profile",
|
||||
"compatibility")
|
||||
if file_project.format_pal or profile in (
|
||||
"compatibility", "high", "custom"):
|
||||
self.command_var.append("ac3")
|
||||
else:
|
||||
self.command_var.append("mp2")
|
||||
|
|
@ -532,6 +574,73 @@ class ffmpeg(devedeng.executor.executor):
|
|||
return a
|
||||
return 384
|
||||
|
||||
@staticmethod
|
||||
def _escape_subtitle_path(path):
|
||||
""" Escape a file path for use inside an ffmpeg filtergraph argument.
|
||||
The `subtitles`/`ass` filters parse ':' and '\\' specially, and the
|
||||
whole option may be wrapped in single quotes by the caller. """
|
||||
# backslash first, then the filtergraph-special characters
|
||||
path = path.replace("\\", "\\\\")
|
||||
path = path.replace(":", "\\:")
|
||||
path = path.replace("'", "\\'")
|
||||
path = path.replace("[", "\\[").replace("]", "\\]")
|
||||
path = path.replace(",", "\\,")
|
||||
return path
|
||||
|
||||
def _hardsub_relative_index(self, file_project):
|
||||
""" Returns the 0-based position of the chosen hardsub track among the
|
||||
file's subtitle streams (what ffmpeg's `subtitles` filter `si=`
|
||||
expects), or None if no track is selected / found. """
|
||||
index = getattr(file_project, "hardsub_index", -1)
|
||||
if index is None or index < 0:
|
||||
return None
|
||||
subs = getattr(file_project, "embedded_subtitles", []) or []
|
||||
for rel, track in enumerate(subs):
|
||||
if track["index"] == index:
|
||||
return rel
|
||||
return None
|
||||
|
||||
def _image_overlay_active(self, file_project):
|
||||
""" True when an image-based (PGS/VOBSUB) subtitle track is selected for
|
||||
burn-in, which requires an overlay filtergraph rather than -vf. """
|
||||
if getattr(file_project, "hardsub_kind", "text") != "image":
|
||||
return False
|
||||
return self._hardsub_relative_index(file_project) is not None
|
||||
|
||||
def _apply_image_overlay(self, file_project, pre_chain):
|
||||
""" Replaces the plain -vf path with a -filter_complex that overlays the
|
||||
bitmap subtitle stream onto the video and then applies the same
|
||||
scaling/padding chain. The chosen subtitle stream is taken from the
|
||||
primary input (index 1, the one used for -map 1:video).
|
||||
|
||||
Overlay happens at source resolution; `pre_chain` (crop/pad/scale,
|
||||
possibly empty) is applied to the overlaid result so the final size
|
||||
and aspect are identical to the no-subtitle path. """
|
||||
rel = self._hardsub_relative_index(file_project)
|
||||
video_idx = file_project.video_list[0]
|
||||
# input 1 is the one mapped for video (see -map 1:video above)
|
||||
graph = "[1:%d][1:s:%d]overlay" % (video_idx, rel)
|
||||
if pre_chain:
|
||||
graph += self._filter_sep + pre_chain
|
||||
graph += "[vsub]"
|
||||
self.command_var.append("-filter_complex")
|
||||
self.command_var.append(graph)
|
||||
self._overlay_video_label = "[vsub]"
|
||||
|
||||
def get_hardsub_text_filter(self, file_project):
|
||||
""" Builds the `subtitles=` filtergraph entry to burn a text-based
|
||||
embedded subtitle track into the picture, or None if not applicable.
|
||||
Must run AFTER scaling so the rendered text matches the DVD size. """
|
||||
if getattr(file_project, "hardsub_kind", "text") != "text":
|
||||
return None
|
||||
rel = self._hardsub_relative_index(file_project)
|
||||
if rel is None:
|
||||
return None
|
||||
path = self._escape_subtitle_path(file_project.file_name)
|
||||
# original_size used so libass renders at source res; ffmpeg then maps
|
||||
# onto the already-scaled frame. The filter is placed last in the chain.
|
||||
return "subtitles='%s':si=%d" % (path, rel)
|
||||
|
||||
def create_menu_mpeg(self, n_page, background_music, sound_length, pal, video_rate, audio_rate, output_path, use_mp2, widescreen):
|
||||
|
||||
self.n_page = n_page
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue