mux/mp4: add taic box

TAI Clock Information (`taic`) will be defined in ISO/IEC 23001-17 Amendment 1.
The TAI Clock Information box appears in the sample entry for the applicable track.

The clock information is metadata about the type and quality of a TAI clock. That
can be set via a stream tag. For example:

gst-launch-1.0 -v videotestsrc num-buffers=50 ! video/x-raw,width=1280,height=720,format=I420 !  taginject tags=precision-clock-type=can-sync-to-tai,precision-clock-time-uncertainty-nanoseconds=30000 ! isomp4mux ! filesink location=taic.mp4

This version writes compliant taic boxes if either or both of the tags are set. There
are two values in taic (for clock resolution and clock drift rate) that will be set to
reasonable default values.

taic is intended to be used (in video) with a TAITimestampPacket instance that is
per-sample auxilary information (i.e. via saio and saiz entries). That will be
provided as a follow-up change, based on a meta.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/2246>
This commit is contained in:
Brad Hards 2025-02-26 20:11:39 +11:00
parent 5eea7288d3
commit e1af0533b5
4 changed files with 456 additions and 41 deletions

View file

@ -1100,6 +1100,16 @@ fn write_visual_sample_entry(
})?;
}
if let Some(taic) = &stream.tai_clock_info {
write_full_box(v, b"taic", FULL_BOX_VERSION_0, FULL_BOX_FLAGS_NONE, |v| {
v.extend(taic.time_uncertainty.to_be_bytes());
v.extend(taic.clock_resolution.to_be_bytes());
v.extend(taic.clock_drift_rate.to_be_bytes());
v.extend(((taic.clock_type as u8) << 6).to_be_bytes());
Ok(())
})?;
}
Ok(())
})?;

View file

