cros-codecs 0.0.6

Hardware-accelerated codecs for Linux
Documentation
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use std::sync::Arc;

use v4l2r::controls::codec::VideoGopSize;
use v4l2r::controls::codec::VideoH264IPeriod;
use v4l2r::controls::codec::VideoH264Level;
use v4l2r::controls::codec::VideoH264MaxQp;
use v4l2r::controls::codec::VideoH264MinQp;
use v4l2r::controls::codec::VideoH264Profile;
use v4l2r::controls::codec::VideoPrependSpsPpsToIdr;
use v4l2r::device::Device;

use crate::backend::v4l2::encoder::CaptureBuffers;
use crate::backend::v4l2::encoder::ControlError;
use crate::backend::v4l2::encoder::EncoderCodec;
use crate::backend::v4l2::encoder::InitializationError;
use crate::backend::v4l2::encoder::OutputBufferHandle;
use crate::backend::v4l2::encoder::V4L2Backend;
use crate::codec::h264::parser::Level;
use crate::codec::h264::parser::Profile;
use crate::encoder::h264::EncoderConfig;
use crate::encoder::h264::H264;
use crate::encoder::stateful::StatefulEncoder;
use crate::encoder::PredictionStructure;
use crate::encoder::Tunings;
use crate::Fourcc;
use crate::Resolution;

const PIXEL_FORMAT_H264: v4l2r::PixelFormat = v4l2r::PixelFormat::from_fourcc(b"H264");

pub type V4L2H264Backend<Handle, CaptureBufferz> = V4L2Backend<Handle, CaptureBufferz, H264>;

pub type V4L2StatefulH264Encoder<Handle, CaptureBufferz> =
    StatefulEncoder<Handle, V4L2H264Backend<Handle, CaptureBufferz>>;

impl From<Level> for VideoH264Level {
    fn from(value: Level) -> Self {
        match value {
            Level::L1 => VideoH264Level::L1_0,
            Level::L1B => VideoH264Level::L1B,
            Level::L1_1 => VideoH264Level::L1_1,
            Level::L1_2 => VideoH264Level::L1_2,
            Level::L1_3 => VideoH264Level::L1_3,
            Level::L2_0 => VideoH264Level::L2_0,
            Level::L2_1 => VideoH264Level::L2_1,
            Level::L2_2 => VideoH264Level::L2_2,
            Level::L3 => VideoH264Level::L3_0,
            Level::L3_1 => VideoH264Level::L3_1,
            Level::L3_2 => VideoH264Level::L3_2,
            Level::L4 => VideoH264Level::L4_0,
            Level::L4_1 => VideoH264Level::L4_1,
            Level::L4_2 => VideoH264Level::L4_2,
            Level::L5 => VideoH264Level::L5_0,
            Level::L5_1 => VideoH264Level::L5_1,
            Level::L5_2 => VideoH264Level::L5_2,
            Level::L6 => VideoH264Level::L6_0,
            Level::L6_1 => VideoH264Level::L6_1,
            Level::L6_2 => VideoH264Level::L6_2,
        }
    }
}

impl From<Profile> for VideoH264Profile {
    fn from(value: Profile) -> Self {
        match value {
            Profile::Baseline => Self::Baseline,
            Profile::Main => Self::Main,
            Profile::Extended => Self::Extended,
            Profile::High => Self::High,
            Profile::High10 => Self::High10,
            Profile::High422P => Self::High422,
        }
    }
}

impl<Handle, CaptureBufferz> EncoderCodec for V4L2H264Backend<Handle, CaptureBufferz>
where
    Handle: OutputBufferHandle,
    CaptureBufferz: CaptureBuffers,
{
    fn apply_tunings(device: &Device, tunings: &Tunings) -> Result<(), ControlError> {
        let min_qp = VideoH264MinQp(tunings.min_quality.clamp(1, 51) as i32);
        Self::apply_ctrl(device, "h264 min qp", min_qp)?;

        let max_qp = VideoH264MaxQp(tunings.max_quality.clamp(1, 51) as i32);
        Self::apply_ctrl(device, "h264 max qp", max_qp)?;

        Ok(())
    }
}

