change how timestamps work again, so PCR and PTS are *both* set to start time

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

@ -41,12 +41,10 @@ class FixTS():
# So this is always a safe "latest" value to start at, and it means that if # So this is always a safe "latest" value to start at, and it means that if
# the video is ended with no PCR frames, we default to "same as start time". # the video is ended with no PCR frames, we default to "same as start time".
self.end_time = start_time self.end_time = start_time
# once starting PCR is known, contains value to add to each timestamp # once starting PCR/PTS is known, contains value to add to each timestamp
self.offset = None self.offsets = {"pcr": None, "pts": None}
# buffers fed data until a whole packet can be parsed # buffers fed data until a whole packet can be parsed
self.data = b"" self.data = b""
# buffers packets until first PCR-containing packet
self.pending_packets = []
def feed(self, data): def feed(self, data):
"""Takes more data as a bytestring to add to buffer. """Takes more data as a bytestring to add to buffer.
@ -56,32 +54,8 @@ class FixTS():
while len(self.data) >= self.PACKET_SIZE: while len(self.data) >= self.PACKET_SIZE:
packet = self.data[:self.PACKET_SIZE] packet = self.data[:self.PACKET_SIZE]
self.data = self.data[self.PACKET_SIZE:] self.data = self.data[self.PACKET_SIZE:]
try:
fixed_packet = self._fix_packet(packet) fixed_packet = self._fix_packet(packet)
except OffsetNotReady:
# We can't fix this packet yet, so buffer it for now.
# Note this will also cause any further packets to "queue up" behind it
# (see below).
self.pending_packets.append(packet)
continue
if self.pending_packets and self.offset is not None:
# Offset has been found and we can process all pending packets.
# Note we do this before outputting our new packet to preserve order.
for prev_packet in self.pending_packets:
output.append(self._fix_packet(prev_packet))
self.pending_packets = []
output.append(fixed_packet)
elif self.pending_packets:
# If we have pending packets, we can't output any further packets until after them.
# So we add them to the pending queue. This means re-fixing them later but that's cheap.
self.pending_packets.append(packet)
else:
# Normal case, output them as we fix them. This covers both "offset is known"
# and "offset isn't known, but no packets so far have needed fixing".
output.append(fixed_packet) output.append(fixed_packet)
return b''.join(output) return b''.join(output)
def end(self): def end(self):
@ -90,29 +64,30 @@ class FixTS():
""" """
if len(self.data) > 0: if len(self.data) > 0:
raise ValueError("Stream has a partial packet remaining: {!r}", self.data) raise ValueError("Stream has a partial packet remaining: {!r}", self.data)
if self.pending_packets:
raise ValueError("Stream contained PTS packets but no PCR")
return self.end_time return self.end_time
# We only use PCR to calibrate the offset (ie. we want the first PCR
# to be = start_time, not the first PTS we see which might be the audio stream). # PCRs (which represent the "time when encoded") can vary greatly between
# If we encounter a PTS before a PCR, we throw OffsetNotReady which is handled # encoded videos, sometimes with large offsets between the PCR and the PTS.
# by feed(). # For example, a video might actually start at t=1, when the PCR starts at t=0.
def _convert_time(self, old_time, is_pcr=False): # During playback of such a video, the player would start at t=1. We want to effectively do the same,
# If this is the first PCR we've seen, use it to calibrate offset. # and have the first actual content of the video start at the user's requested time.
if self.offset is None: # So we want both the first PCR and the first PTS to be equal to start_time,
if is_pcr: # with no difference between them. The easiest way to do this is to track their offsets independently.
self.offset = self.start_time - old_time def _convert_time(self, old_time, kind):
else: # If this is the first one we've seen, use it to calibrate offset.
raise OffsetNotReady if self.offsets[kind] is None:
new_time = old_time + self.offset self.offsets[kind] = self.start_time - old_time
# It's rare but possible that when resetting times to start at 0, the second packet new_time = old_time + self.offsets[kind]
# might start slightly earlier than the first and thus have a negative time. # It's possible that the second packet might have a PTS slightly earlier than the first
# This isn't encodable in the data format, so just clamp to 0. # (eg. one is audio and one is video, and their start times slightly differ). We clamp
new_time = max(0, new_time) # the value so that it never goes earlier than the user's requested start time. This may
# Keep track of the nominal "end time" based on latest PCR time. # cause some minor artifacting on the first packet of a stream, but saves us from invalid
# PCR packets *should* be in order but use max just in case. # packets which cause further issues.
if is_pcr: new_time = max(self.start_time, new_time)
# Keep track of the nominal "end time" based on latest PCR or PTS time.
# This can be thought of as finding the "video length" as max(latest ts - first ts) for any kind.
# then adding the requested start time to it to get the end time.
new_end = new_time + self.NOMINAL_PCR_INTERVAL new_end = new_time + self.NOMINAL_PCR_INTERVAL
self.end_time = max(self.end_time, new_end) self.end_time = max(self.end_time, new_end)
return new_time return new_time
@ -163,7 +138,7 @@ class FixTS():
if has_pcr: if has_pcr:
check(field_length >= 7, "Adaptation field indicates PCR but is too small") check(field_length >= 7, "Adaptation field indicates PCR but is too small")
old_time = decode_pcr(packet[6:12]) old_time = decode_pcr(packet[6:12])
new_time = self._convert_time(old_time, is_pcr=True) new_time = self._convert_time(old_time, 'pcr')
encoded = encode_pcr(new_time) encoded = encode_pcr(new_time)
packet = packet[:6] + encoded + packet[12:] packet = packet[:6] + encoded + packet[12:]
assert len(packet) == 188 assert len(packet) == 188
@ -199,7 +174,7 @@ class FixTS():
flags = packet[unit_index + 6] flags = packet[unit_index + 6]
has_pts = bool(flags & 0x80) has_pts = bool(flags & 0x80)
has_dts = bool(flags & 0x40) has_dts = bool(flags & 0x40)
check(not has_dts, "DTS timestamp is present, we cannot handle fixing it") check(not has_dts, "DTS timestamp is present, we cannot fix DTS as it may cause packets to be before start_time")
# Once again, PTS is the first optional field, so we don't need to worry # Once again, PTS is the first optional field, so we don't need to worry
# about other fields being present. # about other fields being present.
if has_pts: if has_pts:
@ -207,15 +182,12 @@ class FixTS():
check(pts_index + 5 <= self.PACKET_SIZE, "Payload too small to read PTS") check(pts_index + 5 <= self.PACKET_SIZE, "Payload too small to read PTS")
raw = packet[pts_index : pts_index + 5] raw = packet[pts_index : pts_index + 5]
pts = decode_ts(raw, 2) pts = decode_ts(raw, 2)
pts = self._convert_time(pts) pts = self._convert_time(pts, 'pts')
encoded = encode_ts(pts, 2) encoded = encode_ts(pts, 2)
packet = packet[:pts_index] + encoded + packet[pts_index + 5:] packet = packet[:pts_index] + encoded + packet[pts_index + 5:]
assert len(packet) == 188 assert len(packet) == 188
return packet
return packet
class OffsetNotReady(Exception):
pass
def bits(value, start, end): def bits(value, start, end):

@ -365,6 +365,10 @@ def ffmpeg_cut_segment(segment, cut_start=None, cut_end=None):
args += ['-ss', str(cut_start)] args += ['-ss', str(cut_start)]
if cut_end: if cut_end:
args += ['-to', str(cut_end)] args += ['-to', str(cut_end)]
# disable B-frames (frames which contain data needed by earlier frames) as a codec option,
# as it changes the order that frames go in the file, which messes with our "concatenate the
# packets" method of concatenating the video.
args += ['-bf', '0']
# output to stdout as MPEG-TS # output to stdout as MPEG-TS
args += ['-f', 'mpegts', '-'] args += ['-f', 'mpegts', '-']
# run it # run it

Loading…
Cancel
Save