#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
reason = "unsafe-libopus's C-shaped API takes i32 counts/lengths; sample_rate, frame sizes, \
and packet lengths are always small in practice (Opus legal rates top out at \
48 kHz, packets are bytes over the network) — `frame_size_samples` already \
rejects values that would not fit i32 before they reach these casts."
)]
use std::collections::VecDeque;
use std::ptr;
use mediaway_common::{AudioFrame, Bytes, CodecKind, Packet, SampleFormat, StreamInfo};
use unsafe_libopus::{
OPUS_SET_BITRATE_REQUEST, OPUS_SET_INBAND_FEC_REQUEST, OPUS_SET_PACKET_LOSS_PERC_REQUEST,
OpusEncoder as RawOpusEncoder, opus_encode_float, opus_encoder_create, opus_encoder_ctl,
opus_encoder_destroy, opus_strerror,
};
use crate::opus::config::{OpusEncoderConfig, frame_size_samples};
use crate::opus::error::OpusError;
const MAX_PACKET_BYTES: usize = 4000;
fn backend_result(code: i32) -> Result<(), OpusError> {
if code < 0 {
Err(OpusError::Backend {
code,
message: opus_strerror(code),
})
} else {
Ok(())
}
}
#[derive(Debug)]
pub struct OpusEncoder {
ptr: ptr::NonNull<RawOpusEncoder>,
stream_info: StreamInfo,
sample_rate: u32,
channels: u16,
frame_size_samples: usize,
pcm_scratch: Vec<f32>,
packet_scratch: Vec<u8>,
pending: VecDeque<Packet>,
closed: bool,
}
unsafe impl Send for OpusEncoder {}
impl OpusEncoder {
pub fn open(config: &OpusEncoderConfig) -> Result<Self, OpusError> {
let frame_size = frame_size_samples(config.sample_rate, config.time_base)?;
let channels = i32::from(config.channels);
let mut err: i32 = 0;
let raw = unsafe {
opus_encoder_create(
config.sample_rate as i32,
channels,
config.application.to_raw(),
&raw mut err,
)
};
let Some(ptr) = ptr::NonNull::new(raw) else {
return Err(OpusError::Backend {
code: err,
message: opus_strerror(err),
});
};
if let Err(e) = configure(ptr, config) {
unsafe { opus_encoder_destroy(ptr.as_ptr()) };
return Err(e);
}
let channels_usize = usize::from(config.channels);
Ok(Self {
ptr,
stream_info: StreamInfo::Audio {
id: 0,
codec: CodecKind::Opus,
time_base: config.time_base,
extra_data: Bytes::new(),
sample_rate: config.sample_rate,
channels: config.channels,
},
sample_rate: config.sample_rate,
channels: config.channels,
frame_size_samples: frame_size,
pcm_scratch: Vec::with_capacity(frame_size * channels_usize),
packet_scratch: vec![0u8; MAX_PACKET_BYTES],
pending: VecDeque::new(),
closed: false,
})
}
#[must_use]
pub const fn stream_info(&self) -> &StreamInfo {
&self.stream_info
}
pub fn push_frame(&mut self, frame: &AudioFrame) -> Result<(), OpusError> {
if self.closed {
return Err(OpusError::Closed);
}
if frame.format != SampleFormat::F32 {
return Err(OpusError::UnsupportedSampleFormat);
}
if frame.sample_rate != self.sample_rate || frame.channels != self.channels {
return Err(OpusError::ConfigMismatch);
}
let expected_bytes =
self.frame_size_samples * usize::from(self.channels) * size_of::<f32>();
if frame.data.len() != expected_bytes {
return Err(OpusError::FrameSizeMismatch {
expected_samples: self.frame_size_samples,
expected_bytes,
actual_bytes: frame.data.len(),
});
}
self.pcm_scratch.clear();
self.pcm_scratch.extend(
frame
.data
.chunks_exact(size_of::<f32>())
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
);
let len = unsafe {
opus_encode_float(
self.ptr.as_ptr(),
self.pcm_scratch.as_ptr(),
self.frame_size_samples as i32,
self.packet_scratch.as_mut_ptr(),
self.packet_scratch.len() as i32,
)
};
if len < 0 {
return Err(OpusError::Backend {
code: len,
message: opus_strerror(len),
});
}
self.pending.push_back(Packet {
stream_id: self.stream_info.id(),
pts: frame.pts,
dts: frame.pts,
duration: frame.duration,
is_keyframe: true,
is_discard: false,
payload: Bytes::copy_from_slice(&self.packet_scratch[..len as usize]),
});
Ok(())
}
pub fn poll_packet(&mut self) -> Result<Option<Packet>, OpusError> {
Ok(self.pending.pop_front())
}
pub const fn flush(&mut self) -> Result<(), OpusError> {
self.closed = true;
Ok(())
}
}
fn configure(
ptr: ptr::NonNull<RawOpusEncoder>,
config: &OpusEncoderConfig,
) -> Result<(), OpusError> {
if let Some(bitrate) = config.bitrate_bps {
let bitrate = i32::try_from(bitrate).unwrap_or(i32::MAX);
backend_result(unsafe {
opus_encoder_ctl!(ptr.as_ptr(), OPUS_SET_BITRATE_REQUEST, bitrate)
})?;
}
if config.inband_fec {
backend_result(unsafe {
opus_encoder_ctl!(ptr.as_ptr(), OPUS_SET_INBAND_FEC_REQUEST, 1i32)
})?;
let loss_percent = i32::from(config.packet_loss_percent);
backend_result(unsafe {
opus_encoder_ctl!(
ptr.as_ptr(),
OPUS_SET_PACKET_LOSS_PERC_REQUEST,
loss_percent
)
})?;
}
Ok(())
}
impl Drop for OpusEncoder {
fn drop(&mut self) {
unsafe { opus_encoder_destroy(self.ptr.as_ptr()) };
}
}
#[cfg(test)]
#[path = "encoder_tests.rs"]
mod tests;