impl<Handle, CaptureBufferz> V4L2H264Backend<Handle, CaptureBufferz>
where
    Handle: OutputBufferHandle,
    CaptureBufferz: CaptureBuffers,
{
    pub fn new(
        device: Arc<Device>,
        capture_buffers: CaptureBufferz,
        config: EncoderConfig,
        fourcc: Fourcc,
        coded_size: Resolution,
        tunings: Tunings,
    ) -> Result<Self, InitializationError> {
        match config.pred_structure {
            PredictionStructure::LowDelay { limit } => {
                let limit = limit as i32;

                Self::apply_ctrl(&device, "gop size", VideoGopSize(limit))?;
                Self::apply_ctrl(&device, "h264 i period", VideoH264IPeriod(limit))?;
            }
        }

        Self::apply_ctrl(&device, "prepend SPS PPS to IDR", VideoPrependSpsPpsToIdr(true))?;
        Self::apply_ctrl(&device, "h264 profile", VideoH264Profile::from(config.profile))?;
        Self::apply_ctrl(&device, "h264 level", VideoH264Level::from(config.level))?;

        Self::create(
            device,
            capture_buffers,
            fourcc,
            coded_size,
            config.resolution,
            PIXEL_FORMAT_H264,
            tunings,
        )
    }
}