@ -16,12 +16,17 @@ use gst_base::subclass::prelude::*;
use num_integer::Integer;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::str::FromStr;
use std::sync::Mutex;
use crate::mp4mux::obu::read_seq_header_obu_bytes;
use crate::mp4mux::TaicClockType;
use std::sync::LazyLock;
use super::boxes;
use super::PrecisionClockTimeUncertaintyNanosecondsTag;
use super::PrecisionClockTypeTag;
use super::TaiClockInfo;
use super::TransformMatrix;
/// Offset between NTP and UNIX epoch in seconds.
@ -75,6 +80,44 @@ pub(crate) static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
)
});
impl FromStr for TaicClockType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"unknown" => Ok(TaicClockType::Unknown),
"cannot-sync-to-tai" => Ok(TaicClockType::CannotSync),
"can-sync-to-tai" => Ok(TaicClockType::CanSync),
_ => bail!("unknown TAI Clock type: {}", s),
}
}
}
impl<'a> Tag<'a> for PrecisionClockTypeTag {
type TagType = &'a str;
const TAG_NAME: &'static glib::GStr = glib::gstr!("precision-clock-type");
}
impl CustomTag<'_> for PrecisionClockTypeTag {
const FLAG: gst::TagFlag = gst::TagFlag::Meta;
const NICK: &'static glib::GStr = glib::gstr!("precision-clock-type");
const DESCRIPTION: &'static glib::GStr =
glib::gstr!("ISO/IEC 23001-17 TAI Clock type information");
}
impl Tag<'_> for PrecisionClockTimeUncertaintyNanosecondsTag {
type TagType = i32;
const TAG_NAME: &'static glib::GStr =
glib::gstr!("precision-clock-time-uncertainty-nanoseconds");
}
impl CustomTag<'_> for PrecisionClockTimeUncertaintyNanosecondsTag {
const FLAG: gst::TagFlag = gst::TagFlag::Meta;
const NICK: &'static glib::GStr = glib::gstr!("precision-clock-time-uncertainty-nanoseconds");
const DESCRIPTION: &'static glib::GStr =
glib::gstr!("ISO/IEC 23001-17 TAI Clock time uncertainty (in nanoseconds) information");
}
const DEFAULT_INTERLEAVE_BYTES: Option<u64> = None;
const DEFAULT_INTERLEAVE_TIME: Option<gst::ClockTime> = Some(gst::ClockTime::from_mseconds(500));
@ -97,6 +140,22 @@ impl Default for Settings {
}
}
// Standard values for taic box (ISO/IEC 23001-17 Amd 1)
const TAIC_TIME_UNCERTAINTY_UNKNOWN: u64 = 0xFFFF_FFFF_FFFF_FFFF;
const TAIC_CLOCK_RESOLUTION_MICROSECONDS: u32 = 1000;
const TAIC_CLOCK_DRIFT_RATE_UNKNOWN: i32 = 0x7FFF_FFFF;
impl Default for TaiClockInfo {
fn default() -> Self {
TaiClockInfo {
time_uncertainty: TAIC_TIME_UNCERTAINTY_UNKNOWN,
clock_resolution: TAIC_CLOCK_RESOLUTION_MICROSECONDS,
clock_drift_rate: TAIC_CLOCK_DRIFT_RATE_UNKNOWN,
clock_type: super::TaicClockType::Unknown,
}
}
}
#[derive(Debug)]
struct PendingBuffer {
buffer: gst::Buffer,
@ -160,6 +219,9 @@ struct Stream {
avg_bitrate: Option<u32>,
max_bitrate: Option<u32>,
/// TAI precision clock information
tai_clock_info: Option<TaiClockInfo>,
}
impl Stream {
@ -1159,6 +1221,10 @@ impl MP4Mux {
let mut language_code = None;
let mut avg_bitrate = None;
let mut max_bitrate = None;
let mut tai_clock_info = None;
let mut clock_type = TaicClockType::Unknown;
let mut found_taic_part = false;
let mut time_uncertainty = TAIC_TIME_UNCERTAINTY_UNKNOWN;
pad.sticky_events_foreach(|ev| {
if let gst::EventView::Tag(ev) = ev.view() {
let tag = ev.tag();
@ -1235,6 +1301,62 @@ impl MP4Mux {
}
avg_bitrate = Some(bitrate);
}
if let Some(tag_value) = ev.tag().get::<PrecisionClockTypeTag>() {
let clock_type_str = tag_value.get();
gst::debug!(
CAT,
imp = self,
"Received TAI clock type from tags: {:?}",
clock_type_str
);
clock_type = match clock_type_str.parse() {
Ok(t) => {
found_taic_part = true;
t
}
Err(err) => {
gst::warning!(CAT, imp = self, "error parsing TAIClockType tag value: {}", err);
TaicClockType::Unknown
}
};
if tag.scope() == gst::TagScope::Global {
gst::info!(
CAT,
obj = pad,
"TAI clock tags scoped 'global' are treated as if they were stream tags.",
);
}
}
if let Some(tag_value) = ev
.tag()
.get::<PrecisionClockTimeUncertaintyNanosecondsTag>()
{
let time_uncertainty_from_tag = tag_value.get();
if time_uncertainty_from_tag < 1 {
gst::warning!(
CAT,
imp = self,
"Ignoring non-positive TAI clock uncertainty from tags: {:?}",
time_uncertainty_from_tag
);
} else {
time_uncertainty = time_uncertainty_from_tag as u64;
gst::debug!(
CAT,
imp = self,
"Received TAI clock uncertainty from tags: {:?}",
time_uncertainty
);
if tag.scope() == gst::TagScope::Global {
gst::info!(
CAT,
obj = pad,
"TAI clock tags scoped 'global' are treated as if they were stream tags.",
);
}
found_taic_part = true;
}
}
}
std::ops::ControlFlow::Continue(gst::EventForeachAction::Keep)
});
@ -1247,6 +1369,16 @@ impl MP4Mux {
}
};
// TODO: also set this when we have GIMI implemented
if found_taic_part {
// TODO: set remaining parts if there are tags implemented
tai_clock_info = Some(TaiClockInfo {
clock_type,
time_uncertainty,
..Default::default()
});
}
gst::info!(CAT, obj = pad, "Configuring caps {caps:?}");
let s = caps.structure(0).unwrap();
@ -1336,6 +1468,7 @@ impl MP4Mux {
stream_orientation,
max_bitrate,
avg_bitrate,
tai_clock_info,
});
}
@ -1957,6 +2090,7 @@ impl AggregatorImpl for MP4Mux {
max_bitrate: stream.max_bitrate,
avg_bitrate: stream.avg_bitrate,
chunks: stream.chunks,
tai_clock_info: stream.tai_clock_info,
});
}

View file

