[ffmpeg] Download and merge in a single step if possible

This commit is contained in:
pukkandan 2021-05-23 03:47:44 +05:30
parent 8d68ab98a7
commit 18e674b4f6
No known key found for this signature in database
GPG Key ID: 0F00D95A001F4698
4 changed files with 70 additions and 40 deletions

View File

@ -130,6 +130,7 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
* Youtube live chat (if available) is considered as a subtitle. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent live chat from downloading * Youtube live chat (if available) is considered as a subtitle. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent live chat from downloading
* Youtube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections * Youtube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
* Unavailable videos are also listed for youtube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this * Unavailable videos are also listed for youtube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
* If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this
For ease of use, a few more compat options are available: For ease of use, a few more compat options are available:
* `--compat-options all`: Use all compat options * `--compat-options all`: Use all compat options

View File

@ -387,8 +387,9 @@ class YoutubeDL(object):
if True, otherwise use ffmpeg/avconv if False, otherwise if True, otherwise use ffmpeg/avconv if False, otherwise
use downloader suggested by extractor if None. use downloader suggested by extractor if None.
compat_opts: Compatibility options. See "Differences in default behavior". compat_opts: Compatibility options. See "Differences in default behavior".
Note that only format-sort, format-spec, no-live-chat, no-attach-info-json Note that only format-sort, format-spec, no-live-chat,
playlist-index, list-formats, no-youtube-channel-redirect no-attach-info-json, playlist-index, list-formats,
no-direct-merge, no-youtube-channel-redirect,
and no-youtube-unavailable-videos works when used via the API and no-youtube-unavailable-videos works when used via the API
The following parameters are not used by YoutubeDL itself, they are used by The following parameters are not used by YoutubeDL itself, they are used by
@ -2294,7 +2295,8 @@ class YoutubeDL(object):
if not test: if not test:
for ph in self._progress_hooks: for ph in self._progress_hooks:
fd.add_progress_hook(ph) fd.add_progress_hook(ph)
self.write_debug('Invoking downloader on %r' % info.get('url')) urls = '", "'.join([f['url'] for f in info.get('requested_formats', [])] or [info['url']])
self.write_debug('Invoking downloader on "%s"' % urls)
new_info = dict(info) new_info = dict(info)
if new_info.get('http_headers') is None: if new_info.get('http_headers') is None:
new_info['http_headers'] = self._calc_headers(new_info) new_info['http_headers'] = self._calc_headers(new_info)
@ -2533,17 +2535,6 @@ class YoutubeDL(object):
success = True success = True
if info_dict.get('requested_formats') is not None: if info_dict.get('requested_formats') is not None:
downloaded = []
merger = FFmpegMergerPP(self)
if self.params.get('allow_unplayable_formats'):
self.report_warning(
'You have requested merging of multiple formats '
'while also allowing unplayable formats to be downloaded. '
'The formats won\'t be merged to prevent data corruption.')
elif not merger.available:
self.report_warning(
'You have requested merging of multiple formats but ffmpeg is not installed. '
'The formats won\'t be merged.')
def compatible_formats(formats): def compatible_formats(formats):
# TODO: some formats actually allow this (mkv, webm, ogg, mp4), but not all of them. # TODO: some formats actually allow this (mkv, webm, ogg, mp4), but not all of them.
@ -2591,27 +2582,57 @@ class YoutubeDL(object):
temp_filename = correct_ext(temp_filename) temp_filename = correct_ext(temp_filename)
dl_filename = existing_file(full_filename, temp_filename) dl_filename = existing_file(full_filename, temp_filename)
info_dict['__real_download'] = False info_dict['__real_download'] = False
if dl_filename is None:
for f in requested_formats: _protocols = set(determine_protocol(f) for f in requested_formats)
new_info = dict(info_dict) if len(_protocols) == 1:
new_info.update(f) info_dict['protocol'] = _protocols.pop()
fname = prepend_extension( directly_mergable = (
self.prepare_filename(new_info, 'temp'), 'no-direct-merge' not in self.params.get('compat_opts', [])
'f%s' % f['format_id'], new_info['ext']) and info_dict.get('protocol') is not None # All requested formats have same protocol
if not self._ensure_dir_exists(fname): and not self.params.get('allow_unplayable_formats')
return and get_suitable_downloader(info_dict, self.params).__name__ == 'FFmpegFD')
downloaded.append(fname) if directly_mergable:
partial_success, real_download = self.dl(fname, new_info) info_dict['url'] = requested_formats[0]['url']
info_dict['__real_download'] = info_dict['__real_download'] or real_download # Treat it as a single download
success = success and partial_success dl_filename = existing_file(full_filename, temp_filename)
if merger.available and not self.params.get('allow_unplayable_formats'): if dl_filename is None:
info_dict['__postprocessors'].append(merger) success, real_download = self.dl(temp_filename, info_dict)
info_dict['__files_to_merge'] = downloaded info_dict['__real_download'] = real_download
# Even if there were no downloads, it is being merged only now else:
info_dict['__real_download'] = True downloaded = []
else: merger = FFmpegMergerPP(self)
for file in downloaded: if self.params.get('allow_unplayable_formats'):
files_to_move[file] = None self.report_warning(
'You have requested merging of multiple formats '
'while also allowing unplayable formats to be downloaded. '
'The formats won\'t be merged to prevent data corruption.')
elif not merger.available:
self.report_warning(
'You have requested merging of multiple formats but ffmpeg is not installed. '
'The formats won\'t be merged.')
if dl_filename is None:
for f in requested_formats:
new_info = dict(info_dict)
del new_info['requested_formats']
new_info.update(f)
fname = prepend_extension(
self.prepare_filename(new_info, 'temp'),
'f%s' % f['format_id'], new_info['ext'])
if not self._ensure_dir_exists(fname):
return
downloaded.append(fname)
partial_success, real_download = self.dl(fname, new_info)
info_dict['__real_download'] = info_dict['__real_download'] or real_download
success = success and partial_success
if merger.available and not self.params.get('allow_unplayable_formats'):
info_dict['__postprocessors'].append(merger)
info_dict['__files_to_merge'] = downloaded
# Even if there were no downloads, it is being merged only now
info_dict['__real_download'] = True
else:
for file in downloaded:
files_to_move[file] = None
else: else:
# Just a single file # Just a single file
dl_filename = existing_file(full_filename, temp_filename) dl_filename = existing_file(full_filename, temp_filename)

