full cuts: Support multiple ranges

This allows full cuts to support multiple ranges in the same way fast cuts do,
by using multiple inputs to ffmpeg and concat filters joining them.

This will be easy to add transitions to later as this is "just" replacing a concat filter
with an xfade + afade filter.
pull/400/head
Mike Lang 6 months ago committed by Mike Lang
parent cc789caa7e
commit 5fbdaf8422

@ -651,40 +651,71 @@ def feed_input(segments, pipe):
@timed('cut_range', @timed('cut_range',
cut_type=lambda _, segments, start, end, encode_args, stream=False: ("full-streamed" if stream else "full-buffered"), cut_type=lambda _, segment_ranges, ranges, encode_args, stream=False: ("full-streamed" if stream else "full-buffered"),
normalize=lambda _, segments, start, end, *a, **k: (end - start).total_seconds(), normalize=lambda _, segment_ranges, ranges, *a, **k: range_total(ranges),
) )
def full_cut_segments(segments, start, end, encode_args, stream=False): def full_cut_segments(segment_ranges, ranges, encode_args, stream=False):
"""If stream=true, assume encode_args gives a streamable format, """If stream=true, assume encode_args gives a streamable format,
and begin returning output immediately instead of waiting for ffmpeg to finish and begin returning output immediately instead of waiting for ffmpeg to finish
and buffering to disk.""" and buffering to disk."""
# Remove holes # for now, hard-code no transitions
segments = [segment for segment in segments if segment is not None] transitions = [None] * (len(ranges) - 1)
# how far into the first segment to begin inputs = []
cut_start = max(0, (start - segments[0].start).total_seconds()) for segments, (start, end) in zip(segment_ranges, ranges):
# duration # Remove holes
duration = (end - start).total_seconds() segments = [segment for segment in segments if segment is not None]
# how far into the first segment to begin
cut_start = max(0, (start - segments[0].start).total_seconds())
# how long the whole section should be (sets the end cut)
duration = (end - start).total_seconds()
args = [
"-ss", cut_start,
"-t", duration,
]
inputs.append((segments, args))
filters = []
# with no transitions, the output stream is just the first input stream
output_video_stream = "0:v"
output_audio_stream = "0:a"
for i, transition in enumerate(transitions):
# combine the current output stream with the next input stream
input_streams = [
output_video_stream, output_audio_stream,
f"{i+1}:v", f"{i+1}:a"
]
input_streams = "".join(f"[{stream}]" for stream in input_streams)
# set new output streams
output_video_stream = f"v{i}"
output_audio_stream = f"a{i}"
outputs = f"[{output_video_stream}][{output_audio_stream}]"
if transition is None:
filters.append(f"{input_streams}concat=n=2:v=1:a=1{outputs}")
else:
raise NotImplementedError
if stream: if stream:
# When streaming, we can just use a pipe # When streaming, we can just use a pipe
tempfile = subprocess.PIPE output_file = subprocess.PIPE
else: else:
# Some ffmpeg output formats require a seekable file. # Some ffmpeg output formats require a seekable file.
# For the same reason, it's not safe to begin uploading until ffmpeg # For the same reason, it's not safe to begin uploading until ffmpeg
# has finished. We create a temporary file for this. # has finished. We create a temporary file for this.
tempfile = TemporaryFile() output_file = TemporaryFile()
args = [] args = []
if cut_start is not None: if filters:
args += ['-ss', cut_start] args += [
if duration is not None: "-filter_complex", "; ".join(filters),
args += ['-t', duration] "-map", f"[{output_video_stream}]",
args += list(encode_args) "-map", f"[{output_audio_stream}]",
]
with ffmpeg_cut_many([segments, ()], args, output_file=tempfile) as ffmpeg: args += encode_args
# When streaming, we can return data as it is available
with ffmpeg_cut_many(inputs, args, output_file) as ffmpeg:
# When streaming, we can return data as it is available.
# Otherwise, just exit the context manager so tempfile is fully written. # Otherwise, just exit the context manager so tempfile is fully written.
if stream: if stream:
for chunk in read_chunks(ffmpeg.stdout): for chunk in read_chunks(ffmpeg.stdout):
@ -692,7 +723,7 @@ def full_cut_segments(segments, start, end, encode_args, stream=False):
# When not streaming, we can only return the data once ffmpeg has exited # When not streaming, we can only return the data once ffmpeg has exited
if not stream: if not stream:
for chunk in read_chunks(tempfile): for chunk in read_chunks(output_file):
yield chunk yield chunk

@ -418,11 +418,8 @@ class Cutter(object):
"streamable" if upload_backend.encoding_streamable else "non-streamable", "streamable" if upload_backend.encoding_streamable else "non-streamable",
upload_backend.encoding_settings, upload_backend.encoding_settings,
)) ))
if len(job.video_ranges) > 1:
raise ValueError("Full cuts do not support multiple ranges")
range = job.video_ranges[0]
cut = full_cut_segments( cut = full_cut_segments(
job.segment_ranges[0], range.start, range.end, job.segment_ranges, job.video_ranges,
upload_backend.encoding_settings, stream=upload_backend.encoding_streamable, upload_backend.encoding_settings, stream=upload_backend.encoding_streamable,
) )

@ -400,10 +400,7 @@ def cut(channel, quality):
# encode as high-quality, without wasting too much cpu on encoding # encode as high-quality, without wasting too much cpu on encoding
stream, muxer, mimetype = (True, 'mpegts', 'video/MP2T') if type == 'mpegts' else (False, 'mp4', 'video/mp4') stream, muxer, mimetype = (True, 'mpegts', 'video/MP2T') if type == 'mpegts' else (False, 'mp4', 'video/mp4')
encoding_args = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '0', '-f', muxer] encoding_args = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '0', '-f', muxer]
if len(ranges) > 1: return Response(full_cut_segments(segment_ranges, ranges, encoding_args, stream=stream), mimetype=mimetype)
return "full cut does not support multiple ranges at this time", 400
start, end = ranges[0]
return Response(full_cut_segments(segment_ranges[0], start, end, encoding_args, stream=stream), mimetype=mimetype)
else: else:
return "Unknown type {!r}".format(type), 400 return "Unknown type {!r}".format(type), 400

Loading…
Cancel
Save