Merge pull request #8611 from remitamine/ffmpegfd
[downloader/external] Add FFmpegFD
This commit is contained in:
		@@ -1,14 +1,16 @@
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from .common import FileDownloader
 | 
			
		||||
from .external import get_external_downloader
 | 
			
		||||
from .f4m import F4mFD
 | 
			
		||||
from .hls import HlsFD
 | 
			
		||||
from .hls import NativeHlsFD
 | 
			
		||||
from .http import HttpFD
 | 
			
		||||
from .rtsp import RtspFD
 | 
			
		||||
from .rtmp import RtmpFD
 | 
			
		||||
from .dash import DashSegmentsFD
 | 
			
		||||
from .rtsp import RtspFD
 | 
			
		||||
from .external import (
 | 
			
		||||
    get_external_downloader,
 | 
			
		||||
    FFmpegFD,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from ..utils import (
 | 
			
		||||
    determine_protocol,
 | 
			
		||||
@@ -16,8 +18,8 @@ from ..utils import (
 | 
			
		||||
 | 
			
		||||
PROTOCOL_MAP = {
 | 
			
		||||
    'rtmp': RtmpFD,
 | 
			
		||||
    'm3u8_native': NativeHlsFD,
 | 
			
		||||
    'm3u8': HlsFD,
 | 
			
		||||
    'm3u8_native': HlsFD,
 | 
			
		||||
    'm3u8': FFmpegFD,
 | 
			
		||||
    'mms': RtspFD,
 | 
			
		||||
    'rtsp': RtspFD,
 | 
			
		||||
    'f4m': F4mFD,
 | 
			
		||||
@@ -30,14 +32,17 @@ def get_suitable_downloader(info_dict, params={}):
 | 
			
		||||
    protocol = determine_protocol(info_dict)
 | 
			
		||||
    info_dict['protocol'] = protocol
 | 
			
		||||
 | 
			
		||||
    # if (info_dict.get('start_time') or info_dict.get('end_time')) and not info_dict.get('requested_formats') and FFmpegFD.can_download(info_dict):
 | 
			
		||||
    #     return FFmpegFD
 | 
			
		||||
 | 
			
		||||
    external_downloader = params.get('external_downloader')
 | 
			
		||||
    if external_downloader is not None:
 | 
			
		||||
        ed = get_external_downloader(external_downloader)
 | 
			
		||||
        if ed.supports(info_dict):
 | 
			
		||||
        if ed.can_download(info_dict):
 | 
			
		||||
            return ed
 | 
			
		||||
 | 
			
		||||
    if protocol == 'm3u8' and params.get('hls_prefer_native'):
 | 
			
		||||
        return NativeHlsFD
 | 
			
		||||
        return HlsFD
 | 
			
		||||
 | 
			
		||||
    return PROTOCOL_MAP.get(protocol, HttpFD)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,11 @@ from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import os.path
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from .common import FileDownloader
 | 
			
		||||
from ..postprocessor.ffmpeg import FFmpegPostProcessor, EXT_TO_OUT_FORMATS
 | 
			
		||||
from ..utils import (
 | 
			
		||||
    cli_option,
 | 
			
		||||
    cli_valueless_option,
 | 
			
		||||
@@ -11,6 +14,8 @@ from ..utils import (
 | 
			
		||||
    cli_configuration_args,
 | 
			
		||||
    encodeFilename,
 | 
			
		||||
    encodeArgument,
 | 
			
		||||
    handle_youtubedl_headers,
 | 
			
		||||
    check_executable,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -45,10 +50,18 @@ class ExternalFD(FileDownloader):
 | 
			
		||||
    def exe(self):
 | 
			
		||||
        return self.params.get('external_downloader')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def available(cls):
 | 
			
		||||
        return check_executable(cls.get_basename(), [cls.AVAILABLE_OPT])
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def supports(cls, info_dict):
 | 
			
		||||
        return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def can_download(cls, info_dict):
 | 
			
		||||
        return cls.available() and cls.supports(info_dict)
 | 
			
		||||
 | 
			
		||||
    def _option(self, command_option, param):
 | 
			
		||||
        return cli_option(self.params, command_option, param)
 | 
			
		||||
 | 
			
		||||
@@ -76,6 +89,8 @@ class ExternalFD(FileDownloader):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CurlFD(ExternalFD):
 | 
			
		||||
    AVAILABLE_OPT = '-V'
 | 
			
		||||
 | 
			
		||||
    def _make_cmd(self, tmpfilename, info_dict):
 | 
			
		||||
        cmd = [self.exe, '--location', '-o', tmpfilename]
 | 
			
		||||
        for key, val in info_dict['http_headers'].items():
 | 
			
		||||
@@ -89,6 +104,8 @@ class CurlFD(ExternalFD):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AxelFD(ExternalFD):
 | 
			
		||||
    AVAILABLE_OPT = '-V'
 | 
			
		||||
 | 
			
		||||
    def _make_cmd(self, tmpfilename, info_dict):
 | 
			
		||||
        cmd = [self.exe, '-o', tmpfilename]
 | 
			
		||||
        for key, val in info_dict['http_headers'].items():
 | 
			
		||||
@@ -99,6 +116,8 @@ class AxelFD(ExternalFD):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WgetFD(ExternalFD):
 | 
			
		||||
    AVAILABLE_OPT = '--version'
 | 
			
		||||
 | 
			
		||||
    def _make_cmd(self, tmpfilename, info_dict):
 | 
			
		||||
        cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies']
 | 
			
		||||
        for key, val in info_dict['http_headers'].items():
 | 
			
		||||
@@ -112,6 +131,8 @@ class WgetFD(ExternalFD):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Aria2cFD(ExternalFD):
 | 
			
		||||
    AVAILABLE_OPT = '-v'
 | 
			
		||||
 | 
			
		||||
    def _make_cmd(self, tmpfilename, info_dict):
 | 
			
		||||
        cmd = [self.exe, '-c']
 | 
			
		||||
        cmd += self._configuration_args([
 | 
			
		||||
@@ -130,12 +151,85 @@ class Aria2cFD(ExternalFD):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HttpieFD(ExternalFD):
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def available(cls):
 | 
			
		||||
        return check_executable('http', ['--version'])
 | 
			
		||||
 | 
			
		||||
    def _make_cmd(self, tmpfilename, info_dict):
 | 
			
		||||
        cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
 | 
			
		||||
        for key, val in info_dict['http_headers'].items():
 | 
			
		||||
            cmd += ['%s:%s' % (key, val)]
 | 
			
		||||
        return cmd
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FFmpegFD(ExternalFD):
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def supports(cls, info_dict):
 | 
			
		||||
        return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def available(cls):
 | 
			
		||||
        return FFmpegPostProcessor().available
 | 
			
		||||
 | 
			
		||||
    def _call_downloader(self, tmpfilename, info_dict):
 | 
			
		||||
        url = info_dict['url']
 | 
			
		||||
        ffpp = FFmpegPostProcessor(downloader=self)
 | 
			
		||||
        if not ffpp.available:
 | 
			
		||||
            self.report_error('m3u8 download detected but ffmpeg or avconv could not be found. Please install one.')
 | 
			
		||||
            return False
 | 
			
		||||
        ffpp.check_version()
 | 
			
		||||
 | 
			
		||||
        args = [ffpp.executable, '-y']
 | 
			
		||||
 | 
			
		||||
        args += self._configuration_args()
 | 
			
		||||
 | 
			
		||||
        # start_time = info_dict.get('start_time') or 0
 | 
			
		||||
        # if start_time:
 | 
			
		||||
        #     args += ['-ss', compat_str(start_time)]
 | 
			
		||||
        # end_time = info_dict.get('end_time')
 | 
			
		||||
        # if end_time:
 | 
			
		||||
        #     args += ['-t', compat_str(end_time - start_time)]
 | 
			
		||||
 | 
			
		||||
        if info_dict['http_headers'] and re.match(r'^https?://', url):
 | 
			
		||||
            # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
 | 
			
		||||
            # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
 | 
			
		||||
            headers = handle_youtubedl_headers(info_dict['http_headers'])
 | 
			
		||||
            args += [
 | 
			
		||||
                '-headers',
 | 
			
		||||
                ''.join('%s: %s\r\n' % (key, val) for key, val in headers.items())]
 | 
			
		||||
 | 
			
		||||
        args += ['-i', url, '-c', 'copy']
 | 
			
		||||
        if info_dict.get('protocol') == 'm3u8':
 | 
			
		||||
            if self.params.get('hls_use_mpegts', False):
 | 
			
		||||
                args += ['-f', 'mpegts']
 | 
			
		||||
            else:
 | 
			
		||||
                args += ['-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
 | 
			
		||||
        else:
 | 
			
		||||
            args += ['-f', EXT_TO_OUT_FORMATS.get(info_dict['ext'], info_dict['ext'])]
 | 
			
		||||
 | 
			
		||||
        args = [encodeArgument(opt) for opt in args]
 | 
			
		||||
        args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
 | 
			
		||||
 | 
			
		||||
        self._debug_cmd(args)
 | 
			
		||||
 | 
			
		||||
        proc = subprocess.Popen(args, stdin=subprocess.PIPE)
 | 
			
		||||
        try:
 | 
			
		||||
            retval = proc.wait()
 | 
			
		||||
        except KeyboardInterrupt:
 | 
			
		||||
            # subprocces.run would send the SIGKILL signal to ffmpeg and the
 | 
			
		||||
            # mp4 file couldn't be played, but if we ask ffmpeg to quit it
 | 
			
		||||
            # produces a file that is playable (this is mostly useful for live
 | 
			
		||||
            # streams). Note that Windows is not affected and produces playable
 | 
			
		||||
            # files (see https://github.com/rg3/youtube-dl/issues/8300).
 | 
			
		||||
            if sys.platform != 'win32':
 | 
			
		||||
                proc.communicate(b'q')
 | 
			
		||||
            raise
 | 
			
		||||
        return retval
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AVconvFD(FFmpegFD):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
_BY_NAME = dict(
 | 
			
		||||
    (klass.get_basename(), klass)
 | 
			
		||||
    for name, klass in globals().items()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,87 +1,19 @@
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import os.path
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from .common import FileDownloader
 | 
			
		||||
from .fragment import FragmentFD
 | 
			
		||||
 | 
			
		||||
from ..compat import compat_urlparse
 | 
			
		||||
from ..postprocessor.ffmpeg import FFmpegPostProcessor
 | 
			
		||||
from ..utils import (
 | 
			
		||||
    encodeArgument,
 | 
			
		||||
    encodeFilename,
 | 
			
		||||
    sanitize_open,
 | 
			
		||||
    handle_youtubedl_headers,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HlsFD(FileDownloader):
 | 
			
		||||
    def real_download(self, filename, info_dict):
 | 
			
		||||
        url = info_dict['url']
 | 
			
		||||
        self.report_destination(filename)
 | 
			
		||||
        tmpfilename = self.temp_name(filename)
 | 
			
		||||
 | 
			
		||||
        ffpp = FFmpegPostProcessor(downloader=self)
 | 
			
		||||
        if not ffpp.available:
 | 
			
		||||
            self.report_error('m3u8 download detected but ffmpeg or avconv could not be found. Please install one.')
 | 
			
		||||
            return False
 | 
			
		||||
        ffpp.check_version()
 | 
			
		||||
 | 
			
		||||
        args = [ffpp.executable, '-y']
 | 
			
		||||
 | 
			
		||||
        if info_dict['http_headers'] and re.match(r'^https?://', url):
 | 
			
		||||
            # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
 | 
			
		||||
            # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
 | 
			
		||||
            headers = handle_youtubedl_headers(info_dict['http_headers'])
 | 
			
		||||
            args += [
 | 
			
		||||
                '-headers',
 | 
			
		||||
                ''.join('%s: %s\r\n' % (key, val) for key, val in headers.items())]
 | 
			
		||||
 | 
			
		||||
        args += ['-i', url, '-c', 'copy']
 | 
			
		||||
        if self.params.get('hls_use_mpegts', False):
 | 
			
		||||
            args += ['-f', 'mpegts']
 | 
			
		||||
        else:
 | 
			
		||||
            args += ['-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
 | 
			
		||||
 | 
			
		||||
        args = [encodeArgument(opt) for opt in args]
 | 
			
		||||
        args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
 | 
			
		||||
 | 
			
		||||
        self._debug_cmd(args)
 | 
			
		||||
 | 
			
		||||
        proc = subprocess.Popen(args, stdin=subprocess.PIPE)
 | 
			
		||||
        try:
 | 
			
		||||
            retval = proc.wait()
 | 
			
		||||
        except KeyboardInterrupt:
 | 
			
		||||
            # subprocces.run would send the SIGKILL signal to ffmpeg and the
 | 
			
		||||
            # mp4 file couldn't be played, but if we ask ffmpeg to quit it
 | 
			
		||||
            # produces a file that is playable (this is mostly useful for live
 | 
			
		||||
            # streams). Note that Windows is not affected and produces playable
 | 
			
		||||
            # files (see https://github.com/rg3/youtube-dl/issues/8300).
 | 
			
		||||
            if sys.platform != 'win32':
 | 
			
		||||
                proc.communicate(b'q')
 | 
			
		||||
            raise
 | 
			
		||||
        if retval == 0:
 | 
			
		||||
            fsize = os.path.getsize(encodeFilename(tmpfilename))
 | 
			
		||||
            self.to_screen('\r[%s] %s bytes' % (args[0], fsize))
 | 
			
		||||
            self.try_rename(tmpfilename, filename)
 | 
			
		||||
            self._hook_progress({
 | 
			
		||||
                'downloaded_bytes': fsize,
 | 
			
		||||
                'total_bytes': fsize,
 | 
			
		||||
                'filename': filename,
 | 
			
		||||
                'status': 'finished',
 | 
			
		||||
            })
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            self.to_stderr('\n')
 | 
			
		||||
            self.report_error('%s exited with code %d' % (ffpp.basename, retval))
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NativeHlsFD(FragmentFD):
 | 
			
		||||
    """ A more limited implementation that does not require ffmpeg """
 | 
			
		||||
class HlsFD(FragmentFD):
 | 
			
		||||
    """ A limited implementation that does not require ffmpeg """
 | 
			
		||||
 | 
			
		||||
    FD_NAME = 'hlsnative'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,19 @@ from ..utils import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
EXT_TO_OUT_FORMATS = {
 | 
			
		||||
    "aac": "adts",
 | 
			
		||||
    "m4a": "ipod",
 | 
			
		||||
    "mka": "matroska",
 | 
			
		||||
    "mkv": "matroska",
 | 
			
		||||
    "mpg": "mpeg",
 | 
			
		||||
    "ogv": "ogg",
 | 
			
		||||
    "ts": "mpegts",
 | 
			
		||||
    "wma": "asf",
 | 
			
		||||
    "wmv": "asf",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FFmpegPostProcessorError(PostProcessingError):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user