Add new "smart" cut technique

pull/158/head
Mike Lang 5 years ago
parent bb3814f9f7
commit b516917e62

@ -421,6 +421,114 @@ def fast_cut_segments(segments, start, end):
yield chunk yield chunk
@timed('cut', cut_type='smart', normalize=lambda _, segments, start, end: (end - start).total_seconds())
def smart_cut_segments(segments, start, end):
"""As fast_cut_segments(), but concats segments by using ffmpeg's concat demuxer,
which should make timestamps etc behave correctly."""
# how far into the first segment to begin (if no hole at start)
cut_start = None
if segments[0] is not None:
cut_start = (start - segments[0].start).total_seconds()
if cut_start < 0:
raise ValueError("First segment doesn't begin until after cut start, but no leading hole indicated")
# how far into the final segment to end (if no hole at end)
cut_end = None
if segments[-1] is not None:
cut_end = (end - segments[-1].start).total_seconds()
if cut_end < 0:
raise ValueError("Last segment ends before cut end, but no trailing hole indicated")
# Set first and last only if they actually need cutting.
# Note this handles both the cut_start = None (no first segment to cut)
# and cut_start = 0 (first segment already starts on time) cases.
first = segments[0] if cut_start else None
last = segments[-1] if cut_end else None
# We start up to three ffmpeg processes:
# Two that cut the first and last segments to size
# One that concats the outputs of the first two procs, along with all the segments from disk
# We pass the output pipe of the first two procs directly to the third.
concat_entries = [] # the lines we will pass to the concat demuxer, either 'pipe:FD' or 'file:PATH'
pipes = [] # the pipes referenced in concat_entries
procs = [] # the ffmpeg processes
input_feeder = None
concat_proc = None
try:
for segment in segments:
if segment is None:
logging.debug("Skipping discontinuity while cutting")
# TODO: If we want to be safe against the possibility of codecs changing,
# we should check the streams_info() after each discontinuity.
continue
# note first and last might be the same segment.
# note a segment will only match if cutting actually needs to be done
# (ie. cut_start or cut_end is not 0)
if segment in (first, last):
proc = ffmpeg_cut_segment(
segment,
cut_start if segment == first else None,
cut_end if segment == last else None,
)
procs.append(proc)
pipes.append(proc.stdout)
concat_entries.append('pipe:{}'.format(proc.stdout.fileno()))
else:
# just pass the file directly
concat_entries.append('file:{}'.format(segment.path))
concat_config = ''.join("file '{}'\n".format(entry) for entry in concat_entries)
args = [
'ffmpeg',
'-hide_banner', '-loglevel', 'error', # suppress noisy output
'-f', 'concat', '-', # read concat config from stdin
'-safe', 0, # trust weird filenames
'-protocol_whitelist', 'file,pipe', # need to explicitly allow pipe
'-c', 'copy', # don't re-encode the actual video
'-fflags', '+genpts', # this does something to do with timestamps?
'-',
]
concat_proc = subprocess.Popen(args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
close_fds=False, # don't close non-stdin/out/err before execing, so that pipe: works
)
procs.append(concat_proc)
def write_config():
concat_proc.stdin.write(concat_config)
concat_proc.stdin.close()
input_feeder = gevent.spawn(write_config)
# we can now close our output pipes since ffmpeg will read them directly
for pipe in pipes:
pipe.close()
# now stream results
for chunk in read_chunks(concat_proc.stdout):
yield chunk
# check if any errors occurred in input writing, or if anything exited non-success.
input_feeder.get()
for i, proc in enumerate(procs):
if proc.wait() != 0:
raise Exception("Smart cut ffmpeg process {}/{} exited {}".format(i, len(procs), proc.returncode))
finally:
# if something goes wrong, try to clean up ignoring errors
if input_feeder is not None:
input_feeder.kill()
for proc in procs:
if proc.poll() is not None:
for action in [proc.kill, proc.stdout.close] + [proc.stdin.close if proc is concat_proc else []]:
try:
action()
except (OSError, IOError):
pass
def feed_input(segments, pipe): def feed_input(segments, pipe):
"""Write each segment's data into the given pipe in order. """Write each segment's data into the given pipe in order.
This is used to provide input to ffmpeg in a full cut.""" This is used to provide input to ffmpeg in a full cut."""
@ -443,6 +551,10 @@ def full_cut_segments(segments, start, end, 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
segments = [segment for segment in segments if segment is not None]
# how far into the first segment to begin # how far into the first segment to begin
cut_start = max(0, (start - segments[0].start).total_seconds()) cut_start = max(0, (start - segments[0].start).total_seconds())
# duration # duration

@ -277,8 +277,10 @@ def cut(channel, quality):
type = request.args.get('type', 'fast') type = request.args.get('type', 'fast')
if type == 'rough': if type == 'rough':
return Response(rough_cut_segments(segments, start, end), mimetype='video/MP2T') return Response(rough_cut_segments(segments, start, end), mimetype='video/MP2T')
if type == 'fast': elif type == 'fast':
return Response(fast_cut_segments(segments, start, end), mimetype='video/MP2T') return Response(fast_cut_segments(segments, start, end), mimetype='video/MP2T')
elif type == 'smart':
return Response(smart_cut_segments(segments, start, end), mimetype='video/MP2T')
elif type in ('mpegts', 'mp4'): elif type in ('mpegts', 'mp4'):
if type == 'mp4': if type == 'mp4':
return "mp4 type has been disabled due to the load it causes", 400 return "mp4 type has been disabled due to the load it causes", 400

Loading…
Cancel
Save