View File

@ -264,8 +264,8 @@ def _real_main(argv=None):
return parsed_compat_opts return parsed_compat_opts
all_compat_opts = [ all_compat_opts = [
'filename', 'format-sort', 'abort-on-error', 'format-spec', 'multistreams', 'filename', 'format-sort', 'abort-on-error', 'format-spec', 'no-playlist-metafiles',
'no-playlist-metafiles', 'no-live-chat', 'playlist-index', 'list-formats', 'multistreams', 'no-live-chat', 'playlist-index', 'list-formats', 'no-direct-merge',
'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-attach-info-json', 'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-attach-info-json',
] ]
compat_opts = parse_compat_opts() compat_opts = parse_compat_opts()

View File

@ -346,7 +346,7 @@ class FFmpegFD(ExternalFD):
return FFmpegPostProcessor().available return FFmpegPostProcessor().available
def _call_downloader(self, tmpfilename, info_dict): def _call_downloader(self, tmpfilename, info_dict):
url = info_dict['url'] urls = [f['url'] for f in info_dict.get('requested_formats', [])] or [info_dict['url']]
ffpp = FFmpegPostProcessor(downloader=self) ffpp = FFmpegPostProcessor(downloader=self)
if not ffpp.available: if not ffpp.available:
self.report_error('m3u8 download detected but ffmpeg could not be found. Please install') self.report_error('m3u8 download detected but ffmpeg could not be found. Please install')
@ -378,7 +378,7 @@ class FFmpegFD(ExternalFD):
# if end_time: # if end_time:
# args += ['-t', compat_str(end_time - start_time)] # args += ['-t', compat_str(end_time - start_time)]
if info_dict.get('http_headers') is not None and re.match(r'^https?://', url): if info_dict.get('http_headers') is not None and re.match(r'^https?://', urls[0]):
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv: # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header. # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
headers = handle_youtubedl_headers(info_dict['http_headers']) headers = handle_youtubedl_headers(info_dict['http_headers'])
@ -436,7 +436,15 @@ class FFmpegFD(ExternalFD):
elif isinstance(conn, compat_str): elif isinstance(conn, compat_str):
args += ['-rtmp_conn', conn] args += ['-rtmp_conn', conn]
args += ['-i', url, '-c', 'copy'] for url in urls:
args += ['-i', url]
args += ['-c', 'copy']
if info_dict.get('requested_formats'):
for (i, fmt) in enumerate(info_dict['requested_formats']):
if fmt.get('acodec') != 'none':
args.extend(['-map', '%d:a:0' % i])
if fmt.get('vcodec') != 'none':
args.extend(['-map', '%d:v:0' % i])
if self.params.get('test', False): if self.params.get('test', False):
args += ['-fs', compat_str(self._TEST_FILE_SIZE)] args += ['-fs', compat_str(self._TEST_FILE_SIZE)]