e00E a0b8bd12e7 Make the native hls downloader use disk space more efficiently
It now saves the url of the most recently completed segment to figure out from where it has to resume.
Before, it kept ALL segments which resulted in using double the disk space neccessary.
2015-10-07 19:24:54 +02:00

127 lines
4.4 KiB
Python

from __future__ import unicode_literals
import os
import re
import subprocess
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,
)
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']:
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
args += [
'-headers',
''.join('%s: %s\r\n' % (key, val) for key, val in info_dict['http_headers'].items())]
args += ['-i', url, '-f', 'mp4', '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
args = [encodeArgument(opt) for opt in args]
args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
self._debug_cmd(args)
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' % (ffpp.basename, retval))
return False
class NativeHlsFD(FragmentFD):
""" A more limited implementation that does not require ffmpeg """
FD_NAME = 'hlsnative'
def real_download(self, filename, info_dict):
man_url = info_dict['url']
self.to_screen('[%s] Downloading m3u8 manifest' % self.FD_NAME)
manifest = self.ydl.urlopen(man_url).read()
last_downloaded_segment_filename = filename + ".last_downloaded_segment"
last_downloaded_segment = None
if os.path.isfile(last_downloaded_segment_filename):
segment_file = open(last_downloaded_segment_filename, 'r')
last_downloaded_segment = segment_file.readline().strip()
segment_file.close()
s = manifest.decode('utf-8', 'ignore')
fragment_urls = []
arrived_at_last_downloaded_segment = (last_downloaded_segment is None)
for line in s.splitlines():
line = line.strip()
if line and not line.startswith('#'):
segment_url = (
line
if re.match(r'^https?://', line)
else compat_urlparse.urljoin(man_url, line))
if arrived_at_last_downloaded_segment:
fragment_urls.append(segment_url)
elif segment_url == last_downloaded_segment:
arrived_at_last_downloaded_segment = True
# We only download the first fragment during the test
if self.params.get('test', False):
break
ctx = {
'filename': filename,
'total_frags': len(fragment_urls),
}
self._prepare_and_start_frag_download(ctx)
for i, frag_url in enumerate(fragment_urls):
frag_filename = '%s-Frag%d' % (ctx['tmpfilename'], i)
success = ctx['dl'].download(frag_filename, {'url': frag_url})
if not success:
return False
down, frag_sanitized = sanitize_open(frag_filename, 'rb')
ctx['dest_stream'].write(down.read())
down.close()
os.remove(encodeFilename(frag_sanitized))
segments_file = open(last_downloaded_segment_filename, 'w')
segments_file.write(frag_url + "\n")
segments_file.close()
self._finish_frag_download(ctx)
if last_downloaded_segment is not None:
os.remove(last_downloaded_segment_filename)
return True