blob: e261a0ca769cfdadc99151f1942d71bbb9dbc6e2 [file] [log] [blame] [edit]
// 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::fs::File;
use std::io::Write;
use std::os::unix::prelude::FileExt;
use std::sync::Arc;
use cros_codecs::backend::v4l2::encoder::find_device_with_capture;
use cros_codecs::backend::v4l2::encoder::v4l2_format_to_frame_layout;
use cros_codecs::backend::v4l2::encoder::EncoderCodec;
use cros_codecs::backend::v4l2::encoder::MmapingCapture;
use cros_codecs::backend::v4l2::encoder::OutputBuffer;
use cros_codecs::backend::v4l2::encoder::OutputBufferHandle;
use cros_codecs::backend::v4l2::encoder::V4L2Backend;
use cros_codecs::bitstream_utils::IvfFileHeader;
use cros_codecs::bitstream_utils::IvfFrameHeader;
use cros_codecs::encoder::simple_encode_loop;
use cros_codecs::encoder::stateful::h264::v4l2::V4L2StatefulH264Encoder;
use cros_codecs::encoder::stateful::h265::v4l2::V4L2StatefulH265Encoder;
use cros_codecs::encoder::stateful::vp8::v4l2::V4L2StatefulVP8Encoder;
use cros_codecs::encoder::stateful::vp9::v4l2::V4L2StatefulVP9Encoder;
use cros_codecs::encoder::stateful::StatefulEncoder;
use cros_codecs::encoder::CodedBitstreamBuffer;
use cros_codecs::encoder::FrameMetadata;
use cros_codecs::encoder::RateControl;
use cros_codecs::encoder::Tunings;
use cros_codecs::image_processing::extend_border_nv12;
use cros_codecs::image_processing::i420_to_nv12_chroma;
use cros_codecs::image_processing::nv12_copy;
use cros_codecs::DecodedFormat;
use cros_codecs::Fourcc;
use cros_codecs::FrameLayout;
use cros_codecs::Resolution;
use v4l2r::device::Device;
use v4l2r::device::DeviceConfig;
use v4l2r::memory::MmapHandle;
use crate::util::Args;
use crate::util::Codec;
// "Handle" abstraction for this particular use case. All Encoders take a "Handle" generic type
// that implements the OutputBufferHandle trait, which basically just tells the Encoder how the
// frame data is going to be loaded into the V4L2 output buffers. This is where we add the code to
// load the frames from the disk. Technically we could do the disk load in DiskFrameReader and
// then pass regular u8 buffers to |queue()|, but this way avoids a copy.
struct MmapNM12Frame<'a> {
resolution: Resolution,
file: &'a File,
input_fourcc: DecodedFormat,
pos: u64,
input_coded_resolution: Resolution,
queue_layout: FrameLayout,
}
impl OutputBufferHandle for MmapNM12Frame<'_> {
type PrimitiveBufferHandles = Vec<MmapHandle>;
fn queue(self, buffer: OutputBuffer<'_, Self::PrimitiveBufferHandles>) -> anyhow::Result<()> {
let mut input_y = vec![0u8; self.input_coded_resolution.get_area()];
let mut input_uv = vec![0u8; self.input_coded_resolution.get_area() / 2];
// Use |read_at()| instead of |read()| so we don't need to take a mutable reference to the
// File. We don't know how many in flight OutputBufferHandles will be created in advance,
// and we can only mutably borrow once. We could get around this with an Rc RefCell or an
// Arc if we decide we need to use |read()| because we want to support non-POSIX platforms.
assert_eq!(
self.file
.read_at(input_y.as_mut_slice(), self.pos)
.expect("Unexpected EOF!"),
self.input_coded_resolution.get_area()
);
match self.input_fourcc {
DecodedFormat::NV12 => {
assert_eq!(
self.file
.read_at(
input_uv.as_mut_slice(),
self.pos + self.input_coded_resolution.get_area() as u64
)
.expect("Unexpected EOF!"),
self.input_coded_resolution.get_area() / 2
);
}
DecodedFormat::I420 => {
let mut input_u = vec![0u8; self.input_coded_resolution.get_area() / 4];
let mut input_v = vec![0u8; self.input_coded_resolution.get_area() / 4];
assert_eq!(
self.file
.read_at(
input_u.as_mut_slice(),
self.pos + self.input_coded_resolution.get_area() as u64
)
.expect("Unexpected EOF!"),
self.input_coded_resolution.get_area() / 4
);
assert_eq!(
self.file
.read_at(
input_v.as_mut_slice(),
self.pos + self.input_coded_resolution.get_area() as u64 * 5 / 4
)
.expect("Unexpected EOF!"),
self.input_coded_resolution.get_area() / 4
);
i420_to_nv12_chroma(
input_u.as_slice(),
input_v.as_slice(),
input_uv.as_mut_slice(),
);
}
_ => panic!("Unsupported input format!"),
};
let mut y_plane = buffer.get_plane_mapping(0).unwrap();
let mut uv_plane = buffer.get_plane_mapping(1).unwrap();
nv12_copy(
input_y.as_slice(),
self.input_coded_resolution.width as usize,
y_plane.as_mut(),
self.queue_layout.planes[0].stride,
input_uv.as_slice(),
self.input_coded_resolution.width as usize,
uv_plane.as_mut(),
self.queue_layout.planes[1].stride,
self.resolution.width as usize,
self.resolution.height as usize,
);
extend_border_nv12(
y_plane.as_mut(),
uv_plane.as_mut(),
self.resolution.width as usize,
self.resolution.height as usize,
self.queue_layout.planes[0].stride as usize,
self.queue_layout.size.height as usize,
);
buffer.queue(&[y_plane.len(), uv_plane.len()])?;
Ok(())
}
}
// Generator for MmapNM12Frames. Note that we do the actual loading from disk in |queue()|, this
// just basically just keeps track of offsets.
struct DiskFrameReader<'a> {
file: &'a File,
input_fourcc: DecodedFormat,
visible_size: Resolution,
input_coded_size: Resolution,
layout: FrameLayout,
pos: u64,
frame_num: usize,
total_frames: usize,
}
impl<'a> Iterator for DiskFrameReader<'a> {
type Item = (FrameMetadata, MmapNM12Frame<'a>);
fn next(&mut self) -> Option<Self::Item> {
if self.frame_num >= self.total_frames {
return None;
}
let meta = FrameMetadata {
timestamp: self.frame_num as u64,
layout: self.layout.clone(),
force_keyframe: false,
};
let handle = MmapNM12Frame {
resolution: self.visible_size,
file: &self.file,
input_fourcc: self.input_fourcc.clone(),
pos: self.pos,
input_coded_resolution: self.input_coded_size,
queue_layout: self.layout.clone(),
};
self.frame_num += 1;
// The 3/2 is an implicit assumption about 4:2:0 subsampling. We probably don't need to
// support other subsampling methods, but if we do, make sure to change this line!
self.pos += self.input_coded_size.get_area() as u64 * 3 / 2;
Some((meta, handle))
}
}
impl<'a> DiskFrameReader<'_> {
pub fn new(
file: &'a File,
input_fourcc: DecodedFormat,
visible_size: Resolution,
input_coded_size: Resolution,
layout: FrameLayout,
total_frames: usize,
) -> DiskFrameReader<'a> {
DiskFrameReader {
file: file,
input_fourcc: input_fourcc,
visible_size: visible_size,
input_coded_size: input_coded_size,
layout: layout,
pos: 0,
frame_num: 0,
total_frames: total_frames,
}
}
}
// V4L2 stateful decoders are all of the form "StatefulEncoder<Handle, V4L2Backend<Handle,
// CaptureBufferz, Codec>>". Since we know that all the encoders in this file are going to be V4L2
// stateful and we know the Handle type is going to be MmapNM12Frame, we can alias this type a
// little bit to make the signature smaller.
type MmapEncoder<'a, Codec> =
StatefulEncoder<MmapNM12Frame<'a>, V4L2Backend<MmapNM12Frame<'a>, MmapingCapture, Codec>>;
fn codec_to_pixelformat(codec: Codec) -> v4l2r::PixelFormat {
match codec {
Codec::H264 => v4l2r::PixelFormat::from_fourcc(b"H264"),
Codec::H265 => v4l2r::PixelFormat::from_fourcc(b"HEVC"),
Codec::VP9 => v4l2r::PixelFormat::from_fourcc(b"VP90"),
Codec::VP8 => v4l2r::PixelFormat::from_fourcc(b"VP80"),
_ => panic!("Unsupported format!"),
}
}
fn codec_to_ivf_magic(codec: Codec) -> [u8; 4] {
match codec {
// Note that H264 does not generally use IVF containers.
Codec::VP8 => IvfFileHeader::CODEC_VP8,
Codec::VP9 => IvfFileHeader::CODEC_VP9,
_ => panic!("Unsupported format!"),
}
}
fn do_encode_loop<'a, Codecz>(
mut encoder: MmapEncoder<'a, Codecz>,
input: &'a File,
args: Args,
) -> ()
where
V4L2Backend<MmapNM12Frame<'a>, MmapingCapture, Codecz>: EncoderCodec,
{
// This is the part where we G_FMT to get the actual dimensions of the queue buffers.
let layout = v4l2_format_to_frame_layout(&encoder.backend().output_format().unwrap());
let mut frame_reader = DiskFrameReader::new(
&input,
args.fourcc,
(args.width, args.height).into(),
(
args.coded_width.unwrap_or(args.width),
args.coded_height.unwrap_or(args.height),
)
.into(),
layout,
args.count,
);
let codec = args.codec.unwrap_or_default();
let output_file = args.output.map(|path| {
let mut output = File::create(path).expect("Error opening output file!");
if codec != Codec::H264 {
let hdr = IvfFileHeader::new(
codec_to_ivf_magic(codec),
args.width as u16,
args.height as u16,
args.framerate,
args.count as u32,
);
hdr.writo_into(&mut output)
.expect("Error writing IVF file header!");
}
output
});
// Unwrapping an optional takes ownership of it, so we do that outside of the lambda so we
// don't violate FnMut's lifetime requirements.
match output_file {
Some(mut output_file) => {
let frame_consumer = |coded_chunk: CodedBitstreamBuffer| {
if codec != Codec::H264 {
let hdr = IvfFrameHeader {
timestamp: coded_chunk.metadata.timestamp,
frame_size: coded_chunk.bitstream.len() as u32,
};
hdr.writo_into(&mut output_file)
.expect("Error writing IVF frame header!");
}
let _ = output_file
.write(&coded_chunk.bitstream[..])
.expect("Error writing output file!");
};
simple_encode_loop(&mut encoder, &mut frame_reader, frame_consumer)
.expect("Failed to encode!");
}
None => {
simple_encode_loop(&mut encoder, &mut frame_reader, |_| ()).expect("Failed to encode!")
}
};
}
pub fn do_encode(input: File, args: Args) -> () {
let codec = args.codec.unwrap_or_default();
let device = find_device_with_capture(codec_to_pixelformat(codec))
.expect("Could not find an encoder for codec");
let device = Device::open(&device, DeviceConfig::new().non_blocking_dqbuf()).expect("open");
let device = Arc::new(device);
let resolution = Resolution {
width: args.width,
height: args.height,
};
let queue_fourcc = Fourcc::from(b"NM12");
let tunings: Tunings = Tunings {
rate_control: RateControl::ConstantBitrate(args.bitrate),
framerate: args.framerate,
..Default::default()
};
match codec {
Codec::H264 => do_encode_loop(
V4L2StatefulH264Encoder::new(
device,
MmapingCapture,
cros_codecs::encoder::h264::EncoderConfig {
resolution: resolution.clone(),
initial_tunings: tunings.clone(),
..Default::default()
},
queue_fourcc,
resolution,
tunings,
)
.expect("Failed to create encoder"),
&input,
args,
),
Codec::H265 => do_encode_loop(
V4L2StatefulH265Encoder::new(
device,
MmapingCapture,
cros_codecs::encoder::h265::EncoderConfig {
resolution: resolution.clone(),
..Default::default()
},
queue_fourcc,
resolution,
tunings,
)
.expect("Failed to create encoder"),
&input,
args,
),
Codec::VP8 => do_encode_loop(
V4L2StatefulVP8Encoder::new(
device,
MmapingCapture,
cros_codecs::encoder::vp8::EncoderConfig {
resolution: resolution.clone(),
..Default::default()
},
queue_fourcc,
resolution,
tunings,
)
.expect("Failed to create encoder"),
&input,
args,
),
Codec::VP9 => do_encode_loop(
V4L2StatefulVP9Encoder::new(
device,
MmapingCapture,
cros_codecs::encoder::vp9::EncoderConfig {
resolution: resolution.clone(),
initial_tunings: tunings.clone(),
..Default::default()
},
queue_fourcc,
resolution,
tunings,
)
.expect("Failed to create encoder"),
&input,
args,
),
_ => panic!("Unsupported format!"),
};
}