Skip to content

Commit

Permalink
Make LibavH264Encoder flush frames out at the right rate when necessary
Browse files Browse the repository at this point in the history
This means they'll get passed to FfmpegOutput at the rate that
generates the correct timestamps. We avoid pacing the output like this
when the output classes don't require it.

Currently not doing this for LibavMjpegEncoder because MJPEG files
don't have timestamps.

Signed-off-by: David Plowman <[email protected]>
  • Loading branch information
davidplowman committed Apr 17, 2024
1 parent 38b76ef commit dd34bc9
Show file tree
Hide file tree
Showing 3 changed files with 19 additions and 2 deletions.
18 changes: 16 additions & 2 deletions picamera2/encoders/libav_h264_encoder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""This is a base class for a multi-threaded software encoder."""

import time
from fractions import Fraction
from math import sqrt

Expand All @@ -24,6 +25,8 @@ def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None,
self.qp = qp
self.profile = profile
self.preset = None
self.drop_final_frames = False
self._lasttimestamp = None

def _setup(self, quality):
# If an explicit quality was specified, use it, otherwise try to preserve any bitrate/qp
Expand Down Expand Up @@ -94,8 +97,18 @@ def _start(self):
self._av_input_format = FORMAT_TABLE[self._format]

def _stop(self):
for packet in self._stream.encode():
self.outputframe(bytes(packet), packet.is_keyframe, timestamp=packet.pts)
if not self.drop_final_frames:
# Annoyingly, libav still has lots of encoded frames internally which we must flush
# out. If the output(s) doesn't understand timestamps, we may need to "pace" these
# frames with correct time intervals. Unpleasant.
for packet in self._stream.encode():
if any(out.needs_pacing for out in self._output) and self._lasttimestamp is not None:
time_system, time_packet = self._lasttimestamp
delay_us = packet.pts - time_packet - (time.monotonic_ns() - time_system) / 1000
if delay_us > 0:
time.sleep(delay_us / 1000000)
self._lasttimestamp = (time.monotonic_ns(), packet.pts)
self.outputframe(bytes(packet), packet.is_keyframe, timestamp=packet.pts)
self._container.close()

def _encode(self, stream, request):
Expand All @@ -104,4 +117,5 @@ def _encode(self, stream, request):
frame = av.VideoFrame.from_ndarray(m.array, format=self._av_input_format, width=self.width)
frame.pts = timestamp_us
for packet in self._stream.encode(frame):
self._lasttimestamp = (time.monotonic_ns(), packet.pts)
self.outputframe(bytes(packet), packet.is_keyframe, timestamp=packet.pts)
2 changes: 2 additions & 0 deletions picamera2/outputs/ffmpegoutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def __init__(self, output_filename, audio=False, audio_device="default", audio_s
self.timeout = 1 if audio else None
# A user can set this to get notifications of FFmpeg failures.
self.error_callback = None
# We don't understand timestamps, so an encoder may have to pace output to us.
self.needs_pacing = True

def start(self):
general_options = ['-loglevel', 'warning',
Expand Down
1 change: 1 addition & 0 deletions picamera2/outputs/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def __init__(self, pts=None):
"""
self.recording = False
self.ptsoutput = pts
self.needs_pacing = False

def start(self):
"""Start recording"""
Expand Down

0 comments on commit dd34bc9

Please sign in to comment.