1 from __future__
import unicode_literals
10 from .common
import AudioConversionError
, PostProcessor
12 from ..compat
import (
13 compat_subprocess_get_DEVNULL
,
30 EXT_TO_OUT_FORMATS
= {
48 'vorbis': 'libvorbis',
53 class FFmpegPostProcessorError(PostProcessingError
):
57 class FFmpegPostProcessor(PostProcessor
):
58 def __init__(self
, downloader
=None):
59 PostProcessor
.__init
__(self
, downloader
)
60 self
._determine
_executables
()
62 def check_version(self
):
63 if not self
.available
:
64 raise FFmpegPostProcessorError('ffmpeg or avconv not found. Please install one.')
66 required_version
= '10-0' if self
.basename
== 'avconv' else '1.0'
67 if is_outdated_version(
68 self
._versions
[self
.basename
], required_version
):
69 warning
= 'Your copy of %s is outdated, update %s to version %s or newer if you encounter any errors.' % (
70 self
.basename
, self
.basename
, required_version
)
72 self
._downloader
.report_warning(warning
)
75 def get_versions(downloader
=None):
76 return FFmpegPostProcessor(downloader
)._versions
78 def _determine_executables(self
):
79 programs
= ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
83 self
.probe_basename
= None
88 prefer_ffmpeg
= self
._downloader
.params
.get('prefer_ffmpeg', False)
89 location
= self
._downloader
.params
.get('ffmpeg_location')
90 if location
is not None:
91 if not os
.path
.exists(location
):
92 self
._downloader
.report_warning(
93 'ffmpeg-location %s does not exist! '
94 'Continuing without avconv/ffmpeg.' % (location
))
97 elif not os
.path
.isdir(location
):
98 basename
= os
.path
.splitext(os
.path
.basename(location
))[0]
99 if basename
not in programs
:
100 self
._downloader
.report_warning(
101 'Cannot identify executable %s, its basename should be one of %s. '
102 'Continuing without avconv/ffmpeg.' %
103 (location
, ', '.join(programs
)))
106 location
= os
.path
.dirname(os
.path
.abspath(location
))
107 if basename
in ('ffmpeg', 'ffprobe'):
111 (p
, os
.path
.join(location
, p
)) for p
in programs
)
112 self
._versions
= dict(
113 (p
, get_exe_version(self
._paths
[p
], args
=['-version']))
115 if self
._versions
is None:
116 self
._versions
= dict(
117 (p
, get_exe_version(p
, args
=['-version'])) for p
in programs
)
118 self
._paths
= dict((p
, p
) for p
in programs
)
121 prefs
= ('ffmpeg', 'avconv')
123 prefs
= ('avconv', 'ffmpeg')
125 if self
._versions
[p
]:
130 prefs
= ('ffprobe', 'avprobe')
132 prefs
= ('avprobe', 'ffprobe')
134 if self
._versions
[p
]:
135 self
.probe_basename
= p
140 return self
.basename
is not None
143 def executable(self
):
144 return self
._paths
[self
.basename
]
147 def probe_available(self
):
148 return self
.probe_basename
is not None
151 def probe_executable(self
):
152 return self
._paths
[self
.probe_basename
]
154 def get_audio_codec(self
, path
):
155 if not self
.probe_available
:
156 raise PostProcessingError('ffprobe or avprobe not found. Please install one.')
159 encodeFilename(self
.probe_executable
, True),
160 encodeArgument('-show_streams'),
161 encodeFilename(self
._ffmpeg
_filename
_argument
(path
), True)]
162 if self
._downloader
.params
.get('verbose', False):
163 self
._downloader
.to_screen('[debug] %s command line: %s' % (self
.basename
, shell_quote(cmd
)))
164 handle
= subprocess
.Popen(cmd
, stderr
=compat_subprocess_get_DEVNULL(), stdout
=subprocess
.PIPE
, stdin
=subprocess
.PIPE
)
165 output
= handle
.communicate()[0]
166 if handle
.wait() != 0:
168 except (IOError, OSError):
171 for line
in output
.decode('ascii', 'ignore').split('\n'):
172 if line
.startswith('codec_name='):
173 audio_codec
= line
.split('=')[1].strip()
174 elif line
.strip() == 'codec_type=audio' and audio_codec
is not None:
178 def run_ffmpeg_multiple_files(self
, input_paths
, out_path
, opts
):
182 os
.stat(encodeFilename(path
)).st_mtime
for path
in input_paths
)
184 opts
+= self
._configuration
_args
()
187 for path
in input_paths
:
189 encodeArgument('-i'),
190 encodeFilename(self
._ffmpeg
_filename
_argument
(path
), True)
192 cmd
= ([encodeFilename(self
.executable
, True), encodeArgument('-y')] +
194 [encodeArgument(o
) for o
in opts
] +
195 [encodeFilename(self
._ffmpeg
_filename
_argument
(out_path
), True)])
197 if self
._downloader
.params
.get('verbose', False):
198 self
._downloader
.to_screen('[debug] ffmpeg command line: %s' % shell_quote(cmd
))
199 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, stdin
=subprocess
.PIPE
)
200 stdout
, stderr
= p
.communicate()
201 if p
.returncode
!= 0:
202 stderr
= stderr
.decode('utf-8', 'replace')
203 msg
= stderr
.strip().split('\n')[-1]
204 raise FFmpegPostProcessorError(msg
)
205 self
.try_utime(out_path
, oldest_mtime
, oldest_mtime
)
207 def run_ffmpeg(self
, path
, out_path
, opts
):
208 self
.run_ffmpeg_multiple_files([path
], out_path
, opts
)
210 def _ffmpeg_filename_argument(self
, fn
):
211 # Always use 'file:' because the filename may contain ':' (ffmpeg
212 # interprets that as a protocol) or can start with '-' (-- is broken in
213 # ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details)
214 # Also leave '-' intact in order not to break streaming to stdout.
215 return 'file:' + fn
if fn
!= '-' else fn
218 class FFmpegExtractAudioPP(FFmpegPostProcessor
):
219 def __init__(self
, downloader
=None, preferredcodec
=None, preferredquality
=None, nopostoverwrites
=False):
220 FFmpegPostProcessor
.__init
__(self
, downloader
)
221 if preferredcodec
is None:
222 preferredcodec
= 'best'
223 self
._preferredcodec
= preferredcodec
224 self
._preferredquality
= preferredquality
225 self
._nopostoverwrites
= nopostoverwrites
227 def run_ffmpeg(self
, path
, out_path
, codec
, more_opts
):
231 acodec_opts
= ['-acodec', codec
]
232 opts
= ['-vn'] + acodec_opts
+ more_opts
234 FFmpegPostProcessor
.run_ffmpeg(self
, path
, out_path
, opts
)
235 except FFmpegPostProcessorError
as err
:
236 raise AudioConversionError(err
.msg
)
238 def run(self
, information
):
239 path
= information
['filepath']
241 filecodec
= self
.get_audio_codec(path
)
242 if filecodec
is None:
243 raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe')
246 if self
._preferredcodec
== 'best' or self
._preferredcodec
== filecodec
or (self
._preferredcodec
== 'm4a' and filecodec
== 'aac'):
247 if filecodec
== 'aac' and self
._preferredcodec
in ['m4a', 'best']:
248 # Lossless, but in another container
251 more_opts
= ['-bsf:a', 'aac_adtstoasc']
252 elif filecodec
in ['aac', 'flac', 'mp3', 'vorbis', 'opus']:
253 # Lossless if possible
255 extension
= filecodec
256 if filecodec
== 'aac':
257 more_opts
= ['-f', 'adts']
258 if filecodec
== 'vorbis':
262 acodec
= 'libmp3lame'
265 if self
._preferredquality
is not None:
266 if int(self
._preferredquality
) < 10:
267 more_opts
+= ['-q:a', self
._preferredquality
]
269 more_opts
+= ['-b:a', self
._preferredquality
+ 'k']
271 # We convert the audio (lossy if codec is lossy)
272 acodec
= ACODECS
[self
._preferredcodec
]
273 extension
= self
._preferredcodec
275 if self
._preferredquality
is not None:
276 # The opus codec doesn't support the -aq option
277 if int(self
._preferredquality
) < 10 and extension
!= 'opus':
278 more_opts
+= ['-q:a', self
._preferredquality
]
280 more_opts
+= ['-b:a', self
._preferredquality
+ 'k']
281 if self
._preferredcodec
== 'aac':
282 more_opts
+= ['-f', 'adts']
283 if self
._preferredcodec
== 'm4a':
284 more_opts
+= ['-bsf:a', 'aac_adtstoasc']
285 if self
._preferredcodec
== 'vorbis':
287 if self
._preferredcodec
== 'wav':
289 more_opts
+= ['-f', 'wav']
291 prefix
, sep
, ext
= path
.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups
292 new_path
= prefix
+ sep
+ extension
294 information
['filepath'] = new_path
295 information
['ext'] = extension
297 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
298 if (new_path
== path
or
299 (self
._nopostoverwrites
and os
.path
.exists(encodeFilename(new_path
)))):
300 self
._downloader
.to_screen('[ffmpeg] Post-process file %s exists, skipping' % new_path
)
301 return [], information
304 self
._downloader
.to_screen('[ffmpeg] Destination: ' + new_path
)
305 self
.run_ffmpeg(path
, new_path
, acodec
, more_opts
)
306 except AudioConversionError
as e
:
307 raise PostProcessingError(
308 'audio conversion failed: ' + e
.msg
)
310 raise PostProcessingError('error running ' + self
.basename
)
312 # Try to update the date time for extracted audio file.
313 if information
.get('filetime') is not None:
315 new_path
, time
.time(), information
['filetime'],
316 errnote
='Cannot update utime of audio file')
318 return [path
], information
321 class FFmpegVideoConvertorPP(FFmpegPostProcessor
):
322 def __init__(self
, downloader
=None, preferedformat
=None):
323 super(FFmpegVideoConvertorPP
, self
).__init
__(downloader
)
324 self
._preferedformat
= preferedformat
326 def run(self
, information
):
327 path
= information
['filepath']
328 if information
['ext'] == self
._preferedformat
:
329 self
._downloader
.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path
, self
._preferedformat
))
330 return [], information
332 if self
._preferedformat
== 'avi':
333 options
.extend(['-c:v', 'libxvid', '-vtag', 'XVID'])
334 prefix
, sep
, ext
= path
.rpartition('.')
335 outpath
= prefix
+ sep
+ self
._preferedformat
336 self
._downloader
.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information
['ext'], self
._preferedformat
) + outpath
)
337 self
.run_ffmpeg(path
, outpath
, options
)
338 information
['filepath'] = outpath
339 information
['format'] = self
._preferedformat
340 information
['ext'] = self
._preferedformat
341 return [path
], information
344 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor
):
345 def run(self
, information
):
346 if information
['ext'] not in ('mp4', 'webm', 'mkv'):
347 self
._downloader
.to_screen('[ffmpeg] Subtitles can only be embedded in mp4, webm or mkv files')
348 return [], information
349 subtitles
= information
.get('requested_subtitles')
351 self
._downloader
.to_screen('[ffmpeg] There aren\'t any subtitles to embed')
352 return [], information
354 filename
= information
['filepath']
356 ext
= information
['ext']
359 webm_vtt_warn
= False
361 for lang
, sub_info
in subtitles
.items():
362 sub_ext
= sub_info
['ext']
363 if ext
!= 'webm' or ext
== 'webm' and sub_ext
== 'vtt':
364 sub_langs
.append(lang
)
365 sub_filenames
.append(subtitles_filename(filename
, lang
, sub_ext
))
367 if not webm_vtt_warn
and ext
== 'webm' and sub_ext
!= 'vtt':
369 self
._downloader
.to_screen('[ffmpeg] Only WebVTT subtitles can be embedded in webm files')
372 return [], information
374 input_files
= [filename
] + sub_filenames
379 # Don't copy the existing subtitles, we may be running the
380 # postprocessor a second time
383 if information
['ext'] == 'mp4':
384 opts
+= ['-c:s', 'mov_text']
385 for (i
, lang
) in enumerate(sub_langs
):
386 opts
.extend(['-map', '%d:0' % (i
+ 1)])
387 lang_code
= ISO639Utils
.short2long(lang
)
388 if lang_code
is not None:
389 opts
.extend(['-metadata:s:s:%d' % i
, 'language=%s' % lang_code
])
391 temp_filename
= prepend_extension(filename
, 'temp')
392 self
._downloader
.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename
)
393 self
.run_ffmpeg_multiple_files(input_files
, temp_filename
, opts
)
394 os
.remove(encodeFilename(filename
))
395 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
397 return sub_filenames
, information
400 class FFmpegMetadataPP(FFmpegPostProcessor
):
404 def add(meta_list
, info_list
=None):
406 info_list
= meta_list
407 if not isinstance(meta_list
, (list, tuple)):
408 meta_list
= (meta_list
,)
409 if not isinstance(info_list
, (list, tuple)):
410 info_list
= (info_list
,)
411 for info_f
in info_list
:
412 if info
.get(info_f
) is not None:
413 for meta_f
in meta_list
:
414 metadata
[meta_f
] = info
[info_f
]
417 add('title', ('track', 'title'))
418 add('date', 'upload_date')
419 add(('description', 'comment'), 'description')
420 add('purl', 'webpage_url')
421 add('track', 'track_number')
422 add('artist', ('artist', 'creator', 'uploader', 'uploader_id'))
426 add('disc', 'disc_number')
429 self
._downloader
.to_screen('[ffmpeg] There isn\'t any metadata to add')
432 filename
= info
['filepath']
433 temp_filename
= prepend_extension(filename
, 'temp')
434 in_filenames
= [filename
]
437 if info
['ext'] == 'm4a':
438 options
.extend(['-vn', '-acodec', 'copy'])
440 options
.extend(['-c', 'copy'])
442 for (name
, value
) in metadata
.items():
443 options
.extend(['-metadata', '%s=%s' % (name
, value
)])
445 chapters
= info
.get('chapters', [])
447 metadata_filename
= replace_extension(filename
, 'meta')
448 with io
.open(metadata_filename
, 'wt', encoding
='utf-8') as f
:
449 def ffmpeg_escape(text
):
450 return re
.sub(r
'(=|;|#|\\|\n)', r
'\\\1', text
)
452 metadata_file_content
= ';FFMETADATA1\n'
453 for chapter
in chapters
:
454 metadata_file_content
+= '[CHAPTER]\nTIMEBASE=1/1000\n'
455 metadata_file_content
+= 'START=%d\n' % (chapter
['start_time'] * 1000)
456 metadata_file_content
+= 'END=%d\n' % (chapter
['end_time'] * 1000)
457 chapter_title
= chapter
.get('title')
459 metadata_file_content
+= 'title=%s\n' % ffmpeg_escape(chapter_title
)
460 f
.write(metadata_file_content
)
461 in_filenames
.append(metadata_filename
)
462 options
.extend(['-map_metadata', '1'])
464 self
._downloader
.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename
)
465 self
.run_ffmpeg_multiple_files(in_filenames
, temp_filename
, options
)
467 os
.remove(metadata_filename
)
468 os
.remove(encodeFilename(filename
))
469 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
473 class FFmpegMergerPP(FFmpegPostProcessor
):
475 filename
= info
['filepath']
476 temp_filename
= prepend_extension(filename
, 'temp')
477 args
= ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0']
478 self
._downloader
.to_screen('[ffmpeg] Merging formats into "%s"' % filename
)
479 self
.run_ffmpeg_multiple_files(info
['__files_to_merge'], temp_filename
, args
)
480 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
481 return info
['__files_to_merge'], info
484 # TODO: figure out merge-capable ffmpeg version
485 if self
.basename
!= 'avconv':
488 required_version
= '10-0'
489 if is_outdated_version(
490 self
._versions
[self
.basename
], required_version
):
491 warning
= ('Your copy of %s is outdated and unable to properly mux separate video and audio files, '
492 'youtube-dl will download single file media. '
493 'Update %s to version %s or newer to fix this.') % (
494 self
.basename
, self
.basename
, required_version
)
496 self
._downloader
.report_warning(warning
)
501 class FFmpegFixupStretchedPP(FFmpegPostProcessor
):
503 stretched_ratio
= info
.get('stretched_ratio')
504 if stretched_ratio
is None or stretched_ratio
== 1:
507 filename
= info
['filepath']
508 temp_filename
= prepend_extension(filename
, 'temp')
510 options
= ['-c', 'copy', '-aspect', '%f' % stretched_ratio
]
511 self
._downloader
.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename
)
512 self
.run_ffmpeg(filename
, temp_filename
, options
)
514 os
.remove(encodeFilename(filename
))
515 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
520 class FFmpegFixupM4aPP(FFmpegPostProcessor
):
522 if info
.get('container') != 'm4a_dash':
525 filename
= info
['filepath']
526 temp_filename
= prepend_extension(filename
, 'temp')
528 options
= ['-c', 'copy', '-f', 'mp4']
529 self
._downloader
.to_screen('[ffmpeg] Correcting container in "%s"' % filename
)
530 self
.run_ffmpeg(filename
, temp_filename
, options
)
532 os
.remove(encodeFilename(filename
))
533 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
538 class FFmpegFixupM3u8PP(FFmpegPostProcessor
):
540 filename
= info
['filepath']
541 if self
.get_audio_codec(filename
) == 'aac':
542 temp_filename
= prepend_extension(filename
, 'temp')
544 options
= ['-c', 'copy', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
545 self
._downloader
.to_screen('[ffmpeg] Fixing malformed AAC bitstream in "%s"' % filename
)
546 self
.run_ffmpeg(filename
, temp_filename
, options
)
548 os
.remove(encodeFilename(filename
))
549 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
553 class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor
):
554 def __init__(self
, downloader
=None, format
=None):
555 super(FFmpegSubtitlesConvertorPP
, self
).__init
__(downloader
)
559 subs
= info
.get('requested_subtitles')
560 filename
= info
['filepath']
561 new_ext
= self
.format
563 if new_format
== 'vtt':
564 new_format
= 'webvtt'
566 self
._downloader
.to_screen('[ffmpeg] There aren\'t any subtitles to convert')
568 self
._downloader
.to_screen('[ffmpeg] Converting subtitles')
570 for lang
, sub
in subs
.items():
573 self
._downloader
.to_screen(
574 '[ffmpeg] Subtitle file for %s is already in the requested format' % new_ext
)
576 old_file
= subtitles_filename(filename
, lang
, ext
)
577 sub_filenames
.append(old_file
)
578 new_file
= subtitles_filename(filename
, lang
, new_ext
)
580 if ext
in ('dfxp', 'ttml', 'tt'):
581 self
._downloader
.report_warning(
582 'You have requested to convert dfxp (TTML) subtitles into another format, '
583 'which results in style information loss')
586 srt_file
= subtitles_filename(filename
, lang
, 'srt')
588 with open(dfxp_file
, 'rb') as f
:
589 srt_data
= dfxp2srt(f
.read())
591 with io
.open(srt_file
, 'wt', encoding
='utf-8') as f
:
603 sub_filenames
.append(srt_file
)
605 self
.run_ffmpeg(old_file
, new_file
, ['-f', new_format
])
607 with io
.open(new_file
, 'rt', encoding
='utf-8') as f
:
613 return sub_filenames
, info