Implement smart cuts

mike/backfill-extras
Mike Lang 2 years ago committed by Mike Lang
parent fa1603e99a
commit 58b4541306

@ -19,6 +19,7 @@ from gevent import subprocess
from .cached_iterator import CachedIterator from .cached_iterator import CachedIterator
from .stats import timed from .stats import timed
from .fixts import FixTS
def unpadded_b64_decode(s): def unpadded_b64_decode(s):
@ -456,9 +457,45 @@ def fast_cut_segments(segment_ranges, ranges):
yield from fast_cut_range(segments, start, end) yield from fast_cut_range(segments, start, end)
class FixTSSequence:
"""Manages state for concatenating several videos while fixing all their timestamps.
Has the same api as FixTS except instead of end(), we have next(), which also
resets the FixTS to take the next input video."""
def __init__(self):
self.fixts = FixTS(0)
def feed(self, data):
return self.fixts.feed(data)
def next(self):
# Note that if FixTS was unused (no data given) this is a no-op.
# In fact it's theoretically safe to call this function as often as you want
# (as long as you're sure you have no partial packets) as the only consequence
# is that we use a fixed time before the next timestamp instead of the timing from
# the original segments.
t = self.fixts.end()
self.fixts = FixTS(t)
@timed('cut', cut_type='smart', normalize=lambda ret, sr, ranges: range_total(ranges))
def smart_cut_segments(segment_ranges, ranges):
"""
As per fast_cut_segments(), except we also do a "fix" pass over the resulting video stream
to re-time internal timestamps to avoid discontinuities and make sure the video starts at t=0.
"""
if len(segment_ranges) != len(ranges):
raise ValueError("You need to provide one segment list for each range")
fixts = FixTSSequence()
for segments, (start, end) in zip(segment_ranges, ranges):
yield from fast_cut_range(segments, start, end, fixts=fixts)
@timed('cut_range', cut_type='fast', normalize=lambda _, segments, start, end: (end - start).total_seconds()) @timed('cut_range', cut_type='fast', normalize=lambda _, segments, start, end: (end - start).total_seconds())
def fast_cut_range(segments, start, end): def fast_cut_range(segments, start, end, fixts=None):
"""Does a fast cut for an individual range of segments""" """Does a fast cut for an individual range of segments.
If a FixTSSequence is given, fixes timestamps to avoid discontinuities
between cut segments and passed through segments.
"""
# how far into the first segment to begin (if no hole at start) # how far into the first segment to begin (if no hole at start)
cut_start = None cut_start = None
@ -485,12 +522,19 @@ def fast_cut_range(segments, start, end):
logging.debug("Skipping discontinuity while cutting") logging.debug("Skipping discontinuity while cutting")
# TODO: If we want to be safe against the possibility of codecs changing, # TODO: If we want to be safe against the possibility of codecs changing,
# we should check the streams_info() after each discontinuity. # we should check the streams_info() after each discontinuity.
# To keep our output clean, we reset our FixTS so the output doesn't contain
# the discontinuity. The video just cuts to the next segment.
if fixts:
fixts.next()
continue continue
# note first and last might be the same segment. # note first and last might be the same segment.
# note a segment will only match if cutting actually needs to be done # note a segment will only match if cutting actually needs to be done
# (ie. cut_start or cut_end is not 0) # (ie. cut_start or cut_end is not 0)
if segment in (first, last): if segment in (first, last):
if fixts:
fixts.next()
proc = None proc = None
try: try:
proc = ffmpeg_cut_segment( proc = ffmpeg_cut_segment(
@ -500,7 +544,7 @@ def fast_cut_range(segments, start, end):
) )
with closing(proc.stdout): with closing(proc.stdout):
for chunk in read_chunks(proc.stdout): for chunk in read_chunks(proc.stdout):
yield chunk yield fixts.feed(chunk) if fixts else chunk
proc.wait() proc.wait()
except Exception as ex: except Exception as ex:
# try to clean up proc, ignoring errors # try to clean up proc, ignoring errors
@ -516,11 +560,16 @@ def fast_cut_range(segments, start, end):
raise Exception( raise Exception(
"Error while streaming cut: ffmpeg exited {}".format(proc.returncode) "Error while streaming cut: ffmpeg exited {}".format(proc.returncode)
) )
if fixts:
fixts.next()
else: else:
# no cutting needed, just serve the file # no cutting needed, just serve the file
with open(segment.path, 'rb') as f: with open(segment.path, 'rb') as f:
for chunk in read_chunks(f): for chunk in read_chunks(f):
yield chunk yield fixts.feed(chunk) if fixts else chunk
if fixts:
# check for errors and indicate range is finished
fixts.next()
def feed_input(segments, pipe): def feed_input(segments, pipe):

Loading…
Cancel
Save