@ -9,6 +9,7 @@
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
use gst::tags;
mod boxes;
mod imp;
@ -51,6 +52,8 @@ pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
ONVIFMP4Mux::static_type(),
)?;
tags::register::<PrecisionClockTypeTag>();
tags::register::<PrecisionClockTimeUncertaintyNanosecondsTag>();
Ok(())
}
@ -203,6 +206,41 @@ pub(crate) struct ElstInfo {
duration: Option<gst::ClockTime>,
}
pub enum PrecisionClockTimeUncertaintyNanosecondsTag {}
pub enum PrecisionClockTypeTag {}
/// Synchronisation capability of clock
///
/// This is used in the TAIClockInfoBox, see ISO/IEC 23001-17:2024 Amd 1.
#[repr(u8)]
#[allow(dead_code)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum TaicClockType {
/// Clock type is unknown
Unknown = 0u8,
/// Clock does not synchronise to an atomic clock time source
CannotSync = 1u8,
// Clock can synchronise to an atomic clock time source
CanSync = 2u8,
// Reserved - DO NOT USE
Reserved = 3u8,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct TaiClockInfo {
// Set with the PrecisionClockTimeUncertaintyNanoseconds tag
// Defaults to unknown
time_uncertainty: u64,
// Cannot currently be set, defaults to microsecond
clock_resolution: u32,
// Cannot currently be set, defaults to unknown
clock_drift_rate: i32,
// Set with the PrecisionClockType tag
// Defaults to unknown
clock_type: TaicClockType,
}
#[derive(Debug)]
pub(crate) struct Stream {
/// Caps of this stream
@ -243,6 +281,9 @@ pub(crate) struct Stream {
/// Whether this stream should be encoded as an ISO/IEC 23008-12 image sequence
image_sequence: bool,
/// TAI Clock information (ISO/IEC 23001-17 Amd 1)
tai_clock_info: Option<TaiClockInfo>,
}
#[derive(Debug)]

View file

@ -64,6 +64,17 @@ impl Pipeline {
}
}
struct ExpectedConfiguration {
is_audio: bool,
width: u32,
height: u32,
has_ctts: bool,
has_stss: bool,
has_taic: bool,
taic_time_uncertainty: u64,
taic_clock_type: u8,
}
fn test_basic_with(video_enc: &str, audio_enc: &str, cb: impl FnOnce(&Path)) {
let Ok(pipeline) = gst::parse::launch(&format!(
"videotestsrc num-buffers=99 ! {video_enc} ! mux. \
@ -261,15 +272,25 @@ fn test_encode_uncompressed(video_format: &str, width: u32, height: u32) {
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
test_expected_uncompressed_output(location);
test_expected_uncompressed_output(location, width, height);
}
fn test_expected_uncompressed_output(location: &Path) {
fn test_expected_uncompressed_output(location: &Path, width: u32, height: u32) {
check_generic_single_trak_file_structure(
location,
b"iso4".into(),
0,
vec![b"isom".into(), b"mp41".into(), b"mp42".into()],
ExpectedConfiguration {
is_audio: false,
width,
height,
has_ctts: false,
has_stss: false,
has_taic: false,
taic_clock_type: 0, // only if has_taic
taic_time_uncertainty: 0, // only if has_taic
},
);
}
@ -278,6 +299,7 @@ fn check_generic_single_trak_file_structure(
expected_major_brand: mp4_atom::FourCC,
expected_minor_version: u32,
expected_compatible_brands: Vec<mp4_atom::FourCC>,
expected_config: ExpectedConfiguration,
) {
let mut required_top_level_boxes: Vec<mp4_atom::FourCC> = vec![
b"ftyp".into(),
@ -309,11 +331,19 @@ fn check_generic_single_trak_file_structure(
assert!(moov.meta.is_none());
assert!(moov.mvex.is_none());
assert!(moov.udta.is_none());
check_mvhd_sanity(&moov.mvhd);
check_trak_sanity(&moov.trak);
check_mvhd_sanity(&moov.mvhd, &expected_config);
check_trak_sanity(&moov.trak, &expected_config);
}
mp4_atom::Free::KIND => {
let free = mp4_atom::Free::read_atom(&header, &mut input).unwrap();
assert_eq!(free.zeroed.size, 0);
}
mp4_atom::Mdat::KIND => {
let mdat = mp4_atom::Mdat::read_atom(&header, &mut input).unwrap();
assert!(!mdat.data.is_empty());
}
_ => {
let _ = mp4_atom::Any::read_atom(&header, &mut input).unwrap();
panic!("Unexpected top level box: {:?}", header.kind);
}
}
}
@ -579,15 +609,25 @@ fn test_encode_uncompressed_image_sequence(video_format: &str, width: u32, heigh
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
test_expected_image_sequence_output(location);
test_expected_image_sequence_output(location, width, height);
}
fn test_expected_image_sequence_output(location: &Path) {
fn test_expected_image_sequence_output(location: &Path, width: u32, height: u32) {
check_generic_single_trak_file_structure(
location,
b"msf1".into(),
0,
vec![b"iso8".into(), b"msf1".into(), b"unif".into()],
ExpectedConfiguration {
is_audio: false,
width,
height,
has_ctts: false,
has_stss: false,
has_taic: false,
taic_time_uncertainty: 0, // only if has_taic
taic_clock_type: 0, // only if has_taic
},
);
}
@ -647,6 +687,16 @@ fn test_audio_only_output(location: &Path) {
b"iso4".into(),
0,
vec![b"isom".into(), b"mp41".into(), b"mp42".into()],
ExpectedConfiguration {
is_audio: true,
width: 0, // ignored for audio
height: 0, // ignored for audio
has_ctts: false,
has_stss: false,
has_taic: false,
taic_time_uncertainty: 0, // only if has_taic
taic_clock_type: 0, // only if has_taic
},
);
}
@ -667,45 +717,58 @@ fn check_ftyp_output(
}
}
fn check_mvhd_sanity(mvhd: &mp4_atom::Mvhd) {
fn check_mvhd_sanity(mvhd: &mp4_atom::Mvhd, expected_config: &ExpectedConfiguration) {
assert!(mvhd.creation_time > 0);
assert!(mvhd.modification_time > 0);
assert!(mvhd.next_track_id > 1);
assert!(mvhd.duration > 0);
// tkhd.volume might or might not be valid depending on track type
if expected_config.is_audio {
assert!(mvhd.volume.integer() > 0);
} else {
assert_eq!(mvhd.volume.integer(), 1);
assert_eq!(mvhd.volume.decimal(), 0);
}
// TODO: assess remaining values for potential invariant
}
fn check_trak_sanity(trak: &[mp4_atom::Trak]) {
fn check_trak_sanity(trak: &[mp4_atom::Trak], expected_config: &ExpectedConfiguration) {
assert_eq!(trak.len(), 1);
assert!(trak[0].meta.is_none());
check_tkhd_sanity(&trak[0].tkhd);
check_tkhd_sanity(&trak[0].tkhd, expected_config);
check_edts_sanity(&trak[0].edts);
check_mdia_sanity(&trak[0].mdia);
check_mdia_sanity(&trak[0].mdia, expected_config);
}
fn check_edts_sanity(maybe_edts: &Option<mp4_atom::Edts>) {
assert!(maybe_edts.is_some());
let edts = maybe_edts.as_ref().unwrap();
assert!(edts.elst.is_some());
let elst = edts.elst.as_ref().unwrap();
assert!(!elst.entries.is_empty());
assert!(maybe_edts.as_ref().is_some_and(|edts| {
assert!(edts.elst.is_some());
let elst = edts.elst.as_ref().unwrap();
assert!(!elst.entries.is_empty());
true
}));
}
fn check_tkhd_sanity(tkhd: &mp4_atom::Tkhd) {
fn check_tkhd_sanity(tkhd: &mp4_atom::Tkhd, expected_config: &ExpectedConfiguration) {
assert!(tkhd.creation_time > 0);
assert!(tkhd.modification_time > 0);
assert!(tkhd.enabled);
// tkhd.height might or might not be valid depending on track type
// tkhd.width might or might not be valid depending on track type
// tkhd.volume might or might not be valid depending on track type
if expected_config.is_audio {
assert_eq!(tkhd.width, 0.into());
assert_eq!(tkhd.height, 0.into());
assert!(tkhd.volume.integer() > 0);
} else {
assert_eq!(tkhd.width.integer(), expected_config.width as u16);
assert_eq!(tkhd.height.integer(), expected_config.height as u16);
assert_eq!(tkhd.volume.integer(), 0);
assert_eq!(tkhd.volume.decimal(), 0);
}
// TODO: assess remaining values for potential invariant
}
fn check_mdia_sanity(mdia: &mp4_atom::Mdia) {
fn check_mdia_sanity(mdia: &mp4_atom::Mdia, expected_config: &ExpectedConfiguration) {
check_hdlr_sanity(&mdia.hdlr);
check_mdhd_sanity(&mdia.mdhd);
check_minf_sanity(&mdia.minf);
check_minf_sanity(&mdia.minf, expected_config);
}
fn check_hdlr_sanity(hdlr: &mp4_atom::Hdlr) {
@ -724,13 +787,13 @@ fn check_mdhd_sanity(mdhd: &mp4_atom::Mdhd) {
assert!(mdhd.language.len() == 3);
}
fn check_minf_sanity(minf: &mp4_atom::Minf) {
fn check_minf_sanity(minf: &mp4_atom::Minf, expected_config: &ExpectedConfiguration) {
check_dinf_sanity(&minf.dinf);
assert!(
(minf.smhd.is_some() && (minf.vmhd.is_none())
|| (minf.smhd.is_none() || minf.vmhd.is_some()))
);
check_stbl_sanity(&minf.stbl);
check_stbl_sanity(&minf.stbl, expected_config);
}
fn check_dinf_sanity(dinf: &mp4_atom::Dinf) {
@ -740,24 +803,36 @@ fn check_dinf_sanity(dinf: &mp4_atom::Dinf) {
assert!(url.location.is_empty());
}
fn check_stbl_sanity(stbl: &mp4_atom::Stbl) {
fn check_stbl_sanity(stbl: &mp4_atom::Stbl, expected_config: &ExpectedConfiguration) {
assert!(stbl.co64.is_none());
assert!(stbl.ctts.is_none());
if expected_config.has_ctts {
assert!(stbl.ctts.is_some());
// TODO:
// check_ctts_sanity(&stbl.ctts);
} else {
assert!(stbl.ctts.is_none());
}
assert!(stbl.saio.is_none());
assert!(stbl.saiz.is_none());
check_stco_sanity(&stbl.stco);
check_stsc_sanity(&stbl.stsc);
check_stsd_sanity(&stbl.stsd);
assert!(stbl.stss.is_none());
check_stsd_sanity(&stbl.stsd, expected_config);
if expected_config.has_stss {
assert!(stbl.stss.is_some());
// TODO:
// check_stss_sanity(&stbl.ctts);
} else {
assert!(stbl.stss.is_none());
}
check_stsz_sanity(&stbl.stsz);
check_stts_sanity(&stbl.stts);
// TODO: check consistency between sample sizes and chunk / sample offsets
}
fn check_stco_sanity(maybe_stco: &Option<mp4_atom::Stco>) {
assert!(maybe_stco.is_some());
let stco = maybe_stco.as_ref().unwrap();
assert!(!stco.entries.is_empty());
assert!(maybe_stco
.as_ref()
.is_some_and(|stco| { !stco.entries.is_empty() }));
// TODO: see if there is anything generic about the stco entries we could check
}
@ -766,12 +841,51 @@ fn check_stsc_sanity(stsc: &mp4_atom::Stsc) {
// TODO: see if there is anything generic about the stsc entries we could check
}
fn check_stsd_sanity(stsd: &mp4_atom::Stsd) {
fn check_stsd_sanity(stsd: &mp4_atom::Stsd, expected_config: &ExpectedConfiguration) {
assert_eq!(stsd.codecs.len(), 1);
let codec = &stsd.codecs[0];
match codec {
mp4_atom::Codec::Avc1(_avc1) => {
// TODO: check H.264 codec
mp4_atom::Codec::Avc1(avc1) => {
assert_eq!(avc1.visual.width, expected_config.width as u16);
assert_eq!(avc1.visual.height, expected_config.height as u16);
assert_eq!(avc1.visual.depth, 24);
if expected_config.has_taic {
assert!(avc1.taic.as_ref().is_some_and(|taic| {
assert_eq!(taic.clock_type, expected_config.taic_clock_type.into());
assert_eq!(taic.time_uncertainty, expected_config.taic_time_uncertainty);
assert_eq!(taic.clock_drift_rate, 2147483647);
assert_eq!(taic.clock_resolution, 1000);
true
}));
} else {
assert!(avc1.taic.is_none());
}
assert!(avc1
.pasp
.as_ref()
.is_some_and(|pasp| { pasp.h_spacing == 1 && pasp.v_spacing == 1 }));
assert!(avc1.colr.as_ref().is_some_and(|colr| {
match colr {
mp4_atom::Colr::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients: _,
full_range_flag,
} => {
assert_eq!(*colour_primaries, 6);
assert_eq!(*transfer_characteristics, 6);
assert!(!(*full_range_flag));
true
}
mp4_atom::Colr::Ricc { profile: _ } => {
panic!("Incorrect colr type: ricc")
}
mp4_atom::Colr::Prof { profile: _ } => {
panic!("Incorrect colr type: prof")
}
}
}));
}
mp4_atom::Codec::Hev1(_hev1) => {
// TODO: check HEVC codec (maybe shared?)
@ -798,7 +912,7 @@ fn check_stsd_sanity(stsd: &mp4_atom::Stsd) {
// TODO: check OPUS codec
}
mp4_atom::Codec::Uncv(uncv) => {
check_uncv_codec_sanity(uncv);
check_uncv_codec_sanity(uncv, expected_config);
}
mp4_atom::Codec::Unknown(four_cc) => {
todo!("Unsupported codec type: {:?}", four_cc);
@ -806,8 +920,8 @@ fn check_stsd_sanity(stsd: &mp4_atom::Stsd) {
}
}
fn check_uncv_codec_sanity(uncv: &mp4_atom::Uncv) {
check_visual_sample_entry_sanity(&uncv.visual);
fn check_uncv_codec_sanity(uncv: &mp4_atom::Uncv, expected_config: &ExpectedConfiguration) {
check_visual_sample_entry_sanity(&uncv.visual, expected_config);
// See ISO/IEC 23001-17 Table 5 for the profiles
let valid_v0_profiles: Vec<mp4_atom::FourCC> = vec![
b"2vuy".into(),
@ -874,10 +988,13 @@ fn check_uncv_codec_sanity(uncv: &mp4_atom::Uncv) {
}
}
fn check_visual_sample_entry_sanity(visual: &mp4_atom::Visual) {
fn check_visual_sample_entry_sanity(
visual: &mp4_atom::Visual,
expected_config: &ExpectedConfiguration,
) {
assert!(visual.depth > 0);
assert!(visual.height > 0);
assert!(visual.width > 0);
assert_eq!(visual.height, expected_config.height as u16);
assert_eq!(visual.width, expected_config.width as u16);
// TODO: assess remaining values for potential invariant
}
@ -898,3 +1015,116 @@ fn check_stts_sanity(stts: &mp4_atom::Stts) {
assert!(!stts.entries.is_empty());
// TODO: see if there is anything generic about the stts entries we could check
}
fn test_taic_encode(video_enc: &str) {
let filename = format!("taic_{video_enc}.mp4").to_string();
let temp_dir = tempdir().unwrap();
let temp_file_path = temp_dir.path().join(filename);
let location = temp_file_path.as_path();
let pipeline_text = format!("videotestsrc num-buffers=250 ! {video_enc} ! taginject tags=\"precision-clock-type=can-sync-to-TAI,precision-clock-time-uncertainty-nanoseconds=100000\" scope=stream ! isomp4mux ! filesink location={:?}", location);
let Ok(pipeline) = gst::parse::launch(&pipeline_text) else {
panic!("could not build encoding pipeline")
};
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
for msg in pipeline.bus().unwrap().iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
panic!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
}
_ => (),
}
}
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
check_generic_single_trak_file_structure(
location,
b"iso4".into(),
0,
vec![b"isom".into(), b"mp41".into(), b"mp42".into()],
ExpectedConfiguration {
is_audio: false,
width: 320,
height: 240,
has_ctts: true,
has_stss: true,
has_taic: true,
taic_time_uncertainty: 100_000,
taic_clock_type: 2,
},
);
}
fn test_taic_encode_cannot_sync(video_enc: &str) {
let filename = format!("taic_{video_enc}_cannot_sync.mp4").to_string();
let temp_dir = tempdir().unwrap();
let temp_file_path = temp_dir.path().join(filename);
let location = temp_file_path.as_path();
let pipeline_text = format!("videotestsrc num-buffers=250 ! {video_enc} ! taginject tags=\"precision-clock-type=cannot-sync-to-TAI\" scope=stream ! isomp4mux ! filesink location={:?}", location);
let Ok(pipeline) = gst::parse::launch(&pipeline_text) else {
panic!("could not build encoding pipeline")
};
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
for msg in pipeline.bus().unwrap().iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
panic!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
}
_ => (),
}
}
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
check_generic_single_trak_file_structure(
location,
b"iso4".into(),
0,
vec![b"isom".into(), b"mp41".into(), b"mp42".into()],
ExpectedConfiguration {
is_audio: false,
width: 320,
height: 240,
has_ctts: true,
has_stss: true,
has_taic: true,
taic_time_uncertainty: 0xFFFF_FFFF_FFFF_FFFF,
taic_clock_type: 1,
},
);
}
#[test]
fn test_taic_x264() {
init();
test_taic_encode("x264enc");
}
#[test]
fn test_taic_x264_no_sync() {
init();
test_taic_encode_cannot_sync("x264enc");
}