Skip to content

Commit

Permalink
Allow dynamic resolution changes.
Browse files Browse the repository at this point in the history
If a frame size differs from the previous, reinitialize the openh264
encoder instance with the new parameters.
  • Loading branch information
algesten committed Mar 8, 2024
1 parent 2a81e8a commit a9075c3
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 43 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ And **encode** the same YUV back to H.264:
```rust
use openh264::encoder::{Encoder, EncoderConfig};

let config = EncoderConfig::new(512, 512);
let config = EncoderConfig::new();
let api = OpenH264API::from_source();
let mut encoder = Encoder::with_config(api, config)?;

Expand Down
2 changes: 1 addition & 1 deletion openh264/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ And **encode** the same YUV back to H.264:
```rust
use openh264::encoder::{Encoder, EncoderConfig};

let config = EncoderConfig::new(512, 512);
let config = EncoderConfig::new();
let api = OpenH264API::from_source();
let mut encoder = Encoder::with_config(api, config)?;

Expand Down
98 changes: 64 additions & 34 deletions openh264/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::formats::YUVSource;
use crate::{Error, OpenH264API, Timestamp};
use openh264_sys2::{
videoFormatI420, EVideoFormatType, ISVCEncoder, ISVCEncoderVtbl, SEncParamBase, SEncParamExt, SFrameBSInfo, SLayerBSInfo,
SSourcePicture, API, ENCODER_OPTION, ENCODER_OPTION_DATAFORMAT, ENCODER_OPTION_TRACE_LEVEL, RC_MODES, VIDEO_CODING_LAYER,
WELS_LOG_DETAIL, WELS_LOG_QUIET,
SSourcePicture, API, ENCODER_OPTION, ENCODER_OPTION_DATAFORMAT, ENCODER_OPTION_SVC_ENCODE_PARAM_EXT,
ENCODER_OPTION_TRACE_LEVEL, RC_MODES, VIDEO_CODING_LAYER, WELS_LOG_DETAIL, WELS_LOG_QUIET,
};
use std::os::raw::{c_int, c_uchar, c_void};
use std::ptr::{addr_of_mut, null, null_mut};
Expand Down Expand Up @@ -128,8 +128,6 @@ impl RateControlMode {
/// Setting missing? Please file a PR!
#[derive(Default, Copy, Clone, Debug)]
pub struct EncoderConfig {
width: u32,
height: u32,
enable_skip_frame: bool,
target_bitrate: u32,
enable_denoise: bool,
Expand All @@ -141,10 +139,8 @@ pub struct EncoderConfig {

impl EncoderConfig {
/// Creates a new default encoder config.
pub fn new(width: u32, height: u32) -> Self {
pub fn new() -> Self {
Self {
width,
height,
enable_skip_frame: true,
target_bitrate: 120_000,
enable_denoise: false,
Expand Down Expand Up @@ -188,40 +184,27 @@ impl EncoderConfig {

/// An [OpenH264](https://github.com/cisco/openh264) encoder.
pub struct Encoder {
params: SEncParamExt,
config: EncoderConfig,
raw_api: EncoderRawAPI,
bit_stream_info: SFrameBSInfo,
previous_dimensions: Option<(i32, i32)>,
}

unsafe impl Send for Encoder {}
unsafe impl Sync for Encoder {}

impl Encoder {
/// Create an encoder with the provided configuration.
pub fn with_config(api: OpenH264API, mut config: EncoderConfig) -> Result<Self, Error> {
///
/// The width and height will be taken from the [`YUVSource`] when calling [`Encoder::encode()`].
pub fn with_config(api: OpenH264API, config: EncoderConfig) -> Result<Self, Error> {
let raw_api = EncoderRawAPI::new(api)?;
let mut params = SEncParamExt::default();

#[rustfmt::skip]
unsafe {
raw_api.get_default_params(&mut params).ok()?;
params.iPicWidth = config.width as c_int;
params.iPicHeight = config.height as c_int;
params.iRCMode = config.rate_control_mode.to_c();
params.bEnableFrameSkip = config.enable_skip_frame;
params.iTargetBitrate = config.target_bitrate as c_int;
params.bEnableDenoise = config.enable_denoise;
params.fMaxFrameRate = config.max_frame_rate;
raw_api.initialize_ext(&params).ok()?;

raw_api.set_option(ENCODER_OPTION_TRACE_LEVEL, addr_of_mut!(config.debug).cast()).ok()?;
raw_api.set_option(ENCODER_OPTION_DATAFORMAT, addr_of_mut!(config.data_format).cast()).ok()?;
};

Ok(Self {
params,
config,
raw_api,
bit_stream_info: Default::default(),
previous_dimensions: None,
})
}

Expand All @@ -231,9 +214,12 @@ impl Encoder {
/// initialization information. Subsequent packages then contain, amongst others, keyframes
/// ("I frames") or delta frames. The interval at which they are produced depends on the encoder settings.
///
/// The resolution of the encoded frame is allowed to change. Each time it changes, the
/// encoder is re-initialized with the new values.
///
/// # Panics
///
/// Panics if the source image dimension don't match the configured format.
/// Panics if the provided timestamp as milliseconds is out of range of i64.
pub fn encode<T: YUVSource>(&mut self, yuv_source: &T) -> Result<EncodedBitStream<'_>, Error> {
self.encode_at(yuv_source, Timestamp::ZERO)
}
Expand All @@ -244,14 +230,19 @@ impl Encoder {
/// initialization information. Subsequent packages then contain, amongst others, keyframes
/// ("I frames") or delta frames. The interval at which they are produced depends on the encoder settings.
///
/// # Panics
/// The resolution of the encoded frame is allowed to change. Each time it changes, the
/// encoder is re-initialized with the new values.
///
/// Panics if the source image dimension don't match the configured format.
/// # Panics
///
/// Panics if the provided timestamp as milliseconds is out of range of i64.
pub fn encode_at<T: YUVSource>(&mut self, yuv_source: &T, timestamp: Timestamp) -> Result<EncodedBitStream<'_>, Error> {
assert_eq!(yuv_source.width(), self.params.iPicWidth);
assert_eq!(yuv_source.height(), self.params.iPicHeight);
let new_dimensions = (yuv_source.width(), yuv_source.height());

if self.previous_dimensions != Some(new_dimensions) {
self.reinit(new_dimensions.0, new_dimensions.1)?;
self.previous_dimensions = Some(new_dimensions);
}

// Converting *const u8 to *mut u8 should be fine because the encoder _should_
// only read these arrays (TODO: needs verification).
Expand All @@ -264,8 +255,8 @@ impl Encoder {
yuv_source.v().as_ptr() as *mut c_uchar,
null_mut(),
],
iPicWidth: self.params.iPicWidth,
iPicHeight: self.params.iPicHeight,
iPicWidth: new_dimensions.0,
iPicHeight: new_dimensions.1,
uiTimeStamp: timestamp.as_native(),
};

Expand All @@ -278,6 +269,45 @@ impl Encoder {
}
}

fn reinit(&mut self, width: i32, height: i32) -> Result<(), Error> {
let mut params = SEncParamExt::default();

unsafe { self.raw_api.get_default_params(&mut params).ok()? };

params.iPicWidth = width as c_int;
params.iPicHeight = height as c_int;
params.iRCMode = self.config.rate_control_mode.to_c();
params.bEnableFrameSkip = self.config.enable_skip_frame;
params.iTargetBitrate = self.config.target_bitrate as c_int;
params.bEnableDenoise = self.config.enable_denoise;
params.fMaxFrameRate = self.config.max_frame_rate;

if self.previous_dimensions.is_none() {
// First time we call initialize_ext
unsafe {
self.raw_api.initialize_ext(&params).ok()?;
self.raw_api
.set_option(ENCODER_OPTION_TRACE_LEVEL, addr_of_mut!(self.config.debug).cast())
.ok()?;
self.raw_api
.set_option(ENCODER_OPTION_DATAFORMAT, addr_of_mut!(self.config.data_format).cast())
.ok()?;
};
} else {
// Subsequent times we call SetOption
unsafe {
self.raw_api
.set_option(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, addr_of_mut!(params).cast())
.ok()?;

// Start with a new keyframe.
self.raw_api.force_intra_frame(true);
}
}

Ok(())
}

/// Obtain the raw API for advanced use cases.
///
/// When resorting to this call, please consider filing an issue / PR.
Expand Down
2 changes: 1 addition & 1 deletion openh264/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
//! # let h264_in = include_bytes!("../tests/data/multi_512x512.h264");
//! # let yuv = decoder.decode(&h264_in[..])?.ok_or_else(|| Error::msg("Must have image"))?;
//!
//! let config = EncoderConfig::new(512, 512);
//! let config = EncoderConfig::new();
//! let api = OpenH264API::from_source();
//! let mut encoder = Encoder::with_config(api, config)?;
//!
Expand Down
2 changes: 1 addition & 1 deletion openh264/tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fn what_goes_around_comes_around() -> Result<(), Error> {
let src = include_bytes!("data/lenna_128x128.rgb");

let api = OpenH264API::from_source();
let config = EncoderConfig::new(128, 128);
let config = EncoderConfig::new();
let mut encoder = Encoder::with_config(api, config)?;
let mut converter = YUVBuffer::new(128, 128);

Expand Down
47 changes: 42 additions & 5 deletions openh264/tests/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use openh264::{Error, OpenH264API, Timestamp};
#[cfg(feature = "source")]
fn can_get_encoder() -> Result<(), Error> {
let api = OpenH264API::from_source();
let config = EncoderConfig::new(300, 200);
let config = EncoderConfig::new();
let _encoder = Encoder::with_config(api, config)?;

Ok(())
Expand All @@ -21,7 +21,7 @@ fn encode() -> Result<(), Error> {
let src = include_bytes!("data/lenna_128x128.rgb");

let api = OpenH264API::from_source();
let config = EncoderConfig::new(128, 128);
let config = EncoderConfig::new();
let mut encoder = Encoder::with_config(api, config)?;
let mut converter = YUVBuffer::new(128, 128);

Expand Down Expand Up @@ -59,7 +59,7 @@ fn encode_at_timestamp_roundtrips() -> Result<(), Error> {
let src = include_bytes!("data/lenna_128x128.rgb");

let api = OpenH264API::from_source();
let config = EncoderConfig::new(128, 128);
let config = EncoderConfig::new();
let mut encoder = Encoder::with_config(api, config)?;
let mut converter = YUVBuffer::new(128, 128);

Expand Down Expand Up @@ -88,7 +88,7 @@ fn encoder_sps_pps() -> Result<(), Error> {
let src = include_bytes!("data/lenna_128x128.rgb");

let api = OpenH264API::from_source();
let config = EncoderConfig::new(128, 128);
let config = EncoderConfig::new();
let mut encoder = Encoder::with_config(api, config)?;
let mut converter = YUVBuffer::new(128, 128);

Expand Down Expand Up @@ -119,7 +119,7 @@ fn what_goes_around_comes_around() -> Result<(), Error> {
let yuv = decoder.decode(src)?.ok_or_else(|| Error::msg("Must have image"))?;

let api = OpenH264API::from_source();
let config = EncoderConfig::new(512, 512);
let config = EncoderConfig::new();
let mut encoder = Encoder::with_config(api, config)?;

let stream = encoder.encode(&yuv)?;
Expand All @@ -146,3 +146,40 @@ fn what_goes_around_comes_around() -> Result<(), Error> {

Ok(())
}

#[test]
#[cfg(feature = "source")]
fn encode_change_resolution() -> Result<(), Error> {
let src1 = include_bytes!("data/lenna_128x128.rgb");
let src2 = include_bytes!("data/lenna_512x512.rgb");

let api = OpenH264API::from_source();
let config = EncoderConfig::new();
let mut encoder = Encoder::with_config(api, config)?;

let converter1 = {
let mut buf = YUVBuffer::new(128, 128);
buf.read_rgb(src1);
buf
};

let stream = encoder.encode(&converter1)?;

assert_eq!(stream.frame_type(), FrameType::IDR);
assert_eq!(stream.num_layers(), 2);
assert_eq!(stream.layer(0).unwrap().nal_count(), 2);

let converter2 = {
let mut buf = YUVBuffer::new(512, 512);
buf.read_rgb(src2);
buf
};

let stream = encoder.encode(&converter2)?;

assert_eq!(stream.frame_type(), FrameType::IDR);
assert_eq!(stream.num_layers(), 2);
assert_eq!(stream.layer(0).unwrap().nal_count(), 2);

Ok(())
}

0 comments on commit a9075c3

Please sign in to comment.