diff --git a/Cargo.lock b/Cargo.lock index e397479..689a711 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -188,9 +188,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitstream-io" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d524fdb78bf6dc6d2dc4c02043e4b4962ede0a17ae3e13f0ed211a7eda5897" +checksum = "9d28070975aaf4ef1fd0bd1f29b739c06c2cdd9972e090617fb6dca3b2cb564e" [[package]] name = "bitvec" @@ -391,7 +391,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -417,13 +417,17 @@ name = "client" version = "0.0.0" dependencies = [ "anyhow", + "bitstream-io", "bytes", "clap 4.1.4", "futures", "itertools", "log", "mylog", + "pretty_assertions", "retina", + "test-case", + "thiserror", "tokio", "url", ] @@ -606,6 +610,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.98", +] + [[package]] name = "ctr" version = "0.6.0" @@ -658,7 +672,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.98", ] [[package]] @@ -669,7 +683,7 @@ checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -696,7 +710,7 @@ dependencies = [ "nom 6.1.2", "num-bigint", "num-traits", - "syn", + "syn 1.0.98", ] [[package]] @@ -707,7 +721,7 @@ checksum = "c73af209b6a5dc8ca7cbaba720732304792cddc933cfea3d74509c2b1ef2f436" dependencies = [ "num-bigint", "num-traits", - "syn", + "syn 1.0.98", ] [[package]] @@ -754,7 +768,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -764,9 +778,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" dependencies = [ "derive_builder_core", - "syn", + "syn 1.0.98", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -794,7 +814,7 @@ checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -981,7 +1001,7 @@ checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -1545,6 +1565,15 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "p256" version = "0.10.1" @@ -1612,7 +1641,7 @@ checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -1701,6 +1730,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1710,7 +1751,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.98", "version_check", ] @@ -1727,18 +1768,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -1884,10 +1925,12 @@ dependencies = [ "once_cell", "pin-project", "pretty-hex", + "pretty_assertions", "rand", "rtsp-types", "sdp-types", "smallvec", + "test-case", "thiserror", "time", "tokio", @@ -2139,7 +2182,7 @@ checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -2310,6 +2353,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -2318,7 +2372,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", "unicode-xid", ] @@ -2337,6 +2391,41 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-case" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679b019fb241da62cc449b33b224d19ebe1c6767b495569765115dd7f7f9fba4" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dc21b5887f4032c4656502d085dc28f2afbb686f25f216472bb0526f4b1b88" +dependencies = [ + "cfg-if", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.98", +] + +[[package]] +name = "test-case-macros" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3786898e0be151a96f730fd529b0e8a10f5990fa2a7ea14e37ca27613c05190" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.98", + "test-case-core", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -2345,22 +2434,22 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.10", ] [[package]] @@ -2427,7 +2516,7 @@ checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -2635,7 +2724,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 1.0.98", "wasm-bindgen-shared", ] @@ -2657,7 +2746,7 @@ checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3095,6 +3184,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yasna" version = "0.4.0" @@ -3121,6 +3216,6 @@ checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 51d2dc8..ddfd4d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ url = "2.2.1" [dev-dependencies] criterion = { version = "0.4.0", features = ["async_tokio"] } mylog = { git = "https://github.com/scottlamb/mylog" } +pretty_assertions = "1.3.0" +test-case = "3.0.0" tokio = { version = "1.5.0", features = ["io-util", "macros", "rt-multi-thread", "test-util"] } [profile.bench] diff --git a/src/client/timeline.rs b/src/client/timeline.rs index 4256dba..0fc6451 100644 --- a/src/client/timeline.rs +++ b/src/client/timeline.rs @@ -118,6 +118,7 @@ impl Timeline { Ok(( Timestamp { timestamp, + dts: None, clock_rate: self.clock_rate, start, }, diff --git a/src/codec/aac.rs b/src/codec/aac.rs index 067c16f..c613b96 100644 --- a/src/codec/aac.rs +++ b/src/codec/aac.rs @@ -805,6 +805,7 @@ mod tests { ).unwrap(); let timestamp = crate::Timestamp { timestamp: 42, + dts: None, clock_rate: NonZeroU32::new(48_000).unwrap(), start: 0, }; @@ -1001,6 +1002,7 @@ mod tests { ).unwrap(); let timestamp = crate::Timestamp { timestamp: 42, + dts: None, clock_rate: NonZeroU32::new(48_000).unwrap(), start: 0, }; @@ -1105,6 +1107,7 @@ mod tests { ).unwrap(); let timestamp = crate::Timestamp { timestamp: 42, + dts: None, clock_rate: NonZeroU32::new(48_000).unwrap(), start: 0, }; @@ -1211,6 +1214,7 @@ mod tests { ).unwrap(); let timestamp = crate::Timestamp { timestamp: 42, + dts: None, clock_rate: NonZeroU32::new(48_000).unwrap(), start: 0, }; @@ -1320,6 +1324,7 @@ mod tests { ).unwrap(); let timestamp = crate::Timestamp { timestamp: 42, + dts: None, clock_rate: NonZeroU32::new(48_000).unwrap(), start: 0, }; diff --git a/src/codec/h264.rs b/src/codec/h264.rs index 1e5e71f..5f4216a 100644 --- a/src/codec/h264.rs +++ b/src/codec/h264.rs @@ -3,6 +3,10 @@ //! [H.264](https://www.itu.int/rec/T-REC-H.264-201906-I/en)-encoded video. +pub mod dts_extractor; +pub mod emulation_prevention; +pub mod sps; + use std::convert::TryFrom; use std::fmt::Write; @@ -11,6 +15,7 @@ use h264_reader::nal::{NalHeader, UnitType}; use log::{debug, log_enabled, trace}; use crate::{ + codec::h264::{dts_extractor::DtsExtractor, sps::Sps}, rtp::{ReceivedPacket, ReceivedPacketBuilder}, Error, Timestamp, }; @@ -43,6 +48,9 @@ pub(crate) struct Depacketizer { /// In state `PreMark`, an entry for each NAL. /// Kept around (empty) in other states to re-use the backing allocation. nals: Vec, + + sps: Option, + dts_extractor: Option, } #[derive(Debug)] @@ -107,13 +115,16 @@ impl Depacketizer { )); } - let parameters = match format_specific_params { - None => None, + let (parameters, sps) = match format_specific_params { + None => (None, None), Some(fp) => match InternalParameters::parse_format_specific_params(fp) { - Ok(p) => Some(p), + Ok(p) => { + let sps = Sps::deserialize(&p.sps_nal).map_err(|e| e.to_string())?; + (Some(p), Some(sps)) + } Err(e) => { log::warn!("Ignoring bad H.264 format-specific-params {:?}: {}", fp, e); - None + (None, None) } }, }; @@ -123,6 +134,8 @@ impl Depacketizer { pieces: Vec::new(), nals: Vec::new(), parameters, + sps, + dts_extractor: None, }) } @@ -425,7 +438,11 @@ impl Depacketizer { } } - fn finalize_access_unit(&mut self, au: AccessUnit, reason: &str) -> Result { + fn finalize_access_unit( + &mut self, + mut au: AccessUnit, + reason: &str, + ) -> Result { let mut piece_idx = 0; let mut retained_len = 0usize; let mut is_random_access_point = false; @@ -472,11 +489,23 @@ impl Depacketizer { } let mut data = Vec::with_capacity(retained_len); piece_idx = 0; + + let mut nalus: Vec> = Vec::new(); + for nal in &self.nals { + let hdr: u8 = nal.hdr.into(); let next_piece_idx = usize::try_from(nal.next_piece_idx).expect("u32 fits in usize"); let nal_pieces = &self.pieces[piece_idx..next_piece_idx]; + + let mut nalu = Vec::new(); + nalu.push(hdr.to_owned()); + for piece in nal_pieces { + nalu.extend_from_slice(&piece[..]); + } + nalus.push(nalu); + data.extend_from_slice(&nal.len.to_be_bytes()[..]); - data.push(nal.hdr.into()); + data.push(hdr); let mut actual_len = 1; for piece in nal_pieces { data.extend_from_slice(&piece[..]); @@ -488,6 +517,30 @@ impl Depacketizer { ); piece_idx = next_piece_idx; } + + if let Some(sps) = &self.sps { + let pts = au.timestamp.timestamp(); + + // If first sync sample has been received. + if let Some(dts_extractor) = &mut self.dts_extractor { + au.timestamp.dts = Some( + dts_extractor + .extract(sps, &nalus, pts) + .map_err(|e| e.to_string())?, + ); + + // Skip samples silently until we find one with an IDR. + } else if is_random_access_point { + let mut dts_extractor = DtsExtractor::new(); + dts_extractor + .extract(sps, &nalus, 0) + .map_err(|e| e.to_string())?; + self.dts_extractor = Some(dts_extractor); + + au.timestamp.dts = Some(0); + } + } + debug_assert_eq!(retained_len, data.len()); self.nals.clear(); self.pieces.clear(); @@ -511,6 +564,7 @@ impl Depacketizer { } _ => false, }; + Ok(VideoFrame { has_new_parameters, loss: au.loss, @@ -997,6 +1051,7 @@ enum PacketizerState { #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use std::num::NonZeroU32; use crate::testutil::init_logging; @@ -1062,6 +1117,7 @@ mod tests { let mut d = super::Depacketizer::new(90_000, Some("packetization-mode=1;profile-level-id=64001E;sprop-parameter-sets=Z2QAHqwsaoLA9puCgIKgAAADACAAAAMD0IAA,aO4xshsA")).unwrap(); let timestamp = crate::Timestamp { timestamp: 0, + dts: None, clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; @@ -1171,11 +1227,13 @@ mod tests { let mut d = super::Depacketizer::new(90_000, Some("packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKCgL/lQ,aO48sA==")).unwrap(); let ts1 = crate::Timestamp { timestamp: 0, + dts: None, clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; let ts2 = crate::Timestamp { timestamp: 1, + dts: Some(0), clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; @@ -1255,11 +1313,13 @@ mod tests { let mut d = super::Depacketizer::new(90_000, Some("packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKCgL/lQ,aO48sA==")).unwrap(); let ts1 = crate::Timestamp { timestamp: 0, + dts: None, clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; let ts2 = crate::Timestamp { timestamp: 1, + dts: Some(0), clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; @@ -1361,6 +1421,7 @@ mod tests { } let timestamp = crate::Timestamp { timestamp: 0, + dts: None, clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; @@ -1469,6 +1530,7 @@ mod tests { // The stream should honor in-band parameters. let timestamp = crate::Timestamp { timestamp: 0, + dts: None, clock_rate: NonZeroU32::new(90_000).unwrap(), start: 0, }; diff --git a/src/codec/h264/dts_extractor.rs b/src/codec/h264/dts_extractor.rs new file mode 100644 index 0000000..763d760 --- /dev/null +++ b/src/codec/h264/dts_extractor.rs @@ -0,0 +1,403 @@ +// https://github.com/bluenviron/gortsplib/blob/f0540b4eee760583b2d94f022674b0f3b0f8c8b0/pkg/codecs/h264/dts_extractor.go + +use crate::codec::h264::{ + emulation_prevention::emulation_prevention_remove, + sps::{self, Sps}, +}; +use bitstream_io::{BitRead, BitReader}; +use h264_reader::nal::UnitType; +use std::io::Cursor; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DtsExtractorError { + #[error("{0}")] + FromInt(#[from] std::num::TryFromIntError), + + #[error("frame_mbs_only_flag = 0 is not supported")] + FrameMbsNotSupported, + + #[error("POC not found")] + PocNotFound, + + #[error("invalid POC")] + PocInvalid, + + #[error("{0}")] + SpsUnmarshal(#[from] sps::Error), + + #[error("SPS not received yet")] + SpsNotReceivedYet, + + #[error("pic_order_cnt_type = 1 is not supported yet")] + PicOrderCntType1Unsupported, + + #[error("DTS is greater than PTS: '{0}' vs '{1}'")] + DtsGreaterThanPts(i64, i64), + + #[error("DTS is not monotonically increasing, was {0}, now is {1}")] + DtsNotIncreasing(i64, i64), + + #[error("read: {0}")] + Read(#[from] std::io::Error), + + #[error("unit type value {0} out of range")] + UnitType(u8), +} + +fn get_picture_order_count(sps: &Sps, buf: &[u8]) -> Result { + let buf = emulation_prevention_remove(&buf[..6]); + let mut r = BitReader::new(Cursor::new(buf)); + + r.skip(8)?; + + sps::read_golomb_unsigned(&mut r)?; // first_mb_in_slice + sps::read_golomb_unsigned(&mut r)?; // slice_type + sps::read_golomb_unsigned(&mut r)?; // pic_parameter_set_id + r.skip(sps.log2_max_frame_num_minus4 + 4)?; // frame_num + + if !sps.frame_mbs_only_flag { + return Err(DtsExtractorError::FrameMbsNotSupported); + } + + let pic_order_cnt_lsb: u32 = r.read(sps.log2_max_pic_order_cnt_lsb_minus4 + 4)?; + + Ok(pic_order_cnt_lsb) +} + +fn find_picture_order_count(sps: &Sps, au: &[Vec]) -> Result { + for nalu in au { + let typ = nalu[0] & 0b00011111; + let typ = UnitType::for_id(typ).map_err(|_| DtsExtractorError::UnitType(typ))?; + if typ == UnitType::SliceLayerWithoutPartitioningNonIdr { + let poc = get_picture_order_count(sps, nalu)?; + return Ok(poc); + } + } + Err(DtsExtractorError::PocNotFound) +} + +fn get_picture_order_count_diff( + sps: &Sps, + poc1: u32, + poc2: u32, +) -> Result { + let diff = i32::try_from(poc1)? - i32::try_from(poc2)?; + if diff < -((1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 3)) - 1) { + Ok(diff + (1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 4))) + } else if diff > ((1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 3)) - 1) { + Ok(diff - (1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 4))) + } else { + Ok(diff) + } +} + +// Allows to extract DTS from PTS. +#[derive(Debug)] +pub struct DtsExtractor { + prev_dts_filled: bool, + prev_dts: i64, + expected_poc: u32, + reordered_frames: isize, + pause_dts: isize, + poc_increment: isize, +} + +impl Default for DtsExtractor { + fn default() -> Self { + Self::new() + } +} + +impl DtsExtractor { + pub fn new() -> Self { + Self { + poc_increment: 2, + prev_dts_filled: false, + prev_dts: 0, + expected_poc: 0, + reordered_frames: 0, + pause_dts: 0, + } + } + + // Extracts the DTS of a access unit. + pub fn extract( + &mut self, + sps: &Sps, + au: &[Vec], + pts: i64, + ) -> Result { + let dts = self.extract_inner(sps, au, pts)?; + if dts > pts { + return Err(DtsExtractorError::DtsGreaterThanPts(dts, pts)); + } + + if self.prev_dts_filled && dts <= self.prev_dts { + return Err(DtsExtractorError::DtsNotIncreasing(self.prev_dts, dts)); + } + + self.prev_dts = dts; + self.prev_dts_filled = true; + Ok(dts) + } + + fn extract_inner( + &mut self, + sps: &Sps, + au: &[Vec], + pts: i64, + ) -> Result { + let mut idr_present = false; + + for nalu in au { + let typ = nalu[0] & 0b00011111; + let typ = UnitType::for_id(typ).map_err(|_| DtsExtractorError::UnitType(typ))?; + if typ == UnitType::SliceLayerWithoutPartitioningIdr { + idr_present = true + } + } + + if sps.pic_order_cnt_type == 2 { + return Ok(pts); + } + + if sps.pic_order_cnt_type == 1 { + return Err(DtsExtractorError::PicOrderCntType1Unsupported); + } + + if idr_present { + self.expected_poc = 0; + self.reordered_frames = 0; + self.pause_dts = 0; + self.poc_increment = 2; + return Ok(pts); + } + + self.expected_poc += u32::try_from(self.poc_increment)?; + self.expected_poc &= (1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 4)) - 1; + + if self.pause_dts > 0 { + self.pause_dts -= 1; + return Ok(self.prev_dts + 1); + } + + let poc = find_picture_order_count(sps, au)?; + + if self.poc_increment == 2 && (poc % 2) != 0 { + self.poc_increment = 1; + self.expected_poc /= 2; + } + + let poc_diff = isize::try_from(get_picture_order_count_diff(sps, poc, self.expected_poc)?)? + + self.reordered_frames * self.poc_increment; + + if poc_diff < 0 { + return Err(DtsExtractorError::PocInvalid); + } + + if poc_diff == 0 { + return Ok(pts); + } + + let reordered_frames = + (poc_diff - self.reordered_frames * self.poc_increment) / self.poc_increment; + if reordered_frames > self.reordered_frames { + self.pause_dts = reordered_frames - self.reordered_frames - 1; + self.reordered_frames = reordered_frames; + return Ok(self.prev_dts + 1); + } + + Ok(self.prev_dts + + ((pts - self.prev_dts) * i64::try_from(self.poc_increment)? + / i64::try_from(poc_diff + self.poc_increment)?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + struct SequenceSample { + nalus: Vec>, + dts: i64, + pts: i64, + } + + #[test_case( + &[ + 0x67, 0x64, 0x00, 0x28, 0xac, 0xd9, 0x40, 0x78, + 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, + 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, + 0xc6, 0x58, + ], + vec![ + SequenceSample{ + nalus: vec![ + vec![ // IDR + 0x65, 0x88, 0x84, 0x00, 0x33, 0xff, + ], + ], + dts: 3333333, + pts: 3333333, + }, + SequenceSample{ + nalus: vec![vec![0x41, 0x9a, 0x21, 0x6c, 0x45, 0xff]], + dts: 3336666, + pts: 3336666, + }, + SequenceSample{ + nalus: vec![vec![0x41, 0x9a, 0x42, 0x3c, 0x21, 0x93]], + dts: 3340000, + pts: 3340000, + }, + SequenceSample{ + nalus: vec![vec![0x41, 0x9a, 0x63, 0x49, 0xe1, 0x0f]], + dts: 3343333, + pts: 3343333, + }, + SequenceSample{ + nalus: vec![vec![0x41, 0x9a, 0x86, 0x49, 0xe1, 0x0f]], + dts: 3343334, + pts: 3353333, + }, + SequenceSample{ + nalus: vec![vec![0x41, 0x9e, 0xa5, 0x42, 0x7f, 0xf9]], + dts: 3343335, + pts: 3350000, + }, + SequenceSample{ + nalus: vec![vec![0x01, 0x9e, 0xc4, 0x69, 0x13, 0xff]], + dts: 3346666, + pts: 3346666, + }, + SequenceSample{ + nalus: vec![vec![0x41, 0x9a, 0xc8, 0x4b, 0xa8, 0x42]], + dts: 3349999, + pts: 3360000, + }, + ]; "with timing info" + )] + #[test_case( + &[ + 0x27, 0x64, 0x00, 0x20, 0xac, 0x52, 0x18, 0x0f, + 0x01, 0x17, 0xef, 0xff, 0x00, 0x01, 0x00, 0x01, + 0x6a, 0x02, 0x02, 0x03, 0x6d, 0x85, 0x6b, 0xde, + 0xf8, 0x08, + ], + vec![ + SequenceSample{ + nalus: vec![ + vec![ // IDR + 0x25, 0xb8, 0x08, 0x02, 0x1f, 0xff, + ], + ], + dts: 85000, + pts: 85000, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe1, 0x05, 0xc7, 0x38, 0xbf]], + dts: 86666, + pts: 86666, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe2, 0x09, 0xa1, 0xce, 0x0b]], + dts: 88333, + pts: 88333, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe3, 0x0d, 0xb1, 0xce, 0x02]], + dts: 90000, + pts: 90000, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe4, 0x11, 0x90, 0x73, 0x80]], + dts: 91666, + pts: 91666, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe5, 0x19, 0x0e, 0x70, 0x01]], + dts: 91667, + pts: 95000, + }, + SequenceSample{ + nalus: vec![vec![0x01, 0xa9, 0x85, 0x7c, 0x93, 0xff]], + dts: 93333, + pts: 93333, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe6, 0x1d, 0x0e, 0x70, 0x01]], + dts: 94999, + pts: 96666, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe7, 0x21, 0x0e, 0x70, 0x01]], + dts: 96666, + pts: 98333, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe8, 0x25, 0x0e, 0x70, 0x01]], + dts: 98333, + pts: 100000, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xe9, 0x29, 0x0e, 0x70, 0x01]], + dts: 99999, + pts: 101666, + }, + SequenceSample{ + nalus: vec![vec![0x21, 0xea, 0x31, 0x0e, 0x70, 0x01]], + dts: 101666, + pts: 105000, + }, + SequenceSample{ + nalus: vec![vec![0x01, 0xaa, 0xcb, 0x7c, 0x93, 0xff]], + dts: 103333, + pts: 103333, + }, + ]; "no timing info" + )] + #[test_case( + &[ + 0x67, 0x64, 0x00, 0x2a, 0xac, 0x2c, 0x6a, 0x81, + 0xe0, 0x08, 0x9f, 0x96, 0x6e, 0x02, 0x02, 0x02, + 0x80, 0x00, 0x03, 0x84, 0x00, 0x00, 0xaf, 0xc8, + 0x02, + ], + vec![ + SequenceSample{ + nalus: vec![ + vec![ // IDR + 0x65, 0xb8, 0x00, 0x00, 0x0b, 0xc8, + ], + ], + dts: 61, + pts: 61, + }, + SequenceSample{ + nalus: vec![vec![0x61, 0xe0, 0x20, 0x00, 0x39, 0x37]], + dts: 101, + pts: 101, + }, + SequenceSample{ + nalus: vec![vec![0x61, 0xe0, 0x40, 0x00, 0x59, 0x37]], + dts: 141, + pts: 141, + }, + SequenceSample{ + nalus: vec![vec![0x61, 0xe0, 0x60, 0x00, 0x79, 0x37]], + dts: 181, + pts: 181, + }, + ]; "poc increment = 1" + )] + fn test_dts_extractor(sps: &[u8], sequence: Vec) { + let sps = Sps::deserialize(sps).unwrap(); + let mut ex = DtsExtractor::new(); + for sample in sequence { + let dts = ex.extract(&sps, &sample.nalus, sample.pts).unwrap(); + assert_eq!(sample.dts, dts); + } + } +} diff --git a/src/codec/h264/emulation_prevention.rs b/src/codec/h264/emulation_prevention.rs new file mode 100644 index 0000000..c86e658 --- /dev/null +++ b/src/codec/h264/emulation_prevention.rs @@ -0,0 +1,62 @@ +// Removes emulation prevention bytes from a NALU. +pub fn emulation_prevention_remove(nalu: &[u8]) -> Vec { + // 0x00 0x00 0x03 0x00 -> 0x00 0x00 0x00 + // 0x00 0x00 0x03 0x01 -> 0x00 0x00 0x01 + // 0x00 0x00 0x03 0x02 -> 0x00 0x00 0x02 + // 0x00 0x00 0x03 0x03 -> 0x00 0x00 0x03 + + let len = nalu.len(); + let mut ret_len = len; + for i in 2..len { + if nalu[i - 2] == 0 && nalu[i - 1] == 0 && nalu[i] == 3 { + ret_len -= 1; + } + } + + let mut ret = Vec::with_capacity(ret_len); + let mut start = 0; + for i in 2..len { + if nalu[i - 2] == 0 && nalu[i - 1] == 0 && nalu[i] == 3 { + ret.extend_from_slice(&nalu[start..i]); + start = i + 1; + } + } + + ret.extend_from_slice(&nalu[start..]); + ret +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + #[test_case( + &[ + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x02, + 0x00, 0x00, 0x03, + ], + &[ + 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x03, 0x01, + 0x00, 0x00, 0x03, 0x02, + 0x00, 0x00, 0x03, 0x03, + ]; "base" + )] + #[test_case( + &[ + 0x00, 0x00, 0x00, + 0x00, 0x00, + ], + &[ + 0x00, 0x00, 0x03, + 0x00, 0x00, 0x03, 0x00, + ]; "double emulation byte" + )] + #[test_case(&[0x00, 0x00], &[0x00, 0x00, 0x03]; "terminal emulation byte")] + fn test_emulation_prevention_remove(unproc: &[u8], proc: &[u8]) { + assert_eq!(unproc, emulation_prevention_remove(proc)) + } +} diff --git a/src/codec/h264/sps.rs b/src/codec/h264/sps.rs new file mode 100644 index 0000000..9154a2c --- /dev/null +++ b/src/codec/h264/sps.rs @@ -0,0 +1,1200 @@ +use crate::codec::h264::emulation_prevention::emulation_prevention_remove; +use bitstream_io::BitRead; +use bitstream_io::{BigEndian, BitReader}; +use std::io::Cursor; +use thiserror::Error; + +const MAX_REF_FRAMES: u32 = 255; + +#[derive(Debug, Error)] +pub enum Error { + #[error("not enough bits")] + NotEnoughBits, + + #[error("num_ref_frames_in_pic_order_cnt_cycle exceeds {}", MAX_REF_FRAMES)] + TooManyRefFrames, + + #[error("invalid pic_order_cnt_type: {0}")] + InvalidPicOrderCntType(u32), + + #[error("{0}")] + FromInt(#[from] std::num::TryFromIntError), + + #[error("read: {0}")] + Read(#[from] std::io::Error), +} + +// H264 sequence parameter set. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Sps { + pub profile_idc: u8, + pub constraint_set0_flag: bool, + pub constraint_set1_flag: bool, + pub constraint_set2_flag: bool, + pub constraint_set3_flag: bool, + pub constraint_set4_flag: bool, + pub constraint_set5_flag: bool, + pub level_idc: u8, + pub id: u32, + + pub chrome_format_idc: u32, + pub separate_colour_plane_flag: bool, + pub bit_depth_luma_minus8: u32, + pub bit_depth_chroma_minus8: u32, + pub qpprime_yzero_transform_bypass_flag: bool, + pub seq_scaling_list: Option, + + pub log2_max_frame_num_minus4: u32, + pub pic_order_cnt_type: u32, + pub log2_max_pic_order_cnt_lsb_minus4: u32, + + pub delta_pic_order_always_zero_flag: bool, + pub offset_for_non_ref_pic: i32, + pub offset_for_top_to_bottom_field: i32, + pub offset_for_ref_frames: Vec, + + pub max_num_ref_frames: u32, + pub gaps_in_frame_num_value_allowed_flag: bool, + pub pic_width_in_mbs_minus1: u32, + pub pic_height_in_map_units_minus1: u32, + pub frame_mbs_only_flag: bool, + + pub mb_adaptive_frame_field_flag: bool, + + pub direct_8x8_inference_flag: bool, + pub frame_cropping: Option, + pub vui: Option, +} + +impl Sps { + pub fn deserialize(buf: &[u8]) -> Result { + let buf = emulation_prevention_remove(buf); + let mut r = BitReader::new(Cursor::new(buf)); + + r.skip(8)?; + let profile_idc = r.read(8)?; + + let constraint_set0_flag = r.read_bit()?; + let constraint_set1_flag = r.read_bit()?; + let constraint_set2_flag = r.read_bit()?; + let constraint_set3_flag = r.read_bit()?; + let constraint_set4_flag = r.read_bit()?; + let constraint_set5_flag = r.read_bit()?; + r.skip(2)?; + + let level_idc = r.read(8)?; + + let id = read_golomb_unsigned(&mut r)?; + + let chrome_format_idc; + let separate_colour_plane_flag; + let bit_depth_luma_minus8; + let bit_depth_chroma_minus8; + let qpprime_yzero_transform_bypass_flag; + let mut seq_scaling_list = None; + + match profile_idc { + 100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135 => { + chrome_format_idc = read_golomb_unsigned(&mut r)?; + + separate_colour_plane_flag = if chrome_format_idc == 3 { + r.read_bit()? + } else { + false + }; + + bit_depth_luma_minus8 = read_golomb_unsigned(&mut r)?; + bit_depth_chroma_minus8 = read_golomb_unsigned(&mut r)?; + qpprime_yzero_transform_bypass_flag = r.read_bit()?; + + let seq_scaling_matrix_present_flag = r.read_bit()?; + if seq_scaling_matrix_present_flag { + let lim = if chrome_format_idc == 3 { 12 } else { 8 }; + seq_scaling_list = Some(SpsSeqScalingList::from_reader(&mut r, lim)?); + } + } + _ => { + chrome_format_idc = 0; + separate_colour_plane_flag = false; + bit_depth_luma_minus8 = 0; + bit_depth_chroma_minus8 = 0; + qpprime_yzero_transform_bypass_flag = false; + } + } + + let log2_max_frame_num_minus4 = read_golomb_unsigned(&mut r)?; + let pic_order_cnt_type = read_golomb_unsigned(&mut r)?; + + let log2_max_pic_order_cnt_lsb_minus4; + let delta_pic_order_always_zero_flag; + let offset_for_non_ref_pic; + let offset_for_top_to_bottom_field; + let mut offset_for_ref_frames; + + match pic_order_cnt_type { + 0 => { + log2_max_pic_order_cnt_lsb_minus4 = read_golomb_unsigned(&mut r)?; + delta_pic_order_always_zero_flag = false; + offset_for_non_ref_pic = 0; + offset_for_top_to_bottom_field = 0; + offset_for_ref_frames = Vec::new(); + } + 1 => { + log2_max_pic_order_cnt_lsb_minus4 = 0; + delta_pic_order_always_zero_flag = r.read_bit()?; + offset_for_non_ref_pic = read_golomb_signed(&mut r)?; + offset_for_top_to_bottom_field = read_golomb_signed(&mut r)?; + + let num_ref_frames_in_pic_order_cnt_cycle = read_golomb_unsigned(&mut r)?; + if num_ref_frames_in_pic_order_cnt_cycle > MAX_REF_FRAMES { + return Err(Error::TooManyRefFrames); + } + + offset_for_ref_frames = + Vec::with_capacity(usize::try_from(num_ref_frames_in_pic_order_cnt_cycle)?); + + for i in 0..num_ref_frames_in_pic_order_cnt_cycle { + let v = read_golomb_signed(&mut r)?; + offset_for_ref_frames[usize::try_from(i)?] = v; + } + } + 2 => { + log2_max_pic_order_cnt_lsb_minus4 = 0; + delta_pic_order_always_zero_flag = false; + offset_for_non_ref_pic = 0; + offset_for_top_to_bottom_field = 0; + offset_for_ref_frames = Vec::new(); + } + _ => { + return Err(Error::InvalidPicOrderCntType(pic_order_cnt_type)); + } + } + + let max_num_ref_frames = read_golomb_unsigned(&mut r)?; + let gaps_in_frame_num_value_allowed_flag = r.read_bit()?; + let pic_width_in_mbs_minus1 = read_golomb_unsigned(&mut r)?; + let pic_height_in_map_units_minus1 = read_golomb_unsigned(&mut r)?; + let frame_mbs_only_flag = r.read_bit()?; + + let mb_adaptive_frame_field_flag = if frame_mbs_only_flag { + false + } else { + r.read_bit()? + }; + + let direct_8x8_inference_flag = r.read_bit()?; + + let mut frame_cropping = None; + let frame_cropping_flag = r.read_bit()?; + if frame_cropping_flag { + frame_cropping = Some(SpsFrameCropping::from_reader(&mut r)?); + } + + let mut vui = None; + let vui_parameters_present_flag = r.read_bit()?; + if vui_parameters_present_flag { + vui = Some(SpsVui::from_reader(&mut r)?); + } + + Ok(Self { + profile_idc, + constraint_set0_flag, + constraint_set1_flag, + constraint_set2_flag, + constraint_set3_flag, + constraint_set4_flag, + constraint_set5_flag, + level_idc, + id, + chrome_format_idc, + separate_colour_plane_flag, + bit_depth_luma_minus8, + bit_depth_chroma_minus8, + qpprime_yzero_transform_bypass_flag, + seq_scaling_list, + log2_max_frame_num_minus4, + pic_order_cnt_type, + log2_max_pic_order_cnt_lsb_minus4, + delta_pic_order_always_zero_flag, + offset_for_non_ref_pic, + offset_for_top_to_bottom_field, + offset_for_ref_frames, + max_num_ref_frames, + gaps_in_frame_num_value_allowed_flag, + pic_width_in_mbs_minus1, + pic_height_in_map_units_minus1, + frame_mbs_only_flag, + mb_adaptive_frame_field_flag, + direct_8x8_inference_flag, + frame_cropping, + vui, + }) + } + + #[cfg(test)] + fn width(&self) -> Result { + let Some(frame_cropping) = &self.frame_cropping else { + return usize::try_from((self.pic_width_in_mbs_minus1 + 1) * 16); + }; + + usize::try_from( + ((self.pic_width_in_mbs_minus1 + 1) * 16) + - (frame_cropping.left_offset + frame_cropping.right_offset) * 2, + ) + } + + #[cfg(test)] + fn height(&self) -> Result { + let f = u32::from(self.frame_mbs_only_flag); + + let Some(frame_cropping) = &self.frame_cropping else { + return usize::try_from((2 - f) * (self.pic_height_in_map_units_minus1 + 1) * 16); + }; + + usize::try_from( + ((2 - f) * (self.pic_height_in_map_units_minus1 + 1) * 16) + - (frame_cropping.top_offset + frame_cropping.bottom_offset) * 2, + ) + } + + #[cfg(test)] + fn fps(&self) -> Option { + let Some(vui) = &self.vui else { + return None; + }; + let Some(timing_info) = &vui.timing_info else { + return None; + }; + + Some((timing_info.time_scale as f64) / (2.0 * (timing_info.num_units_in_tick as f64))) + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct SpsSeqScalingList { + pub scaling_list_4x4: Vec>, + pub use_default_scaling_matrix_4x4_flag: Vec, + pub scaling_list_8x8: Vec>, + pub use_default_scaling_matrix_8x8_flag: Vec, +} + +impl SpsSeqScalingList { + fn from_reader( + r: &mut BitReader, + lim: usize, + ) -> Result { + let mut list = Self::default(); + + for i in 0..lim { + let seq_scaling_list_present_flag = r.read_bit()?; + + if seq_scaling_list_present_flag { + if i < 6 { + let (scaling_list, use_default_scaling_matrix_flag) = read_scaling_list(r, 16)?; + + list.scaling_list_4x4.push(scaling_list); + list.use_default_scaling_matrix_4x4_flag + .push(use_default_scaling_matrix_flag); + } else { + let (scaling_list, use_default_scaling_matrix_flag) = read_scaling_list(r, 64)?; + + list.scaling_list_8x8.push(scaling_list); + list.use_default_scaling_matrix_8x8_flag + .push(use_default_scaling_matrix_flag); + } + } + } + Ok(list) + } +} + +// Hypotetical reference decoder. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct SpsHrd { + pub cpb_cnt_minus1: u32, + pub bit_rate_scale: u8, + pub cpb_size_scale: u8, + pub bit_rate_value_minus1: Vec, + pub cpb_size_value_minus1: Vec, + pub cbr_flag: Vec, + pub initial_cpb_removal_delay_length_minus1: u8, + pub cpb_removal_delay_length_minus1: u8, + pub dpb_output_delay_length_minus1: u8, + pub time_offset_length: u8, +} + +impl SpsHrd { + fn from_reader(r: &mut BitReader) -> Result { + let cpb_cnt_minus1 = read_golomb_unsigned(r)?; + let bit_rate_scale = r.read(4)?; + let cpb_size_scale = r.read(4)?; + + let mut bit_rate_value_minus1 = Vec::new(); + let mut cpb_size_value_minus1 = Vec::new(); + let mut cbr_flag = Vec::new(); + for _ in 0..=cpb_cnt_minus1 { + bit_rate_value_minus1.push(read_golomb_unsigned(r)?); + cpb_size_value_minus1.push(read_golomb_unsigned(r)?); + cbr_flag.push(r.read_bit()?); + } + + let initial_cpb_removal_delay_length_minus1 = r.read(5)?; + let cpb_removal_delay_length_minus1 = r.read(5)?; + let dpb_output_delay_length_minus1 = r.read(5)?; + let time_offset_length = r.read(5)?; + + Ok(Self { + cpb_cnt_minus1, + bit_rate_scale, + cpb_size_scale, + bit_rate_value_minus1, + cpb_size_value_minus1, + cbr_flag, + initial_cpb_removal_delay_length_minus1, + cpb_removal_delay_length_minus1, + dpb_output_delay_length_minus1, + time_offset_length, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SpsTimingInfo { + pub time_scale: u32, + pub num_units_in_tick: u32, + pub fixed_frame_rate_flag: bool, +} + +impl SpsTimingInfo { + fn from_reader(r: &mut BitReader) -> Result { + let num_units_in_tick = r.read(32)?; + let time_scale = r.read(32)?; + let fixed_frame_rate_flag = r.read_bit()?; + Ok(Self { + num_units_in_tick, + time_scale, + fixed_frame_rate_flag, + }) + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct SpsBitstreamRestriction { + pub motion_vectors_over_pic_boundaries_flag: bool, + pub max_bytes_per_pic_denom: u32, + pub max_bits_per_mb_denom: u32, + pub log2_max_mv_length_horizontal: u32, + pub log2_max_mv_length_vertical: u32, + pub max_num_reorder_frames: u32, + pub max_dec_frame_buffering: u32, +} + +impl SpsBitstreamRestriction { + fn from_reader(r: &mut BitReader) -> Result { + let motion_vectors_over_pic_boundaries_flag = r.read_bit()?; + let max_bytes_per_pic_denom = read_golomb_unsigned(r)?; + let max_bits_per_mb_denom = read_golomb_unsigned(r)?; + let log2_max_mv_length_horizontal = read_golomb_unsigned(r)?; + let log2_max_mv_length_vertical = read_golomb_unsigned(r)?; + let max_num_reorder_frames = read_golomb_unsigned(r)?; + let max_dec_frame_buffering = read_golomb_unsigned(r)?; + + Ok(Self { + motion_vectors_over_pic_boundaries_flag, + max_bits_per_mb_denom, + max_num_reorder_frames, + log2_max_mv_length_horizontal, + log2_max_mv_length_vertical, + max_bytes_per_pic_denom, + max_dec_frame_buffering, + }) + } +} + +// Video usability information. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct SpsVui { + pub aspect_ratio_info: Option, + pub video_signal_type: Option, + pub chrome_log_info: Option, + pub timing_info: Option, + pub nal_hrd: Option, + pub vcl_hrd: Option, + pub bitstream_restriction: Option, +} + +impl SpsVui { + fn from_reader(r: &mut BitReader) -> Result { + let mut aspect_ratio_info = None; + let aspect_ratio_info_present_flag = r.read_bit()?; + if aspect_ratio_info_present_flag { + aspect_ratio_info = Some(SpsAspectRatioInfo::from_reader(r)?); + } + + let overscan_info_present_flag = r.read_bit()?; + if overscan_info_present_flag { + r.read_bit()?; + } + + let mut video_signal_type = None; + let video_signal_type_present_flag = r.read_bit()?; + if video_signal_type_present_flag { + video_signal_type = Some(SpsVideoSignalType::from_reader(r)?); + } + + let mut chrome_log_info = None; + let chroma_loc_info_present_flag = r.read_bit()?; + if chroma_loc_info_present_flag { + chrome_log_info = Some(SpsChromaLocInfo::from_reader(r)?) + } + + let mut timing_info = None; + let timing_info_present_flag = r.read_bit()?; + if timing_info_present_flag { + timing_info = Some(SpsTimingInfo::from_reader(r)?); + } + + let mut nal_hrd = None; + let nal_hrd_parameters_present_flag = r.read_bit()?; + if nal_hrd_parameters_present_flag { + nal_hrd = Some(SpsHrd::from_reader(r)?); + } + + let mut vcl_hrd = None; + let vcl_hrd_parameters_present_flag = r.read_bit()?; + if vcl_hrd_parameters_present_flag { + vcl_hrd = Some(SpsHrd::from_reader(r)?); + } + + if nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag { + r.read_bit()?; // Low delay hrd flag. + } + + r.read_bit()?; // Pic struct preset flag. + + let mut bitstream_restriction = None; + let bitstream_restriction_flag = r.read_bit()?; + if bitstream_restriction_flag { + bitstream_restriction = Some(SpsBitstreamRestriction::from_reader(r)?); + } + + Ok(SpsVui { + aspect_ratio_info, + video_signal_type, + chrome_log_info, + timing_info, + nal_hrd, + vcl_hrd, + bitstream_restriction, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SpsAspectRatioInfo { + pub aspect_ratio_idc: u8, + pub sar_width: u16, + pub sar_height: u16, +} + +impl SpsAspectRatioInfo { + fn from_reader(r: &mut BitReader) -> Result { + let aspect_ratio_idc = r.read(8)?; + let mut sar_width = 0; + let mut sar_height = 0; + + // Extended_SAR + if aspect_ratio_idc == 255 { + sar_width = r.read(16)?; + sar_height = r.read(16)?; + } + + Ok(Self { + aspect_ratio_idc, + sar_width, + sar_height, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SpsVideoSignalType { + pub video_format: u8, + pub colour_description: Option, +} + +impl SpsVideoSignalType { + fn from_reader(r: &mut BitReader) -> Result { + let video_format = r.read(3)?; + r.read_bit()?; // Video full range flag. + + let colour_description_present_flag = r.read_bit()?; + let colour_description = if colour_description_present_flag { + Some(SpsColourDescription::from_reader(r)?) + } else { + None + }; + + Ok(Self { + video_format, + colour_description, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SpsColourDescription { + pub colour_primaries: u8, + pub transfer_characteristics: u8, + pub matrix_coefficients: u8, +} + +impl SpsColourDescription { + fn from_reader(r: &mut BitReader) -> Result { + Ok(Self { + colour_primaries: r.read(8)?, + transfer_characteristics: r.read(8)?, + matrix_coefficients: r.read(8)?, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SpsChromaLocInfo { + pub chroma_sample_loc_type_top_field: u32, + pub chroma_sample_loc_type_bottom_field: u32, +} + +impl SpsChromaLocInfo { + fn from_reader(r: &mut BitReader) -> Result { + let chroma_sample_loc_type_top_field = read_golomb_unsigned(r)?; + let chroma_sample_loc_type_bottom_field = read_golomb_unsigned(r)?; + + Ok(Self { + chroma_sample_loc_type_top_field, + chroma_sample_loc_type_bottom_field, + }) + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct SpsFrameCropping { + pub left_offset: u32, + pub right_offset: u32, + pub top_offset: u32, + pub bottom_offset: u32, +} + +impl SpsFrameCropping { + fn from_reader(r: &mut BitReader) -> Result { + let left_offset = read_golomb_unsigned(r)?; + let right_offset = read_golomb_unsigned(r)?; + let top_offset = read_golomb_unsigned(r)?; + let bottom_offset = read_golomb_unsigned(r)?; + Ok(Self { + left_offset, + right_offset, + top_offset, + bottom_offset, + }) + } +} + +fn read_scaling_list( + r: &mut BitReader, + size: usize, +) -> Result<(Vec, bool), Error> { + let mut last_scale: i32 = 8; + let mut next_scale: i32 = 8; + let mut scaling_list: Vec = vec![0; size]; + let mut use_default_scaling_matrix_flag = false; + + for (j, item) in scaling_list.iter_mut().enumerate().take(size) { + if next_scale != 0 { + let delta_scale = read_golomb_signed(r)?; + + next_scale = (last_scale + delta_scale + 256) % 256; + use_default_scaling_matrix_flag = j == 0 && next_scale == 0; + } + + if next_scale == 0 { + *item = last_scale + } else { + *item = next_scale + } + + last_scale = *item + } + + Ok((scaling_list, use_default_scaling_matrix_flag)) +} + +// Reads an unsigned golomb-encoded value. +pub fn read_golomb_unsigned( + r: &mut BitReader, +) -> Result { + let mut leading_zero_bits: u32 = 0; + loop { + let b: u8 = r.read(1)?; + if b != 0 { + break; + } + leading_zero_bits += 1; + } + + let mut code_num: u32 = 0; + for n in (1..=leading_zero_bits).rev() { + let b: u32 = r.read(1)?; + code_num |= b << (n - 1); + } + + Ok(((1 << leading_zero_bits) - 1) + code_num) +} + +// Reads a signed golomb-encoded value. +pub fn read_golomb_signed(r: &mut BitReader) -> Result { + let v = read_golomb_unsigned(r)?; + + let vi = i32::try_from(v)?; + if (vi & 0b00000001) != 0 { + return Ok((vi + 1) / 2); + } + Ok(-vi / 2) +} + +// https://github.com/bluenviron/gortsplib/blob/862b1061f4a208d172cd8d9ed6c61e29f35541ef/pkg/codecs/h264/sps_test.go +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use test_case::test_case; + + #[test_case( + vec![ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + ], + Sps{ + profile_idc: 100, + level_idc: 12, + id: 0, + chrome_format_idc: 1, + log2_max_frame_num_minus4: 6, + pic_order_cnt_type: 2, + max_num_ref_frames: 1, + gaps_in_frame_num_value_allowed_flag: true, + pic_width_in_mbs_minus1: 21, + pic_height_in_map_units_minus1: 17, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + frame_cropping: None, + vui: Some(SpsVui{ + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1, + time_scale: 30, + fixed_frame_rate_flag: true, + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 352, + 288, + 15.0; "352x288" + )] + #[test_case( + vec![ + 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50, + 0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03, + 0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18, + 0xcb, + ], + Sps{ + profile_idc: 100, + level_idc: 31, + chrome_format_idc: 1, + log2_max_pic_order_cnt_lsb_minus4: 2, + max_num_ref_frames: 4, + pic_width_in_mbs_minus1: 79, + pic_height_in_map_units_minus1: 44, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + vui: Some(SpsVui{ + aspect_ratio_info: Some(SpsAspectRatioInfo{ + aspect_ratio_idc: 1, + sar_width: 0, + sar_height: 0, + }), + video_signal_type: Some(SpsVideoSignalType{ + video_format: 5, + colour_description: None, + }), + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1, + time_scale: 60, + fixed_frame_rate_flag: false, + }), + bitstream_restriction: Some(SpsBitstreamRestriction{ + motion_vectors_over_pic_boundaries_flag: true, + log2_max_mv_length_horizontal: 11, + log2_max_mv_length_vertical: 11, + max_num_reorder_frames: 2, + max_dec_frame_buffering: 4, + ..SpsBitstreamRestriction::default() + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1280, + 720, + 30.0; "1280x720" + )] + #[test_case( + vec![ + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, + ], + Sps{ + profile_idc: 66, + constraint_set0_flag: true, + constraint_set1_flag: true, + level_idc: 40, + pic_order_cnt_type: 2, + max_num_ref_frames: 3, + pic_width_in_mbs_minus1: 119, + pic_height_in_map_units_minus1: 67, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + frame_cropping: Some(SpsFrameCropping{ + bottom_offset: 4, + ..SpsFrameCropping::default() + }), + vui: Some(SpsVui{ + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1, + time_scale: 60, + fixed_frame_rate_flag: false, + }), + bitstream_restriction: Some(SpsBitstreamRestriction{ + motion_vectors_over_pic_boundaries_flag: true, + log2_max_mv_length_horizontal: 11, + log2_max_mv_length_vertical: 11, + max_dec_frame_buffering: 3, + ..SpsBitstreamRestriction::default() + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1920, + 1080, + 30.0; "1920x1080 baseline" + )] + #[test_case( + vec![ + 0x67, 0x64, 0x00, 0x28, 0xac, 0xd9, 0x40, 0x78, + 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, + 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, + 0xc6, 0x58, + ], + Sps{ + profile_idc: 100, + level_idc: 40, + chrome_format_idc: 1, + log2_max_pic_order_cnt_lsb_minus4: 2, + max_num_ref_frames: 4, + pic_width_in_mbs_minus1: 119, + pic_height_in_map_units_minus1: 67, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + frame_cropping: Some(SpsFrameCropping{ + bottom_offset: 4, + ..SpsFrameCropping::default() + }), + vui: Some(SpsVui{ + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1, + time_scale: 60, + fixed_frame_rate_flag: false, + }), + bitstream_restriction: Some(SpsBitstreamRestriction{ + motion_vectors_over_pic_boundaries_flag: true, + log2_max_mv_length_horizontal: 11, + log2_max_mv_length_vertical: 11, + max_num_reorder_frames: 2, + max_dec_frame_buffering: 4, + ..SpsBitstreamRestriction::default() + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1920, + 1080, + 30.0; "1920x1080 nvidia" + )] + #[test_case( + vec![ + 0x67, 0x64, 0x00, 0x29, 0xac, 0x13, 0x31, 0x40, + 0x78, 0x04, 0x47, 0xde, 0x03, 0xea, 0x02, 0x02, + 0x03, 0xe0, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, + 0x00, 0x06, 0x52, // 0x80, + ], + Sps{ + profile_idc: 100, + level_idc: 41, + chrome_format_idc: 1, + log2_max_frame_num_minus4: 8, + log2_max_pic_order_cnt_lsb_minus4: 5, + max_num_ref_frames: 4, + pic_width_in_mbs_minus1: 119, + pic_height_in_map_units_minus1: 33, + direct_8x8_inference_flag: true, + frame_cropping: Some(SpsFrameCropping{ + bottom_offset: 2, + ..SpsFrameCropping::default() + }), + vui: Some(SpsVui{ + aspect_ratio_info: Some(SpsAspectRatioInfo{ + aspect_ratio_idc: 1, + sar_width: 0, + sar_height: 0, + }), + video_signal_type: Some(SpsVideoSignalType{ + video_format: 5, + colour_description: Some(SpsColourDescription{ + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + }), + }), + chrome_log_info: Some(SpsChromaLocInfo{ + chroma_sample_loc_type_top_field: 0, + chroma_sample_loc_type_bottom_field: 0, + }), + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1, + time_scale: 50, + fixed_frame_rate_flag: true, + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1920, + 1084, + 25.0; "1920x1080" + )] + #[test_case( + vec![103, 100, 0, 32, 172, 23, 42, 1, 64, 30, 104, 64, 0, 1, 194, 0, 0, 87, 228, 33], + Sps{ + profile_idc: 100, + level_idc: 32, + chrome_format_idc: 1, + log2_max_pic_order_cnt_lsb_minus4: 4, + max_num_ref_frames: 1, + pic_width_in_mbs_minus1: 79, + pic_height_in_map_units_minus1: 59, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + log2_max_frame_num_minus4: 10, + vui: Some(SpsVui{ + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1800, + time_scale: 90000, + fixed_frame_rate_flag: true, + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1280, + 960, + 25.0; "hikvision" + )] + #[test_case( + vec![ + 103, 100, 0, 50, 173, 132, 1, 12, 32, 8, 97, 0, 67, 8, 2, + 24, 64, 16, 194, 0, 132, 59, 80, 20, 0, 90, 211, + 112, 16, 16, 20, 0, 0, 3, 0, 4, 0, 0, 3, 0, 162, 16, + ], + Sps{ + profile_idc: 100, + level_idc: 50, + chrome_format_idc: 1, + seq_scaling_list: Some(SpsSeqScalingList{ + scaling_list_4x4: vec![ + vec![ + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + ], + vec![ + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + ], + vec![ + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + ], + vec![ + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + ], + vec![ + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + ], + vec![ + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + ], + ], + use_default_scaling_matrix_4x4_flag: vec![false, false, false, false, false, false], + ..SpsSeqScalingList::default() + }), + log2_max_frame_num_minus4: 6, + pic_order_cnt_type: 2, + max_num_ref_frames: 1, + gaps_in_frame_num_value_allowed_flag: true, + pic_width_in_mbs_minus1: 159, + pic_height_in_map_units_minus1: 89, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + vui: Some(SpsVui{ + video_signal_type: Some(SpsVideoSignalType{ + video_format: 5, + colour_description: Some(SpsColourDescription{ + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + }), + }), + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1, + time_scale: 40, + fixed_frame_rate_flag: true, + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 2560, + 1440, + 20.0; "scaling matrix" + )] + #[test_case( + vec![ + 103, 100, 0, 42, 172, 44, 172, 7, + 128, 34, 126, 92, 5, 168, 8, 8, + 10, 0, 0, 7, 208, 0, 3, 169, + 129, 192, 0, 0, 76, 75, 0, 0, + 38, 37, 173, 222, 92, 20, + ], + Sps{ + profile_idc: 100, + level_idc: 42, + chrome_format_idc: 1, + log2_max_frame_num_minus4: 4, + log2_max_pic_order_cnt_lsb_minus4: 4, + max_num_ref_frames: 2, + pic_width_in_mbs_minus1: 119, + pic_height_in_map_units_minus1: 67, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + frame_cropping: Some(SpsFrameCropping{ + bottom_offset: 4, + ..SpsFrameCropping::default() + }), + vui: Some(SpsVui{ + aspect_ratio_info: Some(SpsAspectRatioInfo{ + aspect_ratio_idc: 1, + sar_width: 0, + sar_height: 0, + }), + video_signal_type: Some(SpsVideoSignalType{ + video_format: 5, + colour_description: Some(SpsColourDescription{ + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + }), + }), + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1000, + time_scale: 120000, + fixed_frame_rate_flag: true, + }), + nal_hrd: Some(SpsHrd{ + bit_rate_value_minus1: vec![39061], + cpb_size_value_minus1: vec![156249], + cbr_flag: vec![true], + initial_cpb_removal_delay_length_minus1: 23, + cpb_removal_delay_length_minus1: 15, + dpb_output_delay_length_minus1: 5, + time_offset_length: 24, + ..SpsHrd::default() + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1920, + 1080, + 60.0; "1920x1080 nvenc hrd" + )] + #[test_case( + vec![ + 103, 77, 0, 41, 154, 100, 3, 192, + 17, 63, 46, 2, 220, 4, 4, 5, + 0, 0, 3, 3, 232, 0, 0, 195, + 80, 232, 96, 0, 186, 180, 0, 2, + 234, 196, 187, 203, 141, 12, 0, 23, + 86, 128, 0, 93, 88, 151, 121, 112, + 160, + ], + Sps{ + profile_idc: 77, + level_idc: 41, + log2_max_frame_num_minus4: 5, + log2_max_pic_order_cnt_lsb_minus4: 5, + max_num_ref_frames: 1, + pic_width_in_mbs_minus1: 119, + pic_height_in_map_units_minus1: 67, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + frame_cropping: Some(SpsFrameCropping{ + bottom_offset: 4, + ..SpsFrameCropping::default() + }), + vui: Some(SpsVui{ + aspect_ratio_info: Some(SpsAspectRatioInfo{ + aspect_ratio_idc: 1, + sar_width: 0, + sar_height: 0, + }), + video_signal_type: Some(SpsVideoSignalType{ + video_format: 5, + colour_description: Some(SpsColourDescription{ + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + }), + }), + timing_info: Some(SpsTimingInfo{ + num_units_in_tick: 1000, + time_scale: 50000, + fixed_frame_rate_flag: true, + }), + nal_hrd: Some(SpsHrd{ + bit_rate_scale: 4, + cpb_size_scale: 3, + bit_rate_value_minus1: vec![11948], + cpb_size_value_minus1: vec![95585], + cbr_flag: vec![false], + initial_cpb_removal_delay_length_minus1: 23, + cpb_removal_delay_length_minus1: 15, + dpb_output_delay_length_minus1: 5, + time_offset_length: 24, + ..SpsHrd::default() + }), + vcl_hrd: Some(SpsHrd{ + bit_rate_scale: 4, + cpb_size_scale: 3, + bit_rate_value_minus1: vec![11948], + cpb_size_value_minus1: vec![95585], + cbr_flag: vec![false], + initial_cpb_removal_delay_length_minus1: 23, + cpb_removal_delay_length_minus1: 15, + dpb_output_delay_length_minus1: 5, + time_offset_length: 24, + ..SpsHrd::default() + }), + ..SpsVui::default() + }), + ..Sps::default() + }, + 1920, + 1080, + 25.0; "1920x1080 hikvision nal hrd + vcl hrd" + )] + fn test_sps_from_reader(byts: Vec, sps: Sps, width: usize, height: usize, fps: f64) { + let sps2 = Sps::deserialize(&byts).unwrap(); + assert_eq!(sps, sps2); + assert_eq!(width, sps2.width().unwrap()); + assert_eq!(height, sps2.height().unwrap()); + assert_eq!(fps, sps2.fps().unwrap()); + } + + #[test] + fn test_sps_from_reader_real() { + let byts = &[ + 103, 100, 0, 22, 172, 217, 64, 164, 59, 228, 136, 192, 68, 0, 0, 3, 0, 4, 0, 0, 3, 0, + 96, 60, 88, 182, 88, + ]; + let sps = Sps::deserialize(byts).unwrap(); + + let want = Sps { + profile_idc: 100, + level_idc: 22, + chrome_format_idc: 1, + log2_max_pic_order_cnt_lsb_minus4: 2, + max_num_ref_frames: 4, + pic_width_in_mbs_minus1: 40, + pic_height_in_map_units_minus1: 28, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + frame_cropping: Some(SpsFrameCropping { + right_offset: 3, + bottom_offset: 7, + ..SpsFrameCropping::default() + }), + vui: Some(SpsVui { + aspect_ratio_info: Some(SpsAspectRatioInfo { + aspect_ratio_idc: 1, + sar_width: 0, + sar_height: 0, + }), + timing_info: Some(SpsTimingInfo { + num_units_in_tick: 1, + time_scale: 24, + fixed_frame_rate_flag: false, + }), + bitstream_restriction: Some(SpsBitstreamRestriction { + motion_vectors_over_pic_boundaries_flag: true, + log2_max_mv_length_horizontal: 10, + log2_max_mv_length_vertical: 10, + max_num_reorder_frames: 2, + max_dec_frame_buffering: 4, + ..SpsBitstreamRestriction::default() + }), + ..SpsVui::default() + }), + ..Sps::default() + }; + assert_eq!(want, sps); + } + + #[test] + fn test_read_golomb_unsigned() { + let buf = vec![0x38]; + let mut r = BitReader::new(Cursor::new(buf)); + let v = read_golomb_unsigned(&mut r).unwrap(); + assert_eq!(6, v) + } + + #[test] + fn test_read_golomb_signed() { + let buf = vec![0x38]; + let mut r = BitReader::new(Cursor::new(buf)); + let v = read_golomb_signed(&mut r).unwrap(); + assert_eq!(-3, v); + + let buf = vec![0b00100100]; + let mut r = BitReader::new(Cursor::new(buf)); + let v = read_golomb_signed(&mut r).unwrap(); + assert_eq!(2, v) + } +} diff --git a/src/lib.rs b/src/lib.rs index 38de38b..92b2069 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,8 @@ pub struct Timestamp { /// A timestamp which must be compared to `start`. timestamp: i64, + dts: Option, + /// The codec-specified clock rate, in Hz. Must be non-zero. clock_rate: NonZeroU32, @@ -109,6 +111,7 @@ impl Timestamp { pub fn new(timestamp: i64, clock_rate: NonZeroU32, start: u32) -> Option { timestamp.checked_sub(i64::from(start)).map(|_| Timestamp { timestamp, + dts: None, clock_rate, start, }) @@ -120,6 +123,11 @@ impl Timestamp { self.timestamp } + #[inline] + pub fn dts(&self) -> Option { + self.dts + } + /// Returns timestamp of the start of the stream. #[inline] pub fn start(&self) -> u32 { @@ -153,6 +161,7 @@ impl Timestamp { .checked_add(i64::from(delta)) .map(|timestamp| Timestamp { timestamp, + dts: self.dts, clock_rate: self.clock_rate, start: self.start, }) @@ -163,10 +172,11 @@ impl Display for Timestamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{} (mod-2^32: {}), npt {:.03}", + "{} (mod-2^32: {}), npt {:.03}, dts {:?}", self.timestamp, self.timestamp as u32, - self.elapsed_secs() + self.elapsed_secs(), + self.dts, ) } }