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