[downloader/external] Add FFmpegFD(fixes #622)
- replace HlsFD and RtspFD - add basic support for downloading part of the video or audio
This commit is contained in:
parent
8ec64ac683
commit
12b84ac8c1
|
@ -1,14 +1,15 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from .external import get_external_downloader
|
|
||||||
from .f4m import F4mFD
|
from .f4m import F4mFD
|
||||||
from .hls import HlsFD
|
from .hls import HlsFD
|
||||||
from .hls import NativeHlsFD
|
|
||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .rtsp import RtspFD
|
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
from .dash import DashSegmentsFD
|
from .dash import DashSegmentsFD
|
||||||
|
from .external import (
|
||||||
|
get_external_downloader,
|
||||||
|
FFmpegFD,
|
||||||
|
)
|
||||||
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_protocol,
|
determine_protocol,
|
||||||
|
@ -16,10 +17,10 @@ from ..utils import (
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
'm3u8_native': NativeHlsFD,
|
'm3u8_native': HlsFD,
|
||||||
'm3u8': HlsFD,
|
'm3u8': FFmpegFD,
|
||||||
'mms': RtspFD,
|
'mms': FFmpegFD,
|
||||||
'rtsp': RtspFD,
|
'rtsp': FFmpegFD,
|
||||||
'f4m': F4mFD,
|
'f4m': F4mFD,
|
||||||
'http_dash_segments': DashSegmentsFD,
|
'http_dash_segments': DashSegmentsFD,
|
||||||
}
|
}
|
||||||
|
@ -30,6 +31,9 @@ def get_suitable_downloader(info_dict, params={}):
|
||||||
protocol = determine_protocol(info_dict)
|
protocol = determine_protocol(info_dict)
|
||||||
info_dict['protocol'] = protocol
|
info_dict['protocol'] = protocol
|
||||||
|
|
||||||
|
if (info_dict.get('start_time') or info_dict.get('end_time')) and FFmpegFD.supports(info_dict):
|
||||||
|
return FFmpegFD
|
||||||
|
|
||||||
external_downloader = params.get('external_downloader')
|
external_downloader = params.get('external_downloader')
|
||||||
if external_downloader is not None:
|
if external_downloader is not None:
|
||||||
ed = get_external_downloader(external_downloader)
|
ed = get_external_downloader(external_downloader)
|
||||||
|
@ -37,7 +41,7 @@ def get_suitable_downloader(info_dict, params={}):
|
||||||
return ed
|
return ed
|
||||||
|
|
||||||
if protocol == 'm3u8' and params.get('hls_prefer_native'):
|
if protocol == 'm3u8' and params.get('hls_prefer_native'):
|
||||||
return NativeHlsFD
|
return HlsFD
|
||||||
|
|
||||||
return PROTOCOL_MAP.get(protocol, HttpFD)
|
return PROTOCOL_MAP.get(protocol, HttpFD)
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,12 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
|
from ..postprocessor.ffmpeg import FFmpegPostProcessor
|
||||||
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
cli_option,
|
cli_option,
|
||||||
cli_valueless_option,
|
cli_valueless_option,
|
||||||
|
@ -11,6 +15,7 @@ from ..utils import (
|
||||||
cli_configuration_args,
|
cli_configuration_args,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
|
handle_youtubedl_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,6 +141,66 @@ class HttpieFD(ExternalFD):
|
||||||
cmd += ['%s:%s' % (key, val)]
|
cmd += ['%s:%s' % (key, val)]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegFD(ExternalFD):
|
||||||
|
@classmethod
|
||||||
|
def supports(cls, info_dict):
|
||||||
|
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
|
||||||
|
|
||||||
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
|
url = info_dict['url']
|
||||||
|
ffpp = FFmpegPostProcessor(downloader=self)
|
||||||
|
ffpp.check_version()
|
||||||
|
|
||||||
|
args = [ffpp.executable, '-y']
|
||||||
|
|
||||||
|
start_time = info_dict.get('start_time', 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', 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(
|
_BY_NAME = dict(
|
||||||
(klass.get_basename(), klass)
|
(klass.get_basename(), klass)
|
||||||
for name, klass in globals().items()
|
for name, klass in globals().items()
|
||||||
|
|
|
@ -1,87 +1,19 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os.path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
|
|
||||||
from ..compat import compat_urlparse
|
from ..compat import compat_urlparse
|
||||||
from ..postprocessor.ffmpeg import FFmpegPostProcessor
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
encodeArgument,
|
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
handle_youtubedl_headers,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HlsFD(FileDownloader):
|
class HlsFD(FragmentFD):
|
||||||
def real_download(self, filename, info_dict):
|
""" A limited implementation that does not require ffmpeg """
|
||||||
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 """
|
|
||||||
|
|
||||||
FD_NAME = 'hlsnative'
|
FD_NAME = 'hlsnative'
|
||||||
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from ..utils import (
|
|
||||||
check_executable,
|
|
||||||
encodeFilename,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RtspFD(FileDownloader):
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
url = info_dict['url']
|
|
||||||
self.report_destination(filename)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
|
|
||||||
if check_executable('mplayer', ['-h']):
|
|
||||||
args = [
|
|
||||||
'mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy',
|
|
||||||
'-dumpstream', '-dumpfile', tmpfilename, url]
|
|
||||||
elif check_executable('mpv', ['-h']):
|
|
||||||
args = [
|
|
||||||
'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url]
|
|
||||||
else:
|
|
||||||
self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install any.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
retval = subprocess.call(args)
|
|
||||||
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' % (args[0], retval))
|
|
||||||
return False
|
|
Loading…
Reference in New Issue