From e302f3490eac31dc891e46b266bb21bd4ff32075 Mon Sep 17 00:00:00 2001 From: David Plowman Date: Wed, 31 Jan 2024 10:06:26 +0000 Subject: [PATCH] Add support for h.264 codec profiles Both hardware and libav h.264 encoders are updated. The hardware will support baseline, constrained baseline, main and high. libav will support, well, what libav supports (which is a much wider selection). libav users can also change the encoder "preset" for higher quality encode when there is sufficient CPU available. Signed-off-by: David Plowman --- picamera2/encoders/h264_encoder.py | 24 ++++++++++++++++++-- picamera2/encoders/libav_h264_encoder.py | 29 ++++++++++++++++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/picamera2/encoders/h264_encoder.py b/picamera2/encoders/h264_encoder.py index 62567b0d..900daca1 100644 --- a/picamera2/encoders/h264_encoder.py +++ b/picamera2/encoders/h264_encoder.py @@ -4,9 +4,14 @@ V4L2_CID_MPEG_VIDEO_H264_LEVEL, V4L2_CID_MPEG_VIDEO_H264_MAX_QP, V4L2_CID_MPEG_VIDEO_H264_MIN_QP, + V4L2_CID_MPEG_VIDEO_H264_PROFILE, V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER, V4L2_MPEG_VIDEO_H264_LEVEL_4_1, - V4L2_MPEG_VIDEO_H264_LEVEL_4_2, V4L2_PIX_FMT_H264) + V4L2_MPEG_VIDEO_H264_LEVEL_4_2, + V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, + V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, + V4L2_MPEG_VIDEO_H264_PROFILE_HIGH, + V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, V4L2_PIX_FMT_H264) from picamera2.encoders import Quality from picamera2.encoders.v4l2_encoder import V4L2Encoder @@ -15,7 +20,8 @@ class H264Encoder(V4L2Encoder): """Uses functionality from V4L2Encoder""" - def __init__(self, bitrate=None, repeat=True, iperiod=None, framerate=None, enable_sps_framerate=False, qp=None): + def __init__(self, bitrate=None, repeat=True, iperiod=None, framerate=None, enable_sps_framerate=False, + qp=None, profile=None): """H264 Encoder :param bitrate: Bitrate, default None @@ -33,6 +39,7 @@ def __init__(self, bitrate=None, repeat=True, iperiod=None, framerate=None, enab self.iperiod = iperiod self.repeat = repeat self.qp = qp + self.profile = profile # The framerate can be reported in the sequence headers if enable_sps_framerate is set, # but there's no guarantee that frames will be delivered to the codec at that rate! self.framerate = framerate @@ -41,6 +48,19 @@ def __init__(self, bitrate=None, repeat=True, iperiod=None, framerate=None, enab def _start(self): self._controls = [] + # These names match what FFmpeg uses. + profile_lookup = {"baseline": V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, + "constrained baseline": V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, + "main": V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, + "high": V4L2_MPEG_VIDEO_H264_PROFILE_HIGH} + if self.profile: + if not isinstance(self.profile, str): + raise RuntimeError("Profile should be a string value") + profile = self.profile.lower() + if profile in profile_lookup: + self._controls += [(V4L2_CID_MPEG_VIDEO_H264_PROFILE, profile_lookup[profile])] + else: + raise RuntimeError("Profile " + self.profile + " not recognised") if self.iperiod is not None: self._controls += [(V4L2_CID_MPEG_VIDEO_H264_I_PERIOD, self.iperiod)] if self.repeat: diff --git a/picamera2/encoders/libav_h264_encoder.py b/picamera2/encoders/libav_h264_encoder.py index 09e0277c..8c7377dd 100644 --- a/picamera2/encoders/libav_h264_encoder.py +++ b/picamera2/encoders/libav_h264_encoder.py @@ -13,7 +13,7 @@ class LibavH264Encoder(Encoder): """Encoder class that uses libx264 for h.264 encoding.""" - def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None): + def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None, profile=None): """Initialise""" super().__init__() self._codec = "h264" # for now only support h264 @@ -22,6 +22,8 @@ def __init__(self, bitrate=None, repeat=True, iperiod=30, framerate=30, qp=None) self.iperiod = iperiod self.framerate = framerate self.qp = qp + self.profile = profile + self.preset = None def _setup(self, quality): # If an explicit quality was specified, use it, otherwise try to preserve any bitrate/qp @@ -45,20 +47,39 @@ def _start(self): self._stream = self._container.add_stream(self._codec, rate=self.framerate) self._stream.codec_context.thread_count = 8 - self._stream.codec_context.thread_type = av.codec.context.ThreadType.FRAME + self._stream.codec_context.thread_type = av.codec.context.ThreadType.FRAME # noqa self._stream.width = self.width self._stream.height = self.height self._stream.pix_fmt = "yuv420p" + preset = "ultrafast" + if self.profile is not None: + if not isinstance(self.profile, str): + raise RuntimeError("Profile should be a string value") + # Much more helpful to compare profile names case insensitively! + available_profiles = {k.lower(): v for k, v in self._stream.codec.profiles.items()} + profile = self.profile.lower() + if profile not in available_profiles: + raise RuntimeError("Profile " + self.profile + " not recognised") + self._stream.codec_context.profile = available_profiles[profile] + # The "ultrafast" preset always produces baseline, so: + if "baseline" not in profile: + preset = "superfast" + if self.bitrate is not None: self._stream.codec_context.bit_rate = self.bitrate self._stream.codec_context.gop_size = self.iperiod - self._stream.codec_context.options["preset"] = "ultrafast" + + # For those who know what they're doing, let them override the "preset". + if self.preset: + preset = self.preset + self._stream.codec_context.options["preset"] = preset + self._stream.codec_context.options["deblock"] = "1" # Absence of the "global header" flags means that SPS/PPS headers get repeated. if not self.repeat: - self._stream.codec_context.flags |= av.codec.context.Flags.GLOBAL_HEADER + self._stream.codec_context.flags |= av.codec.context.Flags.GLOBAL_HEADER # noqa if self.qp is not None: self._stream.codec_context.qmin = self.qp self._stream.codec_context.qmax = self.qp