Skip to content

Commit

Permalink
Add support for h.264 codec profiles
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
davidplowman committed Jan 31, 2024
1 parent 50e78bf commit e302f34
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 6 deletions.
24 changes: 22 additions & 2 deletions picamera2/encoders/h264_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
29 changes: 25 additions & 4 deletions picamera2/encoders/libav_h264_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit e302f34

Please sign in to comment.