use std::iter::FusedIterator;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameType {
Idr,
P,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GopPattern {
Ipppp { gop: usize },
Ibpbp { gop: usize, b_count: usize },
}
impl GopPattern {
pub const fn iphone_default() -> Self {
Self::Ibpbp { gop: 30, b_count: 1 }
}
pub const fn handbrake_x264_centroid() -> Self {
Self::Ibpbp { gop: 30, b_count: 1 }
}
pub fn auto_select(source_mp4: Option<&[u8]>) -> Self {
let bytes = match source_mp4 {
Some(b) => b,
None => return Self::handbrake_x264_centroid(),
};
analyze_source_pattern(bytes).unwrap_or_else(Self::handbrake_x264_centroid)
}
pub const fn legacy_ipppp(gop: usize) -> Self {
Self::Ipppp { gop }
}
pub fn has_b_frames(&self) -> bool {
matches!(self, Self::Ibpbp { .. })
}
pub fn gop_size(&self) -> usize {
match self {
Self::Ipppp { gop } | Self::Ibpbp { gop, .. } => *gop,
}
}
pub fn legacy_b_count(&self) -> usize {
match self {
Self::Ipppp { .. } => 0,
Self::Ibpbp { b_count, .. } => *b_count,
}
}
pub fn frame_type_at(&self, display_idx: usize) -> FrameType {
match self {
Self::Ipppp { gop } => {
if display_idx.is_multiple_of(*gop) {
FrameType::Idr
} else {
FrameType::P
}
}
Self::Ibpbp { gop, b_count } => {
let pos_in_gop = display_idx % gop;
if pos_in_gop == 0 {
return FrameType::Idr;
}
if pos_in_gop == gop - 1 {
return FrameType::P;
}
let m = b_count + 1;
let sub_gop_pos = (pos_in_gop - 1) % m;
if sub_gop_pos < *b_count {
FrameType::B
} else {
FrameType::P
}
}
}
}
}
impl Default for GopPattern {
fn default() -> Self {
Self::iphone_default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncodeOrderFrame {
pub encode_idx: u32,
pub display_idx: u32,
pub gop_idx: u32,
pub frame_type: FrameType,
}
pub fn iter_encode_order(
n_frames: usize,
pattern: GopPattern,
) -> EncodeOrderIter {
EncodeOrderIter::new(n_frames, pattern)
}
#[derive(Debug, Clone)]
pub struct EncodeOrderIter {
frames: std::vec::IntoIter<EncodeOrderFrame>,
}
impl EncodeOrderIter {
fn new(n_frames: usize, pattern: GopPattern) -> Self {
let mut frames = Vec::with_capacity(n_frames);
let mut encode_idx: u32 = 0;
let mut display_pos = 0usize;
let mut gop_idx: u32 = 0;
let gop_size = pattern.gop_size();
debug_assert!(gop_size > 0, "gop_size must be > 0");
while display_pos < n_frames {
let gop_start = display_pos;
let gop_end = (gop_start + gop_size).min(n_frames);
match pattern {
GopPattern::Ipppp { .. } => {
for d in gop_start..gop_end {
let ft = pattern.frame_type_at(d);
frames.push(EncodeOrderFrame {
encode_idx,
display_idx: d as u32,
gop_idx,
frame_type: ft,
});
encode_idx += 1;
}
}
GopPattern::Ibpbp { b_count, .. } => {
let m = b_count + 1;
frames.push(EncodeOrderFrame {
encode_idx,
display_idx: gop_start as u32,
gop_idx,
frame_type: FrameType::Idr,
});
encode_idx += 1;
let mut sub_start = gop_start + 1;
while sub_start < gop_end {
let sub_end = (sub_start + m).min(gop_end);
let anchor = sub_end - 1;
let anchor_ft = pattern.frame_type_at(anchor);
frames.push(EncodeOrderFrame {
encode_idx,
display_idx: anchor as u32,
gop_idx,
frame_type: anchor_ft,
});
encode_idx += 1;
for d in sub_start..anchor {
let ft = pattern.frame_type_at(d);
debug_assert_eq!(
ft,
FrameType::B,
"sub-GOP intermediate must be B (display_idx={d})",
);
frames.push(EncodeOrderFrame {
encode_idx,
display_idx: d as u32,
gop_idx,
frame_type: ft,
});
encode_idx += 1;
}
sub_start = sub_end;
}
}
}
display_pos = gop_end;
gop_idx += 1;
}
debug_assert_eq!(frames.len(), n_frames);
Self {
frames: frames.into_iter(),
}
}
}
impl Iterator for EncodeOrderIter {
type Item = EncodeOrderFrame;
fn next(&mut self) -> Option<Self::Item> {
self.frames.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.frames.size_hint()
}
}
impl ExactSizeIterator for EncodeOrderIter {}
impl FusedIterator for EncodeOrderIter {}
fn analyze_source_pattern(mp4_bytes: &[u8]) -> Option<GopPattern> {
let parsed = crate::codec::mp4::demux::demux(mp4_bytes).ok()?;
let video_idx = parsed.video_track_idx?;
let track = &parsed.tracks[video_idx];
if !track.is_h264() {
return None;
}
let detected_gop = detect_modal_gop(&track.samples);
let has_b_frames = source_has_ctts(mp4_bytes);
let gop = detected_gop.unwrap_or(30).clamp(1, 600);
if has_b_frames {
Some(GopPattern::Ibpbp { gop, b_count: 1 })
} else {
Some(GopPattern::Ipppp { gop })
}
}
fn detect_modal_gop(samples: &[crate::codec::mp4::Sample]) -> Option<usize> {
let sync_indices: Vec<usize> = samples
.iter()
.enumerate()
.filter_map(|(i, s)| if s.is_sync { Some(i) } else { None })
.collect();
if sync_indices.len() < 2 {
return None;
}
let mut counts: std::collections::HashMap<usize, usize> = Default::default();
for w in sync_indices.windows(2) {
let gap = w[1] - w[0];
*counts.entry(gap).or_insert(0) += 1;
}
counts.into_iter().max_by_key(|&(_, n)| n).map(|(gap, _)| gap)
}
fn source_has_ctts(mp4_bytes: &[u8]) -> bool {
let mut found = false;
walk_mp4_for_box(mp4_bytes, b"ctts", &mut found);
found
}
fn walk_mp4_for_box(data: &[u8], target: &[u8; 4], found: &mut bool) {
if *found {
return;
}
let _ = crate::codec::mp4::iterate_boxes(data, 0, data.len(), |h, content_start, _| {
if *found {
return Ok(());
}
if h.box_type == *target {
*found = true;
return Ok(());
}
match &h.box_type {
b"moov" | b"trak" | b"mdia" | b"minf" | b"stbl" => {
let inner_end = content_start + h.size as usize - h.header_len as usize;
walk_mp4_subtree(data, content_start, inner_end, target, found);
}
_ => {}
}
Ok(())
});
}
fn walk_mp4_subtree(data: &[u8], start: usize, end: usize, target: &[u8; 4], found: &mut bool) {
if *found {
return;
}
let _ = crate::codec::mp4::iterate_boxes(data, start, end, |h, cs, _| {
if *found {
return Ok(());
}
if h.box_type == *target {
*found = true;
return Ok(());
}
match &h.box_type {
b"moov" | b"trak" | b"mdia" | b"minf" | b"stbl" => {
let inner_end = cs + h.size as usize - h.header_len as usize;
walk_mp4_subtree(data, cs, inner_end, target, found);
}
_ => {}
}
Ok(())
});
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_types(n_frames: usize, pattern: GopPattern) -> Vec<FrameType> {
iter_encode_order(n_frames, pattern)
.map(|f| f.frame_type)
.collect()
}
fn collect_display(n_frames: usize, pattern: GopPattern) -> Vec<u32> {
iter_encode_order(n_frames, pattern)
.map(|f| f.display_idx)
.collect()
}
#[test]
fn ipppp_encode_equals_display_order() {
let pat = GopPattern::Ipppp { gop: 5 };
let display: Vec<u32> = collect_display(10, pat);
assert_eq!(display, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn ipppp_idr_at_gop_boundary_only() {
let pat = GopPattern::Ipppp { gop: 5 };
let types = collect_types(10, pat);
assert_eq!(
types,
vec![
FrameType::Idr, FrameType::P, FrameType::P, FrameType::P, FrameType::P,
FrameType::Idr, FrameType::P, FrameType::P, FrameType::P, FrameType::P,
],
);
}
#[test]
fn ibpbp_m2_encode_order_is_anchor_first() {
let pat = GopPattern::Ibpbp { gop: 5, b_count: 1 };
let display = collect_display(5, pat);
assert_eq!(display, vec![0, 2, 1, 4, 3]);
}
#[test]
fn ibpbp_m2_frame_types_match_encode_order() {
let pat = GopPattern::Ibpbp { gop: 5, b_count: 1 };
let types = collect_types(5, pat);
assert_eq!(
types,
vec![FrameType::Idr, FrameType::P, FrameType::B, FrameType::P, FrameType::B],
);
}
#[test]
fn ibpbp_m2_closed_gop_last_is_p() {
let pat = GopPattern::Ibpbp { gop: 10, b_count: 1 };
let frames: Vec<_> = iter_encode_order(10, pat).collect();
let last = frames.iter().find(|f| f.display_idx == 9).unwrap();
assert_eq!(last.frame_type, FrameType::P);
}
#[test]
fn ibpbp_m3_encode_order_two_bs_per_subgop() {
let pat = GopPattern::Ibpbp { gop: 7, b_count: 2 };
let display = collect_display(7, pat);
assert_eq!(display, vec![0, 3, 1, 2, 6, 4, 5]);
}
#[test]
fn multi_gop_idr_periodicity() {
let pat = GopPattern::Ibpbp { gop: 5, b_count: 1 };
let frames: Vec<_> = iter_encode_order(15, pat).collect();
let gop_indices: Vec<u32> = frames.iter().map(|f| f.gop_idx).collect();
assert_eq!(gop_indices.iter().max(), Some(&2));
let idr_displays: Vec<u32> = frames
.iter()
.filter(|f| f.frame_type == FrameType::Idr)
.map(|f| f.display_idx)
.collect();
assert_eq!(idr_displays, vec![0, 5, 10]);
}
#[test]
fn encode_idx_is_monotonic_from_zero() {
let pat = GopPattern::Ibpbp { gop: 5, b_count: 1 };
let frames: Vec<_> = iter_encode_order(10, pat).collect();
for (i, f) in frames.iter().enumerate() {
assert_eq!(f.encode_idx as usize, i);
}
}
#[test]
fn display_indices_form_a_permutation() {
for &(gop, b_count) in &[(5usize, 1usize), (7, 2), (10, 1), (30, 1)] {
let pat = GopPattern::Ibpbp { gop, b_count };
for n in [gop, 2 * gop, 3 * gop, 5, 10, 15, 30] {
let frames: Vec<_> = iter_encode_order(n, pat).collect();
assert_eq!(frames.len(), n);
let mut display: Vec<u32> = frames.iter().map(|f| f.display_idx).collect();
display.sort();
let expected: Vec<u32> = (0..n as u32).collect();
assert_eq!(display, expected,
"display permutation broken for gop={gop} b_count={b_count} n={n}");
}
}
}
#[test]
fn iphone_default_is_m2() {
let pat = GopPattern::iphone_default();
assert!(pat.has_b_frames());
assert_eq!(pat.gop_size(), 30);
if let GopPattern::Ibpbp { gop, b_count } = pat {
assert_eq!(gop, 30);
assert_eq!(b_count, 1);
} else {
panic!("iphone_default should be Ibpbp");
}
}
#[test]
fn legacy_ipppp_has_no_b_frames() {
let pat = GopPattern::legacy_ipppp(10);
assert!(!pat.has_b_frames());
assert_eq!(pat.gop_size(), 10);
}
#[test]
fn auto_select_none_returns_centroid() {
let p = GopPattern::auto_select(None);
assert_eq!(p, GopPattern::Ibpbp { gop: 30, b_count: 1 });
}
#[test]
fn auto_select_garbage_returns_centroid() {
let p = GopPattern::auto_select(Some(b"this is not an mp4 file at all"));
assert_eq!(p, GopPattern::Ibpbp { gop: 30, b_count: 1 });
}
#[test]
fn auto_select_picks_ipppp_for_h264_source_without_ctts() {
let mp4 = build_h264_mp4_for_test( 5, false);
let p = GopPattern::auto_select(Some(&mp4));
assert_eq!(p, GopPattern::Ipppp { gop: 5 });
}
#[test]
fn auto_select_picks_ibpbp_for_h264_source_with_ctts() {
let mp4 = build_h264_mp4_for_test( 5, true);
let p = GopPattern::auto_select(Some(&mp4));
assert_eq!(p, GopPattern::Ibpbp { gop: 5, b_count: 1 });
}
fn build_h264_mp4_for_test(gop_size: usize, with_ctts: bool) -> Vec<u8> {
use crate::codec::mp4::build::{build_mp4, FrameTiming, MuxerProfile};
let mut annexb = Vec::new();
let n_frames = gop_size * 2;
for i in 0..n_frames {
annexb.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x10]);
if i.is_multiple_of(gop_size) {
annexb.extend_from_slice(&[
0, 0, 0, 1,
0x67, 0x64, 0x00, 0x1E, 0xAC, 0xD9, 0x40, 0x40, 0x3C, 0x80,
]);
annexb.extend_from_slice(&[0, 0, 0, 1, 0x68, 0xEB, 0xE3, 0xCB]);
annexb.extend_from_slice(&[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00]);
} else {
annexb.extend_from_slice(&[0, 0, 0, 1, 0x41, 0x9A, 0x00]);
}
}
let mut mp4 = build_mp4(
MuxerProfile::HandbrakeX264,
&annexb,
64,
64,
FrameTiming::FPS_30,
)
.expect("build_mp4");
if with_ctts {
let stub = b"\x00\x00\x00\x10ctts\x00\x00\x00\x00\x00\x00\x00\x00";
mp4.extend_from_slice(stub);
}
mp4
}
#[test]
fn b_frames_never_first_or_last_in_gop() {
for &(gop, b_count) in &[(5usize, 1usize), (7, 2), (10, 1)] {
let pat = GopPattern::Ibpbp { gop, b_count };
for d in [0, gop - 1, gop, 2 * gop - 1, 2 * gop] {
let ft = pat.frame_type_at(d);
assert_ne!(
ft,
FrameType::B,
"B at display_idx={d} for gop={gop}, b_count={b_count}",
);
}
}
}
}