Support arbitrary stream merges

With this change, the merge operator may join any number of media streams,
video or audio. The streams are downloaded in the order specified.

Also, fix the metadata post-processor so that it doesn't leave out
any streams.
This commit is contained in:
felix 2015-08-04 09:07:44 +02:00
parent de7c27cd25
commit d03cfdce1b
2 changed files with 76 additions and 53 deletions

View File

@ -1216,11 +1216,13 @@ def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, ins
group = _parse_format_selection(tokens, inside_group=True) group = _parse_format_selection(tokens, inside_group=True)
current_selector = FormatSelector(GROUP, group, []) current_selector = FormatSelector(GROUP, group, [])
elif string == '+': elif string == '+':
video_selector = current_selector if not current_selector:
audio_selector = _parse_format_selection(tokens, inside_merge=True) raise syntax_error('Unexpected "+"', start)
if not video_selector or not audio_selector: selector_1 = current_selector
raise syntax_error('"+" must be between two format selectors', start) selector_2 = _parse_format_selection(tokens, inside_merge=True)
current_selector = FormatSelector(MERGE, (video_selector, audio_selector), []) if not selector_2:
raise syntax_error('Expected a selector', start)
current_selector = FormatSelector(MERGE, (selector_1, selector_2), [])
else: else:
raise syntax_error('Operator not recognized: "{0}"'.format(string), start) raise syntax_error('Operator not recognized: "{0}"'.format(string), start)
elif type == tokenize.ENDMARKER: elif type == tokenize.ENDMARKER:
@ -1305,47 +1307,59 @@ def selector_function(ctx):
if matches: if matches:
yield matches[-1] yield matches[-1]
elif selector.type == MERGE: elif selector.type == MERGE:
def _merge(formats_info): def _merge(formats_pair):
format_1, format_2 = [f['format_id'] for f in formats_info] format_1, format_2 = formats_pair
# The first format must contain the video and the
# second the audio formats_info = []
if formats_info[0].get('vcodec') == 'none': formats_info.extend(format_1.get('requested_formats', (format_1,)))
self.report_error('The first format must ' formats_info.extend(format_2.get('requested_formats', (format_2,)))
'contain the video, try using '
'"-f %s+%s"' % (format_2, format_1)) video_fmts = [fmt_info for fmt_info in formats_info if fmt_info.get('vcodec') != 'none']
return audio_fmts = [fmt_info for fmt_info in formats_info if fmt_info.get('acodec') != 'none']
# Formats must be opposite (video+audio)
if formats_info[0].get('acodec') == 'none' and formats_info[1].get('acodec') == 'none': the_only_video = video_fmts[0] if len(video_fmts) == 1 else None
self.report_error( the_only_audio = audio_fmts[0] if len(audio_fmts) == 1 else None
'Both formats %s and %s are video-only, you must specify "-f video+audio"'
% (format_1, format_2)) output_ext = self.params.get('merge_output_format')
return if not output_ext:
output_ext = ( if the_only_video:
formats_info[0]['ext'] output_ext = the_only_video['ext']
if self.params.get('merge_output_format') is None elif the_only_audio and not video_fmts:
else self.params['merge_output_format']) output_ext = the_only_audio['ext']
return { else:
output_ext = 'mkv'
new_dict = {
'requested_formats': formats_info, 'requested_formats': formats_info,
'format': '%s+%s' % (formats_info[0].get('format'), 'format': '+'.join(fmt_info.get('format') for fmt_info in formats_info),
formats_info[1].get('format')), 'format_id': '+'.join(fmt_info.get('format_id') for fmt_info in formats_info),
'format_id': '%s+%s' % (formats_info[0].get('format_id'),
formats_info[1].get('format_id')),
'width': formats_info[0].get('width'),
'height': formats_info[0].get('height'),
'resolution': formats_info[0].get('resolution'),
'fps': formats_info[0].get('fps'),
'vcodec': formats_info[0].get('vcodec'),
'vbr': formats_info[0].get('vbr'),
'stretched_ratio': formats_info[0].get('stretched_ratio'),
'acodec': formats_info[1].get('acodec'),
'abr': formats_info[1].get('abr'),
'ext': output_ext, 'ext': output_ext,
} }
video_selector, audio_selector = map(_build_selector_function, selector.selector)
if the_only_video:
new_dict.update({
'width': the_only_video.get('width'),
'height': the_only_video.get('height'),
'resolution': the_only_video.get('resolution'),
'fps': the_only_video.get('fps'),
'vcodec': the_only_video.get('vcodec'),
'vbr': the_only_video.get('vbr'),
'stretched_ratio': the_only_video.get('stretched_ratio'),
})
if the_only_audio:
new_dict.update({
'acodec': the_only_audio.get('acodec'),
'abr': the_only_audio.get('abr'),
})
return new_dict
selector_1, selector_2 = map(_build_selector_function, selector.selector)
def selector_function(ctx): def selector_function(ctx):
for pair in itertools.product( for pair in itertools.product(
video_selector(copy.deepcopy(ctx)), audio_selector(copy.deepcopy(ctx))): selector_1(copy.deepcopy(ctx)), selector_2(copy.deepcopy(ctx))):
yield _merge(pair) yield _merge(pair)
filters = [self._build_format_filter(f) for f in selector.filters] filters = [self._build_format_filter(f) for f in selector.filters]
@ -1875,17 +1889,21 @@ def dl(name, info):
postprocessors = [merger] postprocessors = [merger]
def compatible_formats(formats): def compatible_formats(formats):
video, audio = formats # TODO: some formats actually allow this (mkv, webm, ogg, mp4), but not all of them.
video_formats = [format for format in formats if format.get('vcodec') != 'none']
audio_formats = [format for format in formats if format.get('acodec') != 'none']
if len(video_formats) > 2 or len(audio_formats) > 2:
return False
# Check extension # Check extension
video_ext, audio_ext = video.get('ext'), audio.get('ext') exts = set(format.get('ext') for format in formats)
if video_ext and audio_ext: COMPATIBLE_EXTS = (
COMPATIBLE_EXTS = ( set(('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma')),
('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma'), set(('webm',)),
('webm') )
) for ext_sets in COMPATIBLE_EXTS:
for exts in COMPATIBLE_EXTS: if ext_sets.issuperset(exts):
if video_ext in exts and audio_ext in exts: return True
return True
# TODO: Check acodec/vcodec # TODO: Check acodec/vcodec
return False return False
@ -2064,7 +2082,7 @@ def post_process(self, filename, ie_info):
except PostProcessingError as e: except PostProcessingError as e:
self.report_error(e.msg) self.report_error(e.msg)
if files_to_delete and not self.params.get('keepvideo', False): if files_to_delete and not self.params.get('keepvideo', False):
for old_filename in files_to_delete: for old_filename in set(files_to_delete):
self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename) self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
try: try:
os.remove(encodeFilename(old_filename)) os.remove(encodeFilename(old_filename))

