From 58b4541306dbae11c3ce2579c99e80bff649aba9 Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Mon, 22 May 2023 17:04:32 +1000 Subject: [PATCH] Implement smart cuts --- common/common/segments.py | 57 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/common/common/segments.py b/common/common/segments.py index fd4e565..a8f9ed8 100644 --- a/common/common/segments.py +++ b/common/common/segments.py @@ -19,6 +19,7 @@ from gevent import subprocess from .cached_iterator import CachedIterator from .stats import timed +from .fixts import FixTS def unpadded_b64_decode(s): @@ -456,9 +457,45 @@ def fast_cut_segments(segment_ranges, ranges): 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()) -def fast_cut_range(segments, start, end): - """Does a fast cut for an individual range of segments""" +def fast_cut_range(segments, start, end, fixts=None): + """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) cut_start = None @@ -485,12 +522,19 @@ def fast_cut_range(segments, start, end): 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. + + # 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 # 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): + if fixts: + fixts.next() proc = None try: proc = ffmpeg_cut_segment( @@ -500,7 +544,7 @@ def fast_cut_range(segments, start, end): ) with closing(proc.stdout): for chunk in read_chunks(proc.stdout): - yield chunk + yield fixts.feed(chunk) if fixts else chunk proc.wait() except Exception as ex: # try to clean up proc, ignoring errors @@ -516,11 +560,16 @@ def fast_cut_range(segments, start, end): raise Exception( "Error while streaming cut: ffmpeg exited {}".format(proc.returncode) ) + if fixts: + fixts.next() else: # no cutting needed, just serve the file with open(segment.path, 'rb') as 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):