From 96fccc101f8f579ebd67da176e029803d82634c7 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Sat, 31 Jul 2021 16:23:54 +0530 Subject: [PATCH] [downloader] Allow streaming unmerged formats to stdout using ffmpeg For this to work: 1. The downloader must be ffmpeg 2. The selected formats must have the same protocol 3. The formats must be downloadable by ffmpeg to stdout Partial solution for: https://github.com/ytdl-org/youtube-dl/issues/28146, https://github.com/ytdl-org/youtube-dl/issues/27265 --- yt_dlp/YoutubeDL.py | 24 ++++++++++++++++++------ yt_dlp/downloader/__init__.py | 15 +++++++++------ yt_dlp/downloader/dash.py | 3 ++- yt_dlp/downloader/external.py | 6 +++++- yt_dlp/downloader/hls.py | 3 ++- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index aa8a54a55..11add73ad 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -2405,7 +2405,7 @@ class YoutubeDL(object): } else: params = self.params - fd = get_suitable_downloader(info, params)(self, params) + fd = get_suitable_downloader(info, params, to_stdout=(name == '-'))(self, params) if not test: for ph in self._progress_hooks: fd.add_progress_hook(ph) @@ -2677,6 +2677,8 @@ class YoutubeDL(object): 'Requested formats are incompatible for merge and will be merged into mkv.') def correct_ext(filename): + if filename == '-': + return filename filename_real_ext = os.path.splitext(filename)[1][1:] filename_wo_ext = ( os.path.splitext(filename)[0] @@ -2696,7 +2698,8 @@ class YoutubeDL(object): directly_mergable = FFmpegFD.can_merge_formats(info_dict) if dl_filename is not None: pass - elif (directly_mergable and get_suitable_downloader(info_dict, self.params) == FFmpegFD): + elif (directly_mergable and get_suitable_downloader( + info_dict, self.params, to_stdout=(temp_filename== '-')) == FFmpegFD): info_dict['url'] = '\n'.join(f['url'] for f in requested_formats) success, real_download = self.dl(temp_filename, info_dict) info_dict['__real_download'] = real_download @@ -2713,14 +2716,23 @@ class YoutubeDL(object): 'You have requested merging of multiple formats but ffmpeg is not installed. ' 'The formats won\'t be merged.') + if temp_filename == '-': + reason = ('using a downloader other than ffmpeg' if directly_mergable + else 'but the formats are incompatible for simultaneous download' if merger.available + else 'but ffmpeg is not installed') + self.report_warning( + f'You have requested downloading multiple formats to stdout {reason}. ' + 'The formats will be streamed one after the other') + fname = temp_filename for f in requested_formats: new_info = dict(info_dict) del new_info['requested_formats'] new_info.update(f) - fname = prepend_extension(temp_filename, 'f%s' % f['format_id'], new_info['ext']) - if not self._ensure_dir_exists(fname): - return - downloaded.append(fname) + if temp_filename != '-': + fname = prepend_extension(temp_filename, '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 diff --git a/yt_dlp/downloader/__init__.py b/yt_dlp/downloader/__init__.py index 53393e89f..a0144227e 100644 --- a/yt_dlp/downloader/__init__.py +++ b/yt_dlp/downloader/__init__.py @@ -7,11 +7,12 @@ from ..utils import ( ) -def get_suitable_downloader(info_dict, params={}, default=NO_DEFAULT, protocol=None): +def get_suitable_downloader(info_dict, params={}, default=NO_DEFAULT, protocol=None, to_stdout=False): info_dict['protocol'] = determine_protocol(info_dict) info_copy = info_dict.copy() if protocol: info_copy['protocol'] = protocol + info_copy['to_stdout'] = to_stdout return _get_suitable_downloader(info_copy, params, default) @@ -84,10 +85,11 @@ def _get_suitable_downloader(info_dict, params, default): external_downloader = ( downloaders if isinstance(downloaders, compat_str) or downloaders is None else downloaders.get(shorten_protocol_name(protocol, True), downloaders.get('default'))) - if external_downloader and external_downloader.lower() == 'native': - external_downloader = 'native' - if external_downloader not in (None, 'native'): + if external_downloader is None: + if info_dict['to_stdout'] and FFmpegFD.can_merge_formats(info_dict, params): + return FFmpegFD + elif external_downloader.lower() != 'native': ed = get_external_downloader(external_downloader) if ed.can_download(info_dict, external_downloader): return ed @@ -95,9 +97,10 @@ def _get_suitable_downloader(info_dict, params, default): if protocol in ('m3u8', 'm3u8_native'): if info_dict.get('is_live'): return FFmpegFD - elif external_downloader == 'native': + elif (external_downloader or '').lower() == 'native': return HlsFD - elif get_suitable_downloader(info_dict, params, None, protocol='m3u8_frag_urls'): + elif get_suitable_downloader( + info_dict, params, None, protocol='m3u8_frag_urls', to_stdout=info_dict['to_stdout']): return HlsFD elif params.get('hls_prefer_native') is True: return HlsFD diff --git a/yt_dlp/downloader/dash.py b/yt_dlp/downloader/dash.py index ccc41e158..881ef3a1d 100644 --- a/yt_dlp/downloader/dash.py +++ b/yt_dlp/downloader/dash.py @@ -22,7 +22,8 @@ class DashSegmentsFD(FragmentFD): fragments = info_dict['fragments'][:1] if self.params.get( 'test', False) else info_dict['fragments'] - real_downloader = get_suitable_downloader(info_dict, self.params, None, protocol='dash_frag_urls') + real_downloader = get_suitable_downloader( + info_dict, self.params, None, protocol='dash_frag_urls', to_stdout=(filename== '-')) ctx = { 'filename': filename, diff --git a/yt_dlp/downloader/external.py b/yt_dlp/downloader/external.py index b2a605458..73c2f9edf 100644 --- a/yt_dlp/downloader/external.py +++ b/yt_dlp/downloader/external.py @@ -36,6 +36,7 @@ from ..utils import ( class ExternalFD(FileDownloader): SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps') + can_download_to_stdout = False def real_download(self, filename, info_dict): self.report_destination(filename) @@ -93,7 +94,9 @@ class ExternalFD(FileDownloader): @classmethod def supports(cls, info_dict): - return info_dict['protocol'] in cls.SUPPORTED_PROTOCOLS + return ( + (cls.can_download_to_stdout or not info_dict.get('to_stdout')) + and info_dict['protocol'] in cls.SUPPORTED_PROTOCOLS) @classmethod def can_download(cls, info_dict, path=None): @@ -341,6 +344,7 @@ class HttpieFD(ExternalFD): class FFmpegFD(ExternalFD): SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtsp', 'rtmp', 'rtmp_ffmpeg', 'mms') + can_download_to_stdout = True @classmethod def available(cls, path=None): diff --git a/yt_dlp/downloader/hls.py b/yt_dlp/downloader/hls.py index 79d4ad5e9..6f9dbeedb 100644 --- a/yt_dlp/downloader/hls.py +++ b/yt_dlp/downloader/hls.py @@ -86,7 +86,8 @@ class HlsFD(FragmentFD): if is_webvtt: real_downloader = None # Packing the fragments is not currently supported for external downloader else: - real_downloader = get_suitable_downloader(info_dict, self.params, None, protocol='m3u8_frag_urls') + real_downloader = get_suitable_downloader( + info_dict, self.params, None, protocol='m3u8_frag_urls', to_stdout=(filename== '-')) if real_downloader and not real_downloader.supports_manifest(s): real_downloader = None if real_downloader: