use std::cmp::Reverse;
use std::collections::BinaryHeap;
use bytes::Bytes;
use moq_net::Timestamp;
use openh264::OpenH264API;
use openh264::decoder::{Decoder, DecoderConfig, Flush};
use openh264::formats::YUVSource;
use openh264_sys2::{
DECODING_STATE, dsBitstreamError, dsDataErrorConcealed, dsDepLayerLost, dsErrorFree, dsNoParamSets, dsOutOfMemory,
dsRefListNullPtrs, dsRefLost,
};
use super::{Backend, Codec, Config};
use crate::frame::{I420, Surface};
use crate::{Error, Frame};
pub(crate) const NAME: &str = "openh264";
const PICTURE_LOST: DECODING_STATE = dsRefLost
| dsBitstreamError
| dsDepLayerLost
| dsNoParamSets
| dsDataErrorConcealed
| dsRefListNullPtrs
| dsOutOfMemory;
const DECODER_RESET: DECODING_STATE = dsOutOfMemory | dsRefListNullPtrs;
const MAX_PENDING: usize = 32;
pub(crate) struct Openh264 {
decoder: Decoder,
pending: BinaryHeap<Reverse<Timestamp>>,
lost: u64,
}
fn new_decoder() -> Result<Decoder, Error> {
let config = DecoderConfig::new().flush_after_decode(Flush::NoFlush);
Decoder::with_api_config(OpenH264API::from_source(), config)
.map_err(|e| Error::Codec(anyhow::anyhow!("openh264 decoder init: {e}")))
}
impl Openh264 {
pub(crate) fn open(codec: Codec, config: &Config) -> Result<Box<dyn Backend>, Error> {
Ok(Box::new(Self::new(codec, config)?))
}
fn new(codec: Codec, _config: &Config) -> Result<Self, Error> {
if codec != Codec::H264 {
return Err(Error::Codec(anyhow::anyhow!(
"openh264 cannot decode {}",
codec.label()
)));
}
let decoder = new_decoder()?;
tracing::info!(decoder = NAME, "opened H.264 decoder");
Ok(Self {
decoder,
pending: BinaryHeap::new(),
lost: 0,
})
}
fn picture_lost(&mut self, timestamp: Timestamp, err: &openh264::Error) -> Result<Vec<Frame>, Error> {
let state = i32::try_from(err.native_code()).unwrap_or(dsErrorFree);
if state == dsErrorFree || state & !PICTURE_LOST != 0 {
return Err(Error::Codec(anyhow::anyhow!("openh264 decode: {err}")));
}
if state & DECODER_RESET != 0 {
self.pending.clear();
} else {
let mut removed = false;
self.pending.retain(|pending| {
if !removed && pending.0 == timestamp {
removed = true;
false
} else {
true
}
});
}
self.lost += 1;
if self.lost == 1 {
tracing::warn!(
decoder = NAME,
state = format_args!("{state:#06x}"),
"picture lost, waiting for the next keyframe"
);
} else {
tracing::trace!(decoder = NAME, state = format_args!("{state:#06x}"), "picture lost");
}
Ok(Vec::new())
}
fn picture_out(&mut self) -> Option<Timestamp> {
if self.lost > 0 {
tracing::info!(decoder = NAME, lost = self.lost, "picture recovered");
self.lost = 0;
}
self.pending.pop().map(|Reverse(timestamp)| timestamp)
}
fn picture_in(&mut self, timestamp: Timestamp) {
self.pending.push(Reverse(timestamp));
if self.pending.len() > MAX_PENDING {
let mut kept: Vec<_> = self.pending.drain().collect();
kept.sort_unstable_by_key(|pending| pending.0);
kept.truncate(MAX_PENDING);
self.pending = kept.into_iter().collect();
}
}
}
fn picture(yuv: &impl YUVSource, timestamp: Timestamp) -> Result<Frame, Error> {
let (width, height) = yuv.dimensions();
if width % 2 != 0 || height % 2 != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"decoded frame has odd dimensions {width}x{height}, expected 4:2:0"
)));
}
let (y_stride, uv_stride, _) = yuv.strides();
let frame = I420::from_planes(
yuv.y(),
yuv.u(),
yuv.v(),
y_stride,
uv_stride,
width as u32,
height as u32,
);
Ok(Frame::new(Surface::I420(frame), timestamp))
}
impl Backend for Openh264 {
fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, _keyframe: bool) -> Result<Vec<Frame>, Error> {
self.picture_in(timestamp);
let decoded = match self.decoder.decode(&access_unit) {
Ok(Some(yuv)) => Some(picture(&yuv, timestamp)?),
Ok(None) => None,
Err(err) => return self.picture_lost(timestamp, &err),
};
match decoded {
Some(mut frame) => {
if let Some(timestamp) = self.picture_out() {
frame.timestamp = timestamp;
}
Ok(vec![frame])
}
None => Ok(Vec::new()),
}
}
fn flush(&mut self) -> Result<Vec<Frame>, Error> {
let tail = self
.decoder
.flush_remaining()
.map_err(|e| Error::Codec(anyhow::anyhow!("openh264 flush: {e}")))?;
let decoded: Vec<Frame> = tail
.iter()
.map(|yuv| picture(yuv, Timestamp::ZERO))
.collect::<Result<_, _>>()?;
drop(tail);
let mut frames = Vec::with_capacity(decoded.len());
let mut unstamped = 0usize;
for mut frame in decoded {
match self.picture_out() {
Some(timestamp) => {
frame.timestamp = timestamp;
frames.push(frame);
}
None => unstamped += 1,
}
}
if unstamped > 0 {
tracing::warn!(
decoder = NAME,
dropped = unstamped,
"flushed pictures had no timestamp waiting for them"
);
}
self.pending.clear();
self.lost = 0;
self.decoder = new_decoder()?;
Ok(frames)
}
fn name(&self) -> &str {
NAME
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use moq_mux::codec::annexb;
use super::*;
const BFRAMES: &[u8] = include_bytes!("../test_data/bframes_64x64_pattern_30f.h264");
const PICTURES: usize = 30;
const FRAME_MICROS: u64 = 33_333;
fn nal_type(nal: &Bytes) -> u8 {
nal.first().map_or(0, |b| b & 0x1f)
}
fn access_units() -> Vec<(Bytes, bool)> {
let mut buf = Bytes::from_static(BFRAMES);
let mut iter = annexb::NalIterator::new(&mut buf);
let mut nals: Vec<Bytes> = iter
.by_ref()
.map(|nal| nal.expect("fixture is valid Annex-B"))
.collect();
nals.extend(iter.flush().expect("fixture is valid Annex-B"));
let is_slice = |nal: &Bytes| matches!(nal_type(nal), 1 | 5);
let mut units = Vec::new();
let mut current: Vec<Bytes> = Vec::new();
for nal in nals {
if current.iter().any(is_slice) && matches!(nal_type(&nal), 1 | 5 | 6 | 7 | 8 | 9) {
units.push((
annexb::build_prefix(current.iter()),
current.iter().any(|nal| nal_type(nal) == 5),
));
current.clear();
}
current.push(nal);
}
if current.iter().any(is_slice) {
units.push((
annexb::build_prefix(current.iter()),
current.iter().any(|nal| nal_type(nal) == 5),
));
}
assert_eq!(units.len(), PICTURES, "the fixture split wrongly");
units
}
fn at(index: usize) -> Timestamp {
Timestamp::from_micros(index as u64 * FRAME_MICROS).expect("fixture timestamp")
}
fn open() -> Openh264 {
Openh264::new(Codec::H264, &Config::new()).expect("the software decoder always opens")
}
fn decode_fixture(broken: Option<usize>) -> Vec<Frame> {
let mut decoder = open();
let mut frames = Vec::new();
for (index, (payload, keyframe)) in access_units().into_iter().enumerate() {
let payload = if broken == Some(index) {
payload.slice(..payload.len() / 3)
} else {
payload
};
frames.extend(
decoder
.decode(payload, at(index), keyframe)
.unwrap_or_else(|e| panic!("access unit {index} ended the stream: {e}")),
);
}
frames.extend(decoder.flush().expect("the drain works"));
frames
}
fn pixels(frame: &Frame) -> Vec<u8> {
frame.surface.to_i420().expect("software output is I420").data.clone()
}
#[test]
fn repeated_flushes_keep_decoding() {
let mut decoder = open();
for round in 0..6 {
let mut decoded = 0;
for (index, (payload, keyframe)) in access_units().into_iter().enumerate() {
decoded += decoder
.decode(payload, at(index), keyframe)
.unwrap_or_else(|e| panic!("round {round} access unit {index}: {e}"))
.len();
}
decoded += decoder.flush().expect("the drain works").len();
assert_eq!(decoded, PICTURES, "round {round} lost pictures");
}
}
#[test]
fn a_stream_with_b_frames_decodes_all_the_way_through() {
let mut decoder = open();
let mut decoded = 0;
for (index, (payload, keyframe)) in access_units().into_iter().enumerate() {
decoded += decoder
.decode(payload, at(index), keyframe)
.unwrap_or_else(|e| panic!("access unit {index} failed: {e}"))
.len();
}
decoded += decoder.flush().expect("the drain works").len();
assert_eq!(decoded, PICTURES, "the stream lost pictures");
}
#[test]
fn reordered_pictures_keep_their_own_timestamps() {
let mut decoder = open();
let mut timestamps = Vec::new();
for (index, (payload, keyframe)) in access_units().into_iter().enumerate() {
for frame in decoder.decode(payload, at(index), keyframe).expect("decodes") {
timestamps.push(frame.timestamp.as_micros());
}
}
for frame in decoder.flush().expect("the drain works") {
timestamps.push(frame.timestamp.as_micros());
}
let expected: Vec<u128> = (0..PICTURES).map(|i| at(i).as_micros()).collect();
assert_eq!(timestamps, expected, "pictures came out mis-stamped");
}
#[test]
fn a_truncated_access_unit_costs_pictures_not_the_stream() {
let mut decoder = open();
let units = access_units();
let broken = 5;
let mut decoded = 0;
let mut recovered = 0;
for (index, (payload, keyframe)) in units.into_iter().enumerate() {
let payload = if index == broken {
payload.slice(..payload.len() / 3)
} else {
payload
};
let frames = decoder
.decode(payload, at(index), keyframe)
.unwrap_or_else(|e| panic!("access unit {index} ended the stream: {e}"))
.len();
decoded += frames;
if index > broken {
recovered += frames;
}
}
assert!(decoded > 0, "nothing decoded at all");
assert!(recovered > 0, "the decoder never recovered from the truncated unit");
}
#[test]
fn a_truncated_access_unit_does_not_shift_later_timestamps() {
let clean = decode_fixture(None);
let expected: HashMap<Vec<u8>, Timestamp> =
clean.iter().map(|frame| (pixels(frame), frame.timestamp)).collect();
assert_eq!(expected.len(), clean.len(), "fixture pictures are not unique");
let broken = 5;
let mut recovered = 0;
for frame in decode_fixture(Some(broken)) {
let Some(timestamp) = expected.get(&pixels(&frame)) else {
continue;
};
assert_eq!(
frame.timestamp, *timestamp,
"a surviving picture was stamped as a lost one"
);
if *timestamp >= at(15) {
recovered += 1;
}
}
assert!(recovered > 0, "the second GOP never recovered clean pictures");
}
#[test]
fn pending_limit_preserves_the_next_picture() {
let mut decoder = open();
for index in 0..=MAX_PENDING {
decoder.picture_in(at(index));
}
let timestamps: Vec<_> = std::iter::from_fn(|| decoder.picture_out()).collect();
let expected: Vec<_> = (0..MAX_PENDING).map(at).collect();
assert_eq!(timestamps, expected);
}
}