View File

@ -476,7 +476,7 @@ def add(meta_list, info_list=None):
filename = info['filepath'] filename = info['filepath']
temp_filename = prepend_extension(filename, 'temp') temp_filename = prepend_extension(filename, 'temp')
in_filenames = [filename] in_filenames = [filename]
options = [] options = ['-map', '0']
if info['ext'] == 'm4a': if info['ext'] == 'm4a':
options.extend(['-vn', '-acodec', 'copy']) options.extend(['-vn', '-acodec', 'copy'])
@ -518,7 +518,12 @@ class FFmpegMergerPP(FFmpegPostProcessor):
def run(self, info): def run(self, info):
filename = info['filepath'] filename = info['filepath']
temp_filename = prepend_extension(filename, 'temp') temp_filename = prepend_extension(filename, 'temp')
args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0'] args = ['-c', 'copy']
for (i, fmt) in enumerate(info['requested_formats']):
if fmt.get('acodec') != 'none':
args.extend(['-map', '%u:a:0' % (i)])
if fmt.get('vcodec') != 'none':
args.extend(['-map', '%u:v:0' % (i)])
self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename) self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename)
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args) self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args)
os.rename(encodeFilename(temp_filename), encodeFilename(filename)) os.rename(encodeFilename(temp_filename), encodeFilename(filename))