]>
Commit | Line | Data |
---|---|---|
f0b5d6af PH |
1 | from __future__ import unicode_literals |
2 | ||
3bc2ddcc | 3 | import os |
f0b5d6af | 4 | import re |
3bc2ddcc JMF |
5 | import subprocess |
6 | ||
7 | from .common import FileDownloader | |
f9a5affa S |
8 | from .fragment import FragmentFD |
9 | ||
10 | from ..compat import compat_urlparse | |
11 | from ..postprocessor.ffmpeg import FFmpegPostProcessor | |
1cc79574 | 12 | from ..utils import ( |
027008b1 | 13 | encodeArgument, |
3bc2ddcc | 14 | encodeFilename, |
fcd9e423 | 15 | sanitize_open, |
94e8c804 | 16 | handle_youtubedl_headers, |
3bc2ddcc JMF |
17 | ) |
18 | ||
19 | ||
20 | class HlsFD(FileDownloader): | |
21 | def real_download(self, filename, info_dict): | |
22 | url = info_dict['url'] | |
23 | self.report_destination(filename) | |
24 | tmpfilename = self.temp_name(filename) | |
25 | ||
63948fc6 | 26 | ffpp = FFmpegPostProcessor(downloader=self) |
8ac27a68 | 27 | if not ffpp.available: |
0e44f90e | 28 | self.report_error('m3u8 download detected but ffmpeg or avconv could not be found. Please install one.') |
baf29075 | 29 | return False |
5f9b8394 | 30 | ffpp.check_version() |
027008b1 | 31 | |
f1028194 S |
32 | args = [ffpp.executable, '-y'] |
33 | ||
985e4fdc | 34 | if info_dict['http_headers'] and re.match(r'^https?://', url): |
f1028194 S |
35 | # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv: |
36 | # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header. | |
94e8c804 | 37 | headers = handle_youtubedl_headers(info_dict['http_headers']) |
f1028194 S |
38 | args += [ |
39 | '-headers', | |
94e8c804 | 40 | ''.join('%s: %s\r\n' % (key, val) for key, val in headers.items())] |
f1028194 | 41 | |
7d106a65 JMF |
42 | args += ['-i', url, '-c', 'copy'] |
43 | if self.params.get('hls_use_mpegts', False): | |
44 | args += ['-f', 'mpegts'] | |
45 | else: | |
46 | args += ['-f', 'mp4', '-bsf:a', 'aac_adtstoasc'] | |
f1028194 S |
47 | |
48 | args = [encodeArgument(opt) for opt in args] | |
8a7bbd16 | 49 | args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True)) |
027008b1 | 50 | |
7393746d S |
51 | self._debug_cmd(args) |
52 | ||
9d90e7de JMF |
53 | proc = subprocess.Popen(args, stdin=subprocess.PIPE) |
54 | try: | |
55 | retval = proc.wait() | |
56 | except KeyboardInterrupt: | |
57 | # subprocces.run would send the SIGKILL signal to ffmpeg and the | |
58 | # mp4 file couldn't be played, but if we ask ffmpeg to quit it | |
59 | # produces a file that is playable (this is mostly useful for live | |
60 | # streams) | |
61 | proc.communicate(b'q') | |
62 | raise | |
3bc2ddcc JMF |
63 | if retval == 0: |
64 | fsize = os.path.getsize(encodeFilename(tmpfilename)) | |
4a3da4eb | 65 | self.to_screen('\r[%s] %s bytes' % (args[0], fsize)) |
3bc2ddcc JMF |
66 | self.try_rename(tmpfilename, filename) |
67 | self._hook_progress({ | |
68 | 'downloaded_bytes': fsize, | |
69 | 'total_bytes': fsize, | |
70 | 'filename': filename, | |
71 | 'status': 'finished', | |
72 | }) | |
73 | return True | |
74 | else: | |
0e44f90e | 75 | self.to_stderr('\n') |
73fac4e9 | 76 | self.report_error('%s exited with code %d' % (ffpp.basename, retval)) |
3bc2ddcc | 77 | return False |
f0b5d6af PH |
78 | |
79 | ||
f9a5affa | 80 | class NativeHlsFD(FragmentFD): |
f0b5d6af PH |
81 | """ A more limited implementation that does not require ffmpeg """ |
82 | ||
f9a5affa S |
83 | FD_NAME = 'hlsnative' |
84 | ||
f0b5d6af | 85 | def real_download(self, filename, info_dict): |
f9a5affa S |
86 | man_url = info_dict['url'] |
87 | self.to_screen('[%s] Downloading m3u8 manifest' % self.FD_NAME) | |
88 | manifest = self.ydl.urlopen(man_url).read() | |
f0b5d6af | 89 | |
f9a5affa S |
90 | s = manifest.decode('utf-8', 'ignore') |
91 | fragment_urls = [] | |
f0b5d6af PH |
92 | for line in s.splitlines(): |
93 | line = line.strip() | |
94 | if line and not line.startswith('#'): | |
95 | segment_url = ( | |
96 | line | |
97 | if re.match(r'^https?://', line) | |
f9a5affa S |
98 | else compat_urlparse.urljoin(man_url, line)) |
99 | fragment_urls.append(segment_url) | |
100 | # We only download the first fragment during the test | |
101 | if self.params.get('test', False): | |
b686fc18 | 102 | break |
f0b5d6af | 103 | |
f9a5affa | 104 | ctx = { |
f0b5d6af | 105 | 'filename': filename, |
f9a5affa S |
106 | 'total_frags': len(fragment_urls), |
107 | } | |
108 | ||
109 | self._prepare_and_start_frag_download(ctx) | |
110 | ||
111 | frags_filenames = [] | |
112 | for i, frag_url in enumerate(fragment_urls): | |
113 | frag_filename = '%s-Frag%d' % (ctx['tmpfilename'], i) | |
114 | success = ctx['dl'].download(frag_filename, {'url': frag_url}) | |
115 | if not success: | |
116 | return False | |
fcd9e423 S |
117 | down, frag_sanitized = sanitize_open(frag_filename, 'rb') |
118 | ctx['dest_stream'].write(down.read()) | |
133a2b4a | 119 | down.close() |
fcd9e423 | 120 | frags_filenames.append(frag_sanitized) |
f9a5affa S |
121 | |
122 | self._finish_frag_download(ctx) | |
123 | ||
124 | for frag_file in frags_filenames: | |
fcd9e423 | 125 | os.remove(encodeFilename(frag_file)) |
f9a5affa | 126 | |
f0b5d6af | 127 | return True |