From 339828d777b76e60746054927ca4720d863884bc Mon Sep 17 00:00:00 2001 From: Riteo Date: Tue, 21 May 2024 02:07:23 +0200 Subject: [PATCH 01/23] [pp/FFmpegMetadata] Use metadata stream specifier for info.json The old stream index specifiers would indiscriminately select any JSON attachment, which made stuff like embedding live chat json data risky if not impossible. Also adds `-copy_unknown` as JSON data is "unknown" according to FFmpeg (since it has no codec id) and thus would otherwise be rejected by default. --- yt_dlp/postprocessor/ffmpeg.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 1ed37af518..e36fe80390 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -805,15 +805,19 @@ class FFmpegMetadataPP(FFmpegPostProcessor): write_json_file(self._downloader.sanitize_info(info, self.get_param('clean_infojson', True)), infofn) info['infojson_filename'] = infofn - old_stream, new_stream = self.get_stream_number(info['filepath'], ('tags', 'mimetype'), 'application/json') - if old_stream is not None: - yield ('-map', f'-0:{old_stream}') - new_stream -= 1 + escaped_name = self._ffmpeg_filename_argument(infofn) yield ( - '-attach', self._ffmpeg_filename_argument(infofn), - f'-metadata:s:{new_stream}', 'mimetype=application/json', - f'-metadata:s:{new_stream}', 'filename=info.json', + # In order to override any old info.json reliably we need to + # instruct FFmpeg to consider valid tracks without a codec id, like + # JSON attachments. + '-copy_unknown', + # This map operation allows us to actually replace any previous + # info.json data. + '-map', '-0:m:filename:info.json?', + '-attach', escaped_name, + f'-metadata:s:m:filename:{escaped_name}', 'mimetype=application/json', + f'-metadata:s:m:filename:{escaped_name}', 'filename=info.json', ) From ba3a7232f0d8d744ce8a44773bf94abacd16fad0 Mon Sep 17 00:00:00 2001 From: Riteo Date: Tue, 21 May 2024 14:58:39 +0200 Subject: [PATCH 02/23] [pp/FFmpegEmbedSubtitle] Embed JSON subtitles as Matroska attachments Since we can't embed them as regular subtitles (due to them not having any consistent structure), we embed them as file attachments, if exporting as Matroska. This allows us to have single-file downloads with everything embedded for e.g. archival purposes. --- yt_dlp/postprocessor/ffmpeg.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index e36fe80390..4b6dc0acc4 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -619,13 +619,19 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): webm_vtt_warn = False mp4_ass_warn = False + json_names, json_filenames = [], [] + for lang, sub_info in subtitles.items(): if not os.path.exists(sub_info.get('filepath', '')): self.report_warning(f'Skipping embedding {lang} subtitle because the file is missing') continue sub_ext = sub_info['ext'] if sub_ext == 'json': - self.report_warning('JSON subtitles cannot be embedded') + if info['ext'] in ('mkv', 'mka'): + json_names.append(lang) + json_filenames.append(sub_info['filepath']) + else: + self.report_warning('JSON subtitles can only be embedded in mkv/mka files.') elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt': sub_langs.append(lang) sub_names.append(sub_info.get('name')) @@ -644,11 +650,15 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): input_files = [filename, *sub_filenames] opts = [ + # Attached JSON subtitles don't have a codec id and we have to + # instruct FFMPEG to not discard them because of that. + '-copy_unknown', *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', ] + for i, (lang, name) in enumerate(zip(sub_langs, sub_names)): opts.extend(['-map', f'{i + 1}:0']) lang_code = ISO639Utils.short2long(lang) or lang @@ -657,12 +667,21 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): opts.extend([f'-metadata:s:s:{i}', f'handler_name={name}', f'-metadata:s:s:{i}', f'title={name}']) + for (json_filename, json_name) in zip(json_filenames, json_names): + escaped_json_filename = self._ffmpeg_filename_argument(json_filename) + opts.extend([ + '-map', f'-0:m:filename:{json_name}.json?', + '-attach', escaped_json_filename, + f'-metadata:s:m:filename:{escaped_json_filename}', 'mimetype=application/json', + f'-metadata:s:m:filename:{escaped_json_filename}', f'filename={json_name}.json', + ]) + temp_filename = prepend_extension(filename, 'temp') self.to_screen(f'Embedding subtitles in "{filename}"') self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) os.replace(temp_filename, filename) - files_to_delete = [] if self._already_have_subtitle else sub_filenames + files_to_delete = [] if self._already_have_subtitle else sub_filenames + json_filenames return files_to_delete, info From 550b3a046af9f1aa0bcf62096d1b841e55970f1d Mon Sep 17 00:00:00 2001 From: Riteo Date: Tue, 13 Aug 2024 22:29:11 +0200 Subject: [PATCH 03/23] Use the -copy_unknown flag in the stream copy otions Also split the yield expression as the comment above was a bit misleading (it was only related to the `-dn` flag). --- yt_dlp/postprocessor/ffmpeg.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 4b6dc0acc4..cb715dc7b7 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -221,7 +221,12 @@ class FFmpegPostProcessor(PostProcessor): 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') + yield '-dn' + + # Some streams, such as JSON attachments, are considered of unknown + # type by FFmpeg but we still want to copy them. + yield '-copy_unknown' + if copy: yield from ('-c', 'copy') if ext in ('mp4', 'mov', 'm4a'): From 38a9f70044c2c6cb2910bed9795ab83e554169e7 Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 00:32:06 +0200 Subject: [PATCH 04/23] Use a map for JSON sub handling instead of two lists --- yt_dlp/postprocessor/ffmpeg.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index cb715dc7b7..78d19d3b0c 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -624,7 +624,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): webm_vtt_warn = False mp4_ass_warn = False - json_names, json_filenames = [], [] + json_subs = {} for lang, sub_info in subtitles.items(): if not os.path.exists(sub_info.get('filepath', '')): @@ -633,8 +633,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): sub_ext = sub_info['ext'] if sub_ext == 'json': if info['ext'] in ('mkv', 'mka'): - json_names.append(lang) - json_filenames.append(sub_info['filepath']) + json_subs[lang] = sub_info['filepath'] else: self.report_warning('JSON subtitles can only be embedded in mkv/mka files.') elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt': @@ -672,13 +671,13 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): opts.extend([f'-metadata:s:s:{i}', f'handler_name={name}', f'-metadata:s:s:{i}', f'title={name}']) - for (json_filename, json_name) in zip(json_filenames, json_names): + for json_lang, json_filename in json_subs.items(): escaped_json_filename = self._ffmpeg_filename_argument(json_filename) opts.extend([ - '-map', f'-0:m:filename:{json_name}.json?', + '-map', f'-0:m:filename:{json_lang}.json?', '-attach', escaped_json_filename, f'-metadata:s:m:filename:{escaped_json_filename}', 'mimetype=application/json', - f'-metadata:s:m:filename:{escaped_json_filename}', f'filename={json_name}.json', + f'-metadata:s:m:filename:{escaped_json_filename}', f'filename={json_lang}.json', ]) temp_filename = prepend_extension(filename, 'temp') @@ -686,7 +685,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) os.replace(temp_filename, filename) - files_to_delete = [] if self._already_have_subtitle else sub_filenames + json_filenames + files_to_delete = [] if self._already_have_subtitle else sub_filenames + list(json_subs.values()) return files_to_delete, info From e202aae5d6e2700a054db467961b5068881009fe Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 01:58:12 +0200 Subject: [PATCH 05/23] Remove redundant copy_unknown --- yt_dlp/postprocessor/ffmpeg.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index b0d199636f..6fa1337291 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -655,9 +655,6 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): input_files = [filename, *sub_filenames] opts = [ - # Attached JSON subtitles don't have a codec id and we have to - # instruct FFMPEG to not discard them because of that. - '-copy_unknown', *self.stream_copy_opts(ext=info['ext']), # Don't copy the existing subtitles, we may be running the # postprocessor a second time From 62e274f515e293cd9926a2baa29a2f903df43cb8 Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 02:01:08 +0200 Subject: [PATCH 06/23] Move regular subtitles options to their loop --- yt_dlp/postprocessor/ffmpeg.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 6fa1337291..d986d12b8b 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -654,17 +654,19 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): input_files = [filename, *sub_filenames] - opts = [ - *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', - ] + opts = [*self.stream_copy_opts(ext=info['ext'])] for i, (lang, name) in enumerate(zip(sub_langs, sub_names)): - opts.extend(['-map', f'{i + 1}:0']) lang_code = ISO639Utils.short2long(lang) or lang - opts.extend([f'-metadata:s:s:{i}', f'language={lang_code}']) + opts.extend([ + # Don't copy the existing subtitles, we may be running the + # postprocessor a second time + '-map', '-0:s', + + '-map', f'{i + 1}:0', + f'-metadata:s:s:{i}', f'language={lang_code}' + ]) + if name: opts.extend([f'-metadata:s:s:{i}', f'handler_name={name}', f'-metadata:s:s:{i}', f'title={name}']) From 9db000a9af3e080d7ac4ff0d2ec790580db95b82 Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 02:28:08 +0200 Subject: [PATCH 07/23] Check also if there are json subtitles --- yt_dlp/postprocessor/ffmpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index d986d12b8b..616af58d81 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -649,7 +649,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): mp4_ass_warn = True self.report_warning('ASS subtitles cannot be properly embedded in mp4 files; expect issues') - if not sub_langs: + if not sub_langs and not json_subs: return [], info input_files = [filename, *sub_filenames] From fe5de0005ec7b95ca0c0a92527d5ca407dc48a0b Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 02:46:15 +0200 Subject: [PATCH 08/23] Add extra checks for non-matroska formats when copying --- yt_dlp/postprocessor/ffmpeg.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 616af58d81..35f65cb5b4 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -219,15 +219,25 @@ class FFmpegPostProcessor(PostProcessor): @staticmethod def stream_copy_opts(copy=True, *, ext=None): - yield from ('-map', '0') + if ext in ('mkv', 'mka'): + yield from ('-map', '0') + + # Some streams, such as JSON attachments, are considered of unknown + # type by FFmpeg but we still want to copy them. + yield '-copy_unknown' + else: + # Other containers can get angry at malformed or unknown streams, + # so we'll keep only the ones that are "usable". + yield from ('-map', '0:u') + + # Most containers don't really like unknown streams. Let's make + # sure to get rid of them. + yield '-ignore_unknown' + # 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 '-dn' - # Some streams, such as JSON attachments, are considered of unknown - # type by FFmpeg but we still want to copy them. - yield '-copy_unknown' - if copy: yield from ('-c', 'copy') if ext in ('mp4', 'mov', 'm4a'): @@ -588,7 +598,7 @@ class FFmpegVideoRemuxerPP(FFmpegVideoConvertorPP): @staticmethod def _options(target_ext): - return FFmpegPostProcessor.stream_copy_opts() + return FFmpegPostProcessor.stream_copy_opts(ext=target_ext) class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): From 780bfd044f0d236da4db3e24f0a09b58aca48dbe Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 03:05:11 +0200 Subject: [PATCH 09/23] Pass target extension to all stream_copy_opts instances --- yt_dlp/postprocessor/ffmpeg.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 35f65cb5b4..cefb0430bb 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -572,7 +572,7 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor): @staticmethod def _options(target_ext): - yield from FFmpegPostProcessor.stream_copy_opts(False) + yield from FFmpegPostProcessor.stream_copy_opts(False, ext=target_ext) if target_ext == 'avi': yield from ('-c:v', 'libxvid', '-vtag', 'XVID') @@ -710,7 +710,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor): @staticmethod def _options(target_ext): audio_only = target_ext == 'm4a' - yield from FFmpegPostProcessor.stream_copy_opts(not audio_only) + yield from FFmpegPostProcessor.stream_copy_opts(not audio_only, ext=target_ext) if audio_only: yield from ('-vn', '-acodec', 'copy') @@ -909,7 +909,7 @@ class FFmpegFixupStretchedPP(FFmpegFixupPostProcessor): stretched_ratio = info.get('stretched_ratio') if stretched_ratio not in (None, 1): self._fixup('Fixing aspect ratio', info['filepath'], [ - *self.stream_copy_opts(), '-aspect', f'{stretched_ratio:f}']) + *self.stream_copy_opts(ext=info['ext']), '-aspect', f'{stretched_ratio:f}']) return [], info @@ -917,7 +917,7 @@ class FFmpegFixupM4aPP(FFmpegFixupPostProcessor): @PostProcessor._restrict_to(images=False, video=False) def run(self, info): if info.get('container') == 'm4a_dash': - self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(), '-f', 'mp4']) + self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(ext=info['ext']), '-f', 'mp4']) return [], info @@ -940,7 +940,7 @@ class FFmpegFixupM3u8PP(FFmpegFixupPostProcessor): if self.get_audio_codec(info['filepath']) == 'aac': args.extend(['-bsf:a', 'aac_adtstoasc']) self._fixup('Fixing MPEG-TS in MP4 container', info['filepath'], [ - *self.stream_copy_opts(), *args]) + *self.stream_copy_opts(ext=info['ext']), *args]) return [], info @@ -961,7 +961,7 @@ class FFmpegFixupTimestampPP(FFmpegFixupPostProcessor): opts = ['-vf', 'setpts=PTS-STARTPTS'] else: opts = ['-c', 'copy', '-bsf', 'setts=ts=TS-STARTPTS'] - self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False), '-ss', self.trim]) + self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False, ext=info['ext']), '-ss', self.trim]) return [], info @@ -970,7 +970,7 @@ class FFmpegCopyStreamPP(FFmpegFixupPostProcessor): @PostProcessor._restrict_to(images=False) def run(self, info): - self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts()) + self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts(ext=info['ext'])) return [], info @@ -1099,7 +1099,7 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor): self.to_screen(f'Splitting video by chapters; {len(chapters)} chapters found') 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, self.stream_copy_opts())]) + self.real_run_ffmpeg([(in_file, opts)], [(destination, self.stream_copy_opts(ext=info['ext']))]) if in_file != info['filepath']: self._delete_downloaded_files(in_file, msg=None) return [], info From aaa25eb5088bdc8c94be4c5a47e62a0ec0c0182f Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 14 Aug 2024 03:18:55 +0200 Subject: [PATCH 10/23] Add missing trailing comma --- yt_dlp/postprocessor/ffmpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index cefb0430bb..730765bf37 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -674,7 +674,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): '-map', '-0:s', '-map', f'{i + 1}:0', - f'-metadata:s:s:{i}', f'language={lang_code}' + f'-metadata:s:s:{i}', f'language={lang_code}', ]) if name: From 7fb0c05ff6b0359500c2f230d0af4cdf239e981a Mon Sep 17 00:00:00 2001 From: Riteo Date: Sun, 8 Sep 2024 13:04:59 +0200 Subject: [PATCH 11/23] Revert format check stuff --- yt_dlp/postprocessor/ffmpeg.py | 40 +++++++++++++--------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 730765bf37..616af58d81 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -219,25 +219,15 @@ class FFmpegPostProcessor(PostProcessor): @staticmethod def stream_copy_opts(copy=True, *, ext=None): - if ext in ('mkv', 'mka'): - yield from ('-map', '0') - - # Some streams, such as JSON attachments, are considered of unknown - # type by FFmpeg but we still want to copy them. - yield '-copy_unknown' - else: - # Other containers can get angry at malformed or unknown streams, - # so we'll keep only the ones that are "usable". - yield from ('-map', '0:u') - - # Most containers don't really like unknown streams. Let's make - # sure to get rid of them. - yield '-ignore_unknown' - + 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 '-dn' + # Some streams, such as JSON attachments, are considered of unknown + # type by FFmpeg but we still want to copy them. + yield '-copy_unknown' + if copy: yield from ('-c', 'copy') if ext in ('mp4', 'mov', 'm4a'): @@ -572,7 +562,7 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor): @staticmethod def _options(target_ext): - yield from FFmpegPostProcessor.stream_copy_opts(False, ext=target_ext) + yield from FFmpegPostProcessor.stream_copy_opts(False) if target_ext == 'avi': yield from ('-c:v', 'libxvid', '-vtag', 'XVID') @@ -598,7 +588,7 @@ class FFmpegVideoRemuxerPP(FFmpegVideoConvertorPP): @staticmethod def _options(target_ext): - return FFmpegPostProcessor.stream_copy_opts(ext=target_ext) + return FFmpegPostProcessor.stream_copy_opts() class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): @@ -674,7 +664,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): '-map', '-0:s', '-map', f'{i + 1}:0', - f'-metadata:s:s:{i}', f'language={lang_code}', + f'-metadata:s:s:{i}', f'language={lang_code}' ]) if name: @@ -710,7 +700,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor): @staticmethod def _options(target_ext): audio_only = target_ext == 'm4a' - yield from FFmpegPostProcessor.stream_copy_opts(not audio_only, ext=target_ext) + yield from FFmpegPostProcessor.stream_copy_opts(not audio_only) if audio_only: yield from ('-vn', '-acodec', 'copy') @@ -909,7 +899,7 @@ class FFmpegFixupStretchedPP(FFmpegFixupPostProcessor): stretched_ratio = info.get('stretched_ratio') if stretched_ratio not in (None, 1): self._fixup('Fixing aspect ratio', info['filepath'], [ - *self.stream_copy_opts(ext=info['ext']), '-aspect', f'{stretched_ratio:f}']) + *self.stream_copy_opts(), '-aspect', f'{stretched_ratio:f}']) return [], info @@ -917,7 +907,7 @@ class FFmpegFixupM4aPP(FFmpegFixupPostProcessor): @PostProcessor._restrict_to(images=False, video=False) def run(self, info): if info.get('container') == 'm4a_dash': - self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(ext=info['ext']), '-f', 'mp4']) + self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(), '-f', 'mp4']) return [], info @@ -940,7 +930,7 @@ class FFmpegFixupM3u8PP(FFmpegFixupPostProcessor): if self.get_audio_codec(info['filepath']) == 'aac': args.extend(['-bsf:a', 'aac_adtstoasc']) self._fixup('Fixing MPEG-TS in MP4 container', info['filepath'], [ - *self.stream_copy_opts(ext=info['ext']), *args]) + *self.stream_copy_opts(), *args]) return [], info @@ -961,7 +951,7 @@ class FFmpegFixupTimestampPP(FFmpegFixupPostProcessor): opts = ['-vf', 'setpts=PTS-STARTPTS'] else: opts = ['-c', 'copy', '-bsf', 'setts=ts=TS-STARTPTS'] - self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False, ext=info['ext']), '-ss', self.trim]) + self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False), '-ss', self.trim]) return [], info @@ -970,7 +960,7 @@ class FFmpegCopyStreamPP(FFmpegFixupPostProcessor): @PostProcessor._restrict_to(images=False) def run(self, info): - self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts(ext=info['ext'])) + self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts()) return [], info @@ -1099,7 +1089,7 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor): self.to_screen(f'Splitting video by chapters; {len(chapters)} chapters found') 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, self.stream_copy_opts(ext=info['ext']))]) + self.real_run_ffmpeg([(in_file, opts)], [(destination, self.stream_copy_opts())]) if in_file != info['filepath']: self._delete_downloaded_files(in_file, msg=None) return [], info From 45d1f2bb6ca16185664e0878f75fbbd41eba8966 Mon Sep 17 00:00:00 2001 From: Riteo Date: Sun, 8 Sep 2024 13:22:18 +0200 Subject: [PATCH 12/23] Fix attachments in subpaths --- yt_dlp/extractor/_extractors.py | 2 ++ yt_dlp/postprocessor/ffmpeg.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index 9b73fcd75e..bcb0e1118f 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1,6 +1,8 @@ # flake8: noqa: F401 # isort: off +from .demo import DemoIE + from .youtube import ( # Youtube is moved to the top to improve performance YoutubeIE, YoutubeClipIE, diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 616af58d81..4689a96db4 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -673,11 +673,12 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): for json_lang, json_filename in json_subs.items(): escaped_json_filename = self._ffmpeg_filename_argument(json_filename) + json_basename = os.path.basename(json_filename) opts.extend([ '-map', f'-0:m:filename:{json_lang}.json?', '-attach', escaped_json_filename, - f'-metadata:s:m:filename:{escaped_json_filename}', 'mimetype=application/json', - f'-metadata:s:m:filename:{escaped_json_filename}', f'filename={json_lang}.json', + f'-metadata:s:m:filename:{json_basename}', 'mimetype=application/json', + f'-metadata:s:m:filename:{json_basename}', f'filename={json_lang}.json', ]) temp_filename = prepend_extension(filename, 'temp') From 4b5be635b1e21a44cd1fca8138b9c3d734ba28d1 Mon Sep 17 00:00:00 2001 From: Riteo Date: Sun, 8 Sep 2024 13:23:26 +0200 Subject: [PATCH 13/23] Add missing comma (again) oops --- yt_dlp/postprocessor/ffmpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 4689a96db4..a79b31f895 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -664,7 +664,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): '-map', '-0:s', '-map', f'{i + 1}:0', - f'-metadata:s:s:{i}', f'language={lang_code}' + f'-metadata:s:s:{i}', f'language={lang_code}', ]) if name: From fc349670c331c0f8aa061928fa759d86dd585a14 Mon Sep 17 00:00:00 2001 From: Riteo Date: Sun, 8 Sep 2024 13:30:00 +0200 Subject: [PATCH 14/23] Fix info attachment in subpaths --- yt_dlp/postprocessor/ffmpeg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index a79b31f895..08b798cf97 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -830,6 +830,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor): info['infojson_filename'] = infofn escaped_name = self._ffmpeg_filename_argument(infofn) + info_basename = os.path.basename(infofn) yield ( # In order to override any old info.json reliably we need to @@ -840,8 +841,8 @@ class FFmpegMetadataPP(FFmpegPostProcessor): # info.json data. '-map', '-0:m:filename:info.json?', '-attach', escaped_name, - f'-metadata:s:m:filename:{escaped_name}', 'mimetype=application/json', - f'-metadata:s:m:filename:{escaped_name}', 'filename=info.json', + f'-metadata:s:m:filename:{info_basename}', 'mimetype=application/json', + f'-metadata:s:m:filename:{info_basename}', 'filename=info.json', ) From 17781f9d7dc9e7b8759883a7e82912bd4e3a24eb Mon Sep 17 00:00:00 2001 From: Riteo Date: Sun, 8 Sep 2024 13:33:24 +0200 Subject: [PATCH 15/23] Remove debug thing I'm dumb --- yt_dlp/extractor/_extractors.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index bcb0e1118f..9b73fcd75e 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1,8 +1,6 @@ # flake8: noqa: F401 # isort: off -from .demo import DemoIE - from .youtube import ( # Youtube is moved to the top to improve performance YoutubeIE, YoutubeClipIE, From 85a844aef3ec3580c59d8f8fda32edadb0bdd014 Mon Sep 17 00:00:00 2001 From: Riteo Date: Wed, 11 Sep 2024 11:43:33 +0200 Subject: [PATCH 16/23] Select copy mode depending on extension --- yt_dlp/postprocessor/ffmpeg.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 08b798cf97..cfcca6ef1c 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -220,14 +220,20 @@ class FFmpegPostProcessor(PostProcessor): @staticmethod def stream_copy_opts(copy=True, *, ext=None): yield from ('-map', '0') + + if ext in ('mkv', 'mka'): + # Some streams, such as JSON attachments, are considered of unknown + # type by FFmpeg but we still want to copy them. + yield '-copy_unknown' + else: + # Most containers don't really like unknown streams. Let's make + # sure to get rid of them. + yield '-ignore_unknown' + # 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 '-dn' - # Some streams, such as JSON attachments, are considered of unknown - # type by FFmpeg but we still want to copy them. - yield '-copy_unknown' - if copy: yield from ('-c', 'copy') if ext in ('mp4', 'mov', 'm4a'): @@ -562,7 +568,7 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor): @staticmethod def _options(target_ext): - yield from FFmpegPostProcessor.stream_copy_opts(False) + yield from FFmpegPostProcessor.stream_copy_opts(False, ext=target_ext) if target_ext == 'avi': yield from ('-c:v', 'libxvid', '-vtag', 'XVID') @@ -588,7 +594,7 @@ class FFmpegVideoRemuxerPP(FFmpegVideoConvertorPP): @staticmethod def _options(target_ext): - return FFmpegPostProcessor.stream_copy_opts() + return FFmpegPostProcessor.stream_copy_opts(ext=target_ext) class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): @@ -701,7 +707,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor): @staticmethod def _options(target_ext): audio_only = target_ext == 'm4a' - yield from FFmpegPostProcessor.stream_copy_opts(not audio_only) + yield from FFmpegPostProcessor.stream_copy_opts(not audio_only, ext=target_ext) if audio_only: yield from ('-vn', '-acodec', 'copy') @@ -901,7 +907,7 @@ class FFmpegFixupStretchedPP(FFmpegFixupPostProcessor): stretched_ratio = info.get('stretched_ratio') if stretched_ratio not in (None, 1): self._fixup('Fixing aspect ratio', info['filepath'], [ - *self.stream_copy_opts(), '-aspect', f'{stretched_ratio:f}']) + *self.stream_copy_opts(ext=info['ext']), '-aspect', f'{stretched_ratio:f}']) return [], info @@ -909,7 +915,7 @@ class FFmpegFixupM4aPP(FFmpegFixupPostProcessor): @PostProcessor._restrict_to(images=False, video=False) def run(self, info): if info.get('container') == 'm4a_dash': - self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(), '-f', 'mp4']) + self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(ext=info['ext']), '-f', 'mp4']) return [], info @@ -932,7 +938,7 @@ class FFmpegFixupM3u8PP(FFmpegFixupPostProcessor): if self.get_audio_codec(info['filepath']) == 'aac': args.extend(['-bsf:a', 'aac_adtstoasc']) self._fixup('Fixing MPEG-TS in MP4 container', info['filepath'], [ - *self.stream_copy_opts(), *args]) + *self.stream_copy_opts(ext=info['ext']), *args]) return [], info @@ -953,7 +959,7 @@ class FFmpegFixupTimestampPP(FFmpegFixupPostProcessor): opts = ['-vf', 'setpts=PTS-STARTPTS'] else: opts = ['-c', 'copy', '-bsf', 'setts=ts=TS-STARTPTS'] - self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False), '-ss', self.trim]) + self._fixup('Fixing frame timestamp', info['filepath'], [*opts, *self.stream_copy_opts(False, ext=info['ext']), '-ss', self.trim]) return [], info @@ -962,7 +968,7 @@ class FFmpegCopyStreamPP(FFmpegFixupPostProcessor): @PostProcessor._restrict_to(images=False) def run(self, info): - self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts()) + self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts(ext=info['ext'])) return [], info @@ -1091,7 +1097,7 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor): self.to_screen(f'Splitting video by chapters; {len(chapters)} chapters found') 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, self.stream_copy_opts())]) + self.real_run_ffmpeg([(in_file, opts)], [(destination, self.stream_copy_opts(ext=info['ext']))]) if in_file != info['filepath']: self._delete_downloaded_files(in_file, msg=None) return [], info From 4aa3c401d472816034ec41983e1109e24e78f372 Mon Sep 17 00:00:00 2001 From: Riteo Date: Fri, 8 Nov 2024 03:48:54 +0100 Subject: [PATCH 17/23] Do not pass `-map -0:s` multiple times --- yt_dlp/postprocessor/ffmpeg.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index cfcca6ef1c..af98914b7b 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -662,13 +662,17 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): opts = [*self.stream_copy_opts(ext=info['ext'])] + if sub_langs and sub_names: + # We have regular subtitles available to embed. Don't copy the + # existing subtitles, we may be running the postprocessor a second + # time. + opts.extend([ + '-map', '-0:s', + ]) + for i, (lang, name) in enumerate(zip(sub_langs, sub_names)): lang_code = ISO639Utils.short2long(lang) or lang opts.extend([ - # Don't copy the existing subtitles, we may be running the - # postprocessor a second time - '-map', '-0:s', - '-map', f'{i + 1}:0', f'-metadata:s:s:{i}', f'language={lang_code}', ]) From 1cae3bf46d28725e90e0b432ae7475766b3d7b9f Mon Sep 17 00:00:00 2001 From: Riteo Date: Fri, 8 Nov 2024 03:52:50 +0100 Subject: [PATCH 18/23] Use unpack operator for files to delete --- yt_dlp/postprocessor/ffmpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index af98914b7b..35892e2d34 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -696,7 +696,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) os.replace(temp_filename, filename) - files_to_delete = [] if self._already_have_subtitle else sub_filenames + list(json_subs.values()) + files_to_delete = [] if self._already_have_subtitle else [*sub_filenames, *json_subs.values()] return files_to_delete, info From f17c33409dd58ed783cbfd2fd19bb41e830ebb4b Mon Sep 17 00:00:00 2001 From: Dery Almas Date: Mon, 27 Oct 2025 05:27:52 +0100 Subject: [PATCH 19/23] Fix colons in stream selectors --- yt_dlp/postprocessor/ffmpeg.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 8400595581..fc99dd69f6 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -671,13 +671,17 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): f'-metadata:s:s:{i}', f'title={name}']) for json_lang, json_filename in json_subs.items(): - escaped_json_filename = self._ffmpeg_filename_argument(json_filename) - json_basename = os.path.basename(json_filename) + filename_arg = self._ffmpeg_filename_argument(json_filename) + # Stream selectors use colons as their separator. Any colon in the + # filename (protocol included) needs to be escaped. + filename_arg_escaped = filename_arg.replace(':', r'\:') + + opts.extend([ '-map', f'-0:m:filename:{json_lang}.json?', - '-attach', escaped_json_filename, - f'-metadata:s:m:filename:{json_basename}', 'mimetype=application/json', - f'-metadata:s:m:filename:{json_basename}', f'filename={json_lang}.json', + '-attach', filename_arg, + f'-metadata:s:m:filename:{filename_arg_escaped}', 'mimetype=application/json', + f'-metadata:s:m:filename:{filename_arg_escaped}', f'filename={json_lang}.json', ]) temp_filename = prepend_extension(filename, 'temp') @@ -828,8 +832,10 @@ class FFmpegMetadataPP(FFmpegPostProcessor): write_json_file(self._downloader.sanitize_info(info, self.get_param('clean_infojson', True)), infofn) info['infojson_filename'] = infofn - escaped_name = self._ffmpeg_filename_argument(infofn) - info_basename = os.path.basename(infofn) + filename_arg = self._ffmpeg_filename_argument(infofn) + # Stream selectors use colons as their separator. Any colon in the + # filename (protocol included) needs to be escaped. + filename_arg_escaped = filename_arg.replace(':', r'\:') yield ( # In order to override any old info.json reliably we need to @@ -839,9 +845,9 @@ class FFmpegMetadataPP(FFmpegPostProcessor): # This map operation allows us to actually replace any previous # info.json data. '-map', '-0:m:filename:info.json?', - '-attach', escaped_name, - f'-metadata:s:m:filename:{info_basename}', 'mimetype=application/json', - f'-metadata:s:m:filename:{info_basename}', 'filename=info.json', + '-attach', filename_arg, + f'-metadata:s:m:filename:{filename_arg_escaped}', 'mimetype=application/json', + f'-metadata:s:m:filename:{filename_arg_escaped}', 'filename=info.json', ) From 9d1f3d1d2c6106d3eb93cb8cc0314f08828039b2 Mon Sep 17 00:00:00 2001 From: Dery Almas Date: Mon, 27 Oct 2025 18:56:10 +0100 Subject: [PATCH 20/23] Fix formatting --- yt_dlp/postprocessor/ffmpeg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index fc99dd69f6..427cbd9d90 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -676,7 +676,6 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): # filename (protocol included) needs to be escaped. filename_arg_escaped = filename_arg.replace(':', r'\:') - opts.extend([ '-map', f'-0:m:filename:{json_lang}.json?', '-attach', filename_arg, From a134e0c6bae39ef28633abd77f275609422ca6ba Mon Sep 17 00:00:00 2001 From: Dery Almas Date: Tue, 28 Oct 2025 01:29:20 +0100 Subject: [PATCH 21/23] Escape stream specifiers depending on FFmpeg version --- yt_dlp/postprocessor/ffmpeg.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index 427cbd9d90..d71c691641 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -394,6 +394,16 @@ class FFmpegPostProcessor(PostProcessor): string = string[1:] if string[0] == "'" else "'" + string return string[:-1] if string[-1] == "'" else string + "'" + def _escape_stream_specifier(self, string): + if (is_outdated_version(self._get_version('ffmpeg'), '7.1')): + # Versions older than 7.1 take arguments with colons verbatim. + return string + + # Since 7.1, stream specifier arguments with colons must be escaped or + # they will be interpreted as part of the specifier. This was done to + # reduce ambiguity, but it also broke compatibility. + return string.replace(':', r'\:') + def force_keyframes(self, filename, timestamps): timestamps = orderedSet(timestamps) if timestamps[0] == 0: @@ -672,9 +682,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): for json_lang, json_filename in json_subs.items(): filename_arg = self._ffmpeg_filename_argument(json_filename) - # Stream selectors use colons as their separator. Any colon in the - # filename (protocol included) needs to be escaped. - filename_arg_escaped = filename_arg.replace(':', r'\:') + filename_arg_escaped = self._escape_stream_specifier(filename_arg) opts.extend([ '-map', f'-0:m:filename:{json_lang}.json?', @@ -832,9 +840,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor): info['infojson_filename'] = infofn filename_arg = self._ffmpeg_filename_argument(infofn) - # Stream selectors use colons as their separator. Any colon in the - # filename (protocol included) needs to be escaped. - filename_arg_escaped = filename_arg.replace(':', r'\:') + filename_arg_escaped = self._escape_stream_specifier(filename_arg) yield ( # In order to override any old info.json reliably we need to From d97b8045cc0170736013fe7aeb6c32b2c22ca5a5 Mon Sep 17 00:00:00 2001 From: Dery Almas Date: Fri, 7 Nov 2025 17:25:43 +0100 Subject: [PATCH 22/23] Fix filename inside folders --- yt_dlp/postprocessor/ffmpeg.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index d71c691641..b1eecbb019 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -682,13 +682,16 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): for json_lang, json_filename in json_subs.items(): filename_arg = self._ffmpeg_filename_argument(json_filename) - filename_arg_escaped = self._escape_stream_specifier(filename_arg) + filename_ss = self._escape_stream_specifier(filename_arg) + # FFmpeg sets the filename tag as the basename of the argument, + # protocol and all, for some reason. + filename_ss = os.path.basename(filename_ss) opts.extend([ '-map', f'-0:m:filename:{json_lang}.json?', '-attach', filename_arg, - f'-metadata:s:m:filename:{filename_arg_escaped}', 'mimetype=application/json', - f'-metadata:s:m:filename:{filename_arg_escaped}', f'filename={json_lang}.json', + f'-metadata:s:m:filename:{filename_ss}', 'mimetype=application/json', + f'-metadata:s:m:filename:{filename_ss}', f'filename={json_lang}.json', ]) temp_filename = prepend_extension(filename, 'temp') @@ -840,7 +843,10 @@ class FFmpegMetadataPP(FFmpegPostProcessor): info['infojson_filename'] = infofn filename_arg = self._ffmpeg_filename_argument(infofn) - filename_arg_escaped = self._escape_stream_specifier(filename_arg) + filename_ss = self._escape_stream_specifier(filename_arg) + # FFmpeg sets the filename tag as the basename of the argument, protocol + # and all, for some reason. + filename_ss = os.path.basename(filename_ss) yield ( # In order to override any old info.json reliably we need to @@ -851,8 +857,8 @@ class FFmpegMetadataPP(FFmpegPostProcessor): # info.json data. '-map', '-0:m:filename:info.json?', '-attach', filename_arg, - f'-metadata:s:m:filename:{filename_arg_escaped}', 'mimetype=application/json', - f'-metadata:s:m:filename:{filename_arg_escaped}', 'filename=info.json', + f'-metadata:s:m:filename:{filename_ss}', 'mimetype=application/json', + f'-metadata:s:m:filename:{filename_ss}', 'filename=info.json', ) From 6cd71cedc521d1de722aa52d5a45ae5fd2a8d44b Mon Sep 17 00:00:00 2001 From: Dery Almas Date: Sat, 8 Nov 2025 08:29:26 +0100 Subject: [PATCH 23/23] Escape FFmpeg special characters Apparently stream specifiers care about them. --- yt_dlp/postprocessor/ffmpeg.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index b1eecbb019..d11cea9296 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -395,14 +395,19 @@ class FFmpegPostProcessor(PostProcessor): return string[:-1] if string[-1] == "'" else string + "'" def _escape_stream_specifier(self, string): + # Escape `'` and `/` as per the docs: + # https://ffmpeg.org/ffmpeg-utils.html#toc-Quoting-and-escaping + out = string.replace('\\', '\\\\') + out = out.replace("'", "\\'") + if (is_outdated_version(self._get_version('ffmpeg'), '7.1')): # Versions older than 7.1 take arguments with colons verbatim. - return string + return out # Since 7.1, stream specifier arguments with colons must be escaped or # they will be interpreted as part of the specifier. This was done to # reduce ambiguity, but it also broke compatibility. - return string.replace(':', r'\:') + return out.replace(':', r'\:') def force_keyframes(self, filename, timestamps): timestamps = orderedSet(timestamps)