impl<Handle, CaptureBufferz> V4L2StatefulH264Encoder<Handle, CaptureBufferz>
where
    Handle: OutputBufferHandle,
    CaptureBufferz: CaptureBuffers,
{
    pub fn new(
        device: Arc<Device>,
        capture_buffers: CaptureBufferz,
        config: EncoderConfig,
        fourcc: Fourcc,
        coded_size: Resolution,
        tunings: Tunings,
    ) -> Result<Self, InitializationError> {
        Ok(Self::create(
            tunings.clone(),
            V4L2H264Backend::new(device, capture_buffers, config, fourcc, coded_size, tunings)?,
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::path::PathBuf;
    use std::sync::Arc;

    use v4l2r::device::Device;
    use v4l2r::device::DeviceConfig;

    use crate::backend::v4l2::encoder::find_device_with_capture;
    use crate::backend::v4l2::encoder::tests::perform_v4l2_encoder_dmabuf_test;
    use crate::backend::v4l2::encoder::tests::perform_v4l2_encoder_mmap_test;
    use crate::backend::v4l2::encoder::tests::BoPoolAllocator;
    use crate::backend::v4l2::encoder::tests::GbmDevice;
    use crate::backend::v4l2::encoder::v4l2_format_to_frame_layout;
    use crate::backend::v4l2::encoder::MmapingCapture;
    use crate::encoder::simple_encode_loop;
    use crate::encoder::tests::userptr_test_frame_generator;
    use crate::encoder::RateControl;
    use crate::utils::DmabufFrame;
    use crate::Resolution;

    #[ignore]
    // Ignore this test by default as it requires v4l2m2m-compatible hardware.
    #[test]
    fn test_v4l2_encoder_userptr() {
        const VISIBLE_SIZE: Resolution = Resolution { width: 500, height: 500 };
        const CODED_SIZE: Resolution = Resolution { width: 512, height: 512 };
        const FRAME_COUNT: u64 = 100;

        let _ = env_logger::try_init();

        let device = find_device_with_capture(PIXEL_FORMAT_H264).expect("no H264 encoder found");
        let device = Device::open(&device, DeviceConfig::new().non_blocking_dqbuf()).expect("open");
        let device = Arc::new(device);

        let mut encoder = V4L2StatefulH264Encoder::new(
            device,
            MmapingCapture,
            EncoderConfig { resolution: VISIBLE_SIZE, ..Default::default() },
            Fourcc::from(b"NM12"),
            CODED_SIZE,
            Tunings { rate_control: RateControl::ConstantBitrate(400_000), ..Default::default() },
        )
        .unwrap();

        let format: v4l2r::Format = encoder.backend().output_format().unwrap();
        let layout = v4l2_format_to_frame_layout(&format);

        let mut bitstream = Vec::new();
        let buffer_size =
            format.plane_fmt.iter().map(|plane| plane.sizeimage).max().unwrap() as usize;
        let mut frame_producer = userptr_test_frame_generator(FRAME_COUNT, layout, buffer_size);

        simple_encode_loop(&mut encoder, &mut frame_producer, |coded| {
            bitstream.extend(coded.bitstream)
        })
        .expect("encode loop");

        let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true");
        if write_to_file {
            use std::io::Write;
            let mut out = std::fs::File::create("test_v4l2_encoder_userptr.h264").unwrap();
            out.write_all(&bitstream).unwrap();
            out.flush().unwrap();
        }
    }

    #[ignore]
    // Ignore this test by default as it requires v4l2m2m-compatible hardware.
    #[test]
    fn test_v4l2_encoder_mmap() {
        const VISIBLE_SIZE: Resolution = Resolution { width: 500, height: 500 };
        const CODED_SIZE: Resolution = Resolution { width: 512, height: 512 };
        const FRAME_COUNT: u64 = 100;

        let _ = env_logger::try_init();

        let device = find_device_with_capture(PIXEL_FORMAT_H264).expect("no H264 encoder found");
        let device = Device::open(&device, DeviceConfig::new().non_blocking_dqbuf()).expect("open");
        let device = Arc::new(device);

        let encoder = V4L2StatefulH264Encoder::new(
            device,
            MmapingCapture,
            EncoderConfig { resolution: VISIBLE_SIZE, ..Default::default() },
            Fourcc::from(b"NM12"),
            CODED_SIZE,
            Tunings { rate_control: RateControl::ConstantBitrate(400_000), ..Default::default() },
        )
        .unwrap();

        let mut bitstream = Vec::new();
        perform_v4l2_encoder_mmap_test(FRAME_COUNT, encoder, |coded| {
            bitstream.extend(coded.bitstream)
        });

        let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true");
        if write_to_file {
            use std::io::Write;
            let mut out = std::fs::File::create("test_v4l2_encoder_mmap.h264").unwrap();
            out.write_all(&bitstream).unwrap();
            out.flush().unwrap();
        }
    }

    #[ignore]
    // Ignore this test by default as it requires v4l2m2m-compatible hardware.
    #[test]
    fn test_v4l2_encoder_dmabuf() {
        const VISIBLE_SIZE: Resolution = Resolution { width: 500, height: 500 };
        const CODED_SIZE: Resolution = Resolution { width: 512, height: 512 };
        const FRAME_COUNT: u64 = 100;

        let _ = env_logger::try_init();

        let device = find_device_with_capture(PIXEL_FORMAT_H264).expect("no H264 encoder found");
        let device = Device::open(&device, DeviceConfig::new().non_blocking_dqbuf()).expect("open");
        let device = Arc::new(device);

        let gbm = GbmDevice::open(PathBuf::from("/dev/dri/renderD128"))
            .and_then(gbm::Device::new)
            .expect("failed to create GBM device");

        let gbm = Arc::new(gbm);

        let encoder = V4L2StatefulH264Encoder::<DmabufFrame, _>::new(
            device.clone(),
            BoPoolAllocator::new(gbm.clone()),
            EncoderConfig { resolution: VISIBLE_SIZE, ..Default::default() },
            Fourcc::from(b"NV12"),
            CODED_SIZE,
            Tunings {
                framerate: 30,
                rate_control: RateControl::ConstantBitrate(400_000),
                ..Default::default()
            },
        )
        .unwrap();

        let mut bitstream = Vec::new();

        perform_v4l2_encoder_dmabuf_test(
            CODED_SIZE,
            VISIBLE_SIZE,
            FRAME_COUNT,
            gbm,
            encoder,
            |coded| bitstream.extend(coded.bitstream),
        );

        let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true");
        if write_to_file {
            use std::io::Write;
            let mut out = std::fs::File::create("test_v4l2_encoder_dmabuf.h264").unwrap();
            out.write_all(&bitstream).unwrap();
            out.flush().unwrap();
        }
    }
}