from ..compat import compat_str
from ..utils import (
+ determine_ext,
dfxp2srt,
encodeArgument,
encodeFilename,
def probe_executable(self):
return self._paths[self.probe_basename]
+ @staticmethod
+ def stream_copy_opts(copy=True, *, ext=None):
+ yield from ('-map', '0')
+ # Don't copy Apple TV chapters track, bin_data
+ # See https://github.com/yt-dlp/yt-dlp/issues/2, #19042, #19024, https://trac.ffmpeg.org/ticket/6016
+ yield from ('-dn', '-ignore_unknown')
+ if copy:
+ yield from ('-c', 'copy')
+ # For some reason, '-c copy -map 0' is not enough to copy subtitles
+ if ext in ('mp4', 'mov'):
+ yield from ('-c:s', 'mov_text')
+
def get_audio_codec(self, path):
if not self.probe_available and not self.available:
raise PostProcessingError('ffprobe and ffmpeg not found. Please install or provide the path using --ffmpeg-location')
timestamps = timestamps[1:]
keyframe_file = prepend_extension(filename, 'keyframes.temp')
self.to_screen(f'Re-encoding "{filename}" with appropriate keyframes')
- self.run_ffmpeg(filename, keyframe_file, ['-force_key_frames', ','.join(
- f'{t:.6f}' for t in timestamps)])
+ self.run_ffmpeg(filename, keyframe_file, [
+ *self.stream_copy_opts(False, ext=determine_ext(filename)),
+ '-force_key_frames', ','.join(f'{t:.6f}' for t in timestamps)])
return keyframe_file
def concat_files(self, in_files, out_file, concat_opts=None):
with open(concat_file, 'wt', encoding='utf-8') as f:
f.writelines(self._concat_spec(in_files, concat_opts))
- out_flags = ['-c', 'copy']
- if out_file.rpartition('.')[-1] in ('mp4', 'mov'):
- # For some reason, '-c copy' is not enough to copy subtitles
- out_flags.extend(['-c:s', 'mov_text'])
+ out_flags = list(self.stream_copy_opts(ext=determine_ext(out_file)))
try:
self.real_run_ffmpeg(
else f'already is in target format {source_ext}' if source_ext == target_ext
else None)
if _skip_msg:
- self.to_screen(f'Not {self._ACTION} media file {filename!r}; {_skip_msg}')
+ self.to_screen(f'Not {self._ACTION} media file "{filename}"; {_skip_msg}')
return [], info
outpath = replace_extension(filename, target_ext, source_ext)
@staticmethod
def _options(target_ext):
- return ['-c', 'copy', '-map', '0', '-dn']
+ return FFmpegPostProcessor.stream_copy_opts()
class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
input_files = [filename] + sub_filenames
opts = [
- '-c', 'copy', '-map', '0', '-dn',
+ *self.stream_copy_opts(ext=info['ext']),
# Don't copy the existing subtitles, we may be running the
# postprocessor a second time
'-map', '-0:s',
- # Don't copy Apple TV chapters track, bin_data (see #19042, #19024,
- # https://trac.ffmpeg.org/ticket/6016)
- '-map', '-0:d',
]
- if info['ext'] == 'mp4':
- opts += ['-c:s', 'mov_text']
for i, (lang, name) in enumerate(zip(sub_langs, sub_names)):
opts.extend(['-map', '%d:0' % (i + 1)])
lang_code = ISO639Utils.short2long(lang) or lang
@staticmethod
def _options(target_ext):
- yield from ('-map', '0', '-dn')
- if target_ext == 'm4a':
+ audio_only = target_ext == 'm4a'
+ yield from FFmpegPostProcessor.stream_copy_opts(not audio_only)
+ if audio_only:
yield from ('-vn', '-acodec', 'copy')
- else:
- yield from ('-c', 'copy')
@PostProcessor._restrict_to(images=False)
def run(self, info):
stream_idx = 0
for fmt in info.get('requested_formats') or []:
stream_count = 2 if 'none' not in (fmt.get('vcodec'), fmt.get('acodec')) else 1
- lang = ISO639Utils.short2long(fmt['language']) or fmt.get('language')
+ lang = ISO639Utils.short2long(fmt.get('language') or '') or fmt.get('language')
for i in range(stream_idx, stream_idx + stream_count):
if lang:
metadata[str(i)].setdefault('language', lang)
stretched_ratio = info.get('stretched_ratio')
if stretched_ratio not in (None, 1):
self._fixup('Fixing aspect ratio', info['filepath'], [
- '-c', 'copy', '-map', '0', '-dn', '-aspect', '%f' % stretched_ratio])
+ *self.stream_copy_opts(), '-aspect', '%f' % stretched_ratio])
return [], info
@PostProcessor._restrict_to(images=False, video=False)
def run(self, info):
if info.get('container') == 'm4a_dash':
- self._fixup('Correcting container', info['filepath'], [
- '-c', 'copy', '-map', '0', '-dn', '-f', 'mp4'])
+ self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(), '-f', 'mp4'])
return [], info
def run(self, info):
if all(self._needs_fixup(info)):
self._fixup('Fixing MPEG-TS in MP4 container', info['filepath'], [
- '-c', 'copy', '-map', '0', '-dn', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'])
+ *self.stream_copy_opts(), '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'])
return [], info
opts = ['-vf', 'setpts=PTS-STARTPTS']
else:
opts = ['-c', 'copy', '-bsf', 'setts=ts=TS-STARTPTS']
- self._fixup('Fixing frame timestamp', info['filepath'], opts + ['-map', '0', '-dn', '-ss', self.trim])
+ self._fixup('Fixing frame timestamp', info['filepath'], opts + [*self.stream_copy_opts(False), '-ss', self.trim])
return [], info
-class FFmpegCopyStreamPostProcessor(FFmpegFixupPostProcessor):
+class FFmpegCopyStreamPP(FFmpegFixupPostProcessor):
MESSAGE = 'Copying stream'
@PostProcessor._restrict_to(images=False)
def run(self, info):
- self._fixup(self.MESSAGE, info['filepath'], ['-c', 'copy', '-map', '0', '-dn'])
+ self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts())
return [], info
-class FFmpegFixupDurationPP(FFmpegCopyStreamPostProcessor):
+class FFmpegFixupDurationPP(FFmpegCopyStreamPP):
MESSAGE = 'Fixing video duration'
-class FFmpegFixupDuplicateMoovPP(FFmpegCopyStreamPostProcessor):
+class FFmpegFixupDuplicateMoovPP(FFmpegCopyStreamPP):
MESSAGE = 'Fixing duplicate MOOV atoms'
self.to_screen('Splitting video by chapters; %d chapters found' % len(chapters))
for idx, chapter in enumerate(chapters):
destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
- self.real_run_ffmpeg([(in_file, opts)], [(destination, ['-c', 'copy'])])
+ self.real_run_ffmpeg([(in_file, opts)], [(destination, self.stream_copy_opts())])
if in_file != info['filepath']:
os.remove(in_file)
return [], info
if not has_thumbnail:
self.to_screen('There aren\'t any thumbnails to convert')
return files_to_delete, info
+
+
+class FFmpegConcatPP(FFmpegPostProcessor):
+ def __init__(self, downloader, only_multi_video=False):
+ self._only_multi_video = only_multi_video
+ super().__init__(downloader)
+
+ def concat_files(self, in_files, out_file):
+ if len(in_files) == 1:
+ if os.path.realpath(in_files[0]) != os.path.realpath(out_file):
+ self.to_screen(f'Moving "{in_files[0]}" to "{out_file}"')
+ os.replace(in_files[0], out_file)
+ return []
+
+ codecs = [traverse_obj(self.get_metadata_object(file), ('streams', ..., 'codec_name')) for file in in_files]
+ if len(set(map(tuple, codecs))) > 1:
+ raise PostProcessingError(
+ 'The files have different streams/codecs and cannot be concatenated. '
+ 'Either select different formats or --recode-video them to a common format')
+
+ self.to_screen(f'Concatenating {len(in_files)} files; Destination: {out_file}')
+ super().concat_files(in_files, out_file)
+ return in_files
+
+ @PostProcessor._restrict_to(images=False)
+ def run(self, info):
+ if not info.get('entries') or self._only_multi_video and info['_type'] != 'multi_video':
+ return [], info
+ elif None in info['entries']:
+ raise PostProcessingError('Aborting concatenation because some downloads failed')
+ elif any(len(entry) > 1 for entry in traverse_obj(info, ('entries', ..., 'requested_downloads')) or []):
+ raise PostProcessingError('Concatenation is not supported when downloading multiple separate formats')
+
+ in_files = traverse_obj(info, ('entries', ..., 'requested_downloads', 0, 'filepath'))
+ if not in_files:
+ self.to_screen('There are no files to concatenate')
+ return [], info
+
+ ie_copy = self._downloader._playlist_infodict(info)
+ exts = [traverse_obj(entry, ('requested_downloads', 0, 'ext'), 'ext') for entry in info['entries']]
+ ie_copy['ext'] = exts[0] if len(set(exts)) == 1 else 'mkv'
+ out_file = self._downloader.prepare_filename(ie_copy, 'pl_video')
+
+ files_to_delete = self.concat_files(in_files, out_file)
+
+ info['requested_downloads'] = [{
+ 'filepath': out_file,
+ 'ext': ie_copy['ext'],
+ }]
+ return files_to_delete, info