fmp4mux: don't require dts for predictive-only formats like vp9

This commit is contained in:
Matthew Waters 2022-10-26 20:16:42 +11:00 committed by Sebastian Dröge
parent d46857d3b1
commit d067fb2ec8
4 changed files with 184 additions and 107 deletions

View file

@ -1623,7 +1623,7 @@ fn sample_flags_from_buffer(
timing_info: &super::FragmentTimingInfo, timing_info: &super::FragmentTimingInfo,
buffer: &gst::BufferRef, buffer: &gst::BufferRef,
) -> u32 { ) -> u32 {
if timing_info.intra_only { if timing_info.delta_frames.intra_only() {
(0b00u32 << (16 + 10)) | // leading: unknown (0b00u32 << (16 + 10)) | // leading: unknown
(0b10u32 << (16 + 8)) | // depends: no (0b10u32 << (16 + 8)) | // depends: no
(0b10u32 << (16 + 6)) | // depended: no (0b10u32 << (16 + 6)) | // depended: no
@ -1749,7 +1749,7 @@ fn analyze_buffers(
} }
if let Some(composition_time_offset) = *composition_time_offset { if let Some(composition_time_offset) = *composition_time_offset {
assert!(!timing_info.intra_only); assert!(timing_info.delta_frames.requires_dts());
if composition_time_offset != 0 { if composition_time_offset != 0 {
tr_flags |= SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT; tr_flags |= SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT;
} }

View file

@ -19,6 +19,7 @@ use once_cell::sync::Lazy;
use super::boxes; use super::boxes;
use super::Buffer; use super::Buffer;
use super::DeltaFrames;
/// Offset for the segment in non-single-stream variants. /// Offset for the segment in non-single-stream variants.
const SEGMENT_OFFSET: gst::ClockTime = gst::ClockTime::from_seconds(60 * 60 * 1000); const SEGMENT_OFFSET: gst::ClockTime = gst::ClockTime::from_seconds(60 * 60 * 1000);
@ -124,7 +125,7 @@ struct Stream {
sinkpad: gst_base::AggregatorPad, sinkpad: gst_base::AggregatorPad,
caps: gst::Caps, caps: gst::Caps,
intra_only: bool, delta_frames: DeltaFrames,
queued_gops: VecDeque<Gop>, queued_gops: VecDeque<Gop>,
fragment_filled: bool, fragment_filled: bool,
@ -286,14 +287,14 @@ impl FMP4Mux {
gst::trace!(CAT, obj: stream.sinkpad, "Handling buffer {:?}", buffer); gst::trace!(CAT, obj: stream.sinkpad, "Handling buffer {:?}", buffer);
let intra_only = stream.intra_only; let delta_frames = stream.delta_frames;
if !intra_only && buffer.dts().is_none() { if delta_frames.requires_dts() && buffer.dts().is_none() {
gst::error!(CAT, obj: stream.sinkpad, "Require DTS for video streams"); gst::error!(CAT, obj: stream.sinkpad, "Require DTS for video streams");
return Err(gst::FlowError::Error); return Err(gst::FlowError::Error);
} }
if intra_only && buffer.flags().contains(gst::BufferFlags::DELTA_UNIT) { if delta_frames.intra_only() && buffer.flags().contains(gst::BufferFlags::DELTA_UNIT) {
gst::error!(CAT, obj: stream.sinkpad, "Intra-only stream with delta units"); gst::error!(CAT, obj: stream.sinkpad, "Intra-only stream with delta units");
return Err(gst::FlowError::Error); return Err(gst::FlowError::Error);
} }
@ -328,12 +329,12 @@ impl FMP4Mux {
})?; })?;
// Enforce monotonically increasing PTS for intra-only streams // Enforce monotonically increasing PTS for intra-only streams
if intra_only { if !delta_frames.requires_dts() {
if pts < stream.current_position { if pts < stream.current_position {
gst::warning!( gst::warning!(
CAT, CAT,
obj: stream.sinkpad, obj: stream.sinkpad,
"Decreasing PTS {} < {} for intra-only stream", "Decreasing PTS {} < {}",
pts, pts,
stream.current_position, stream.current_position,
); );
@ -344,7 +345,7 @@ impl FMP4Mux {
end_pts = std::cmp::max(end_pts, pts); end_pts = std::cmp::max(end_pts, pts);
} }
let (dts_position, dts, end_dts) = if intra_only { let (dts_position, dts, end_dts) = if !delta_frames.requires_dts() {
(None, None, None) (None, None, None)
} else { } else {
// Negative DTS are handled via the dts_offset and by having negative composition time // Negative DTS are handled via the dts_offset and by having negative composition time
@ -466,10 +467,14 @@ impl FMP4Mux {
let gop = Gop { let gop = Gop {
start_pts: pts, start_pts: pts,
start_dts: dts, start_dts: dts,
start_dts_position: if intra_only { None } else { dts_position }, start_dts_position: if !delta_frames.requires_dts() {
None
} else {
dts_position
},
earliest_pts: pts, earliest_pts: pts,
earliest_pts_position: pts_position, earliest_pts_position: pts_position,
final_earliest_pts: intra_only, final_earliest_pts: !delta_frames.requires_dts(),
end_pts, end_pts,
end_dts, end_dts,
final_end_pts: false, final_end_pts: false,
@ -490,14 +495,14 @@ impl FMP4Mux {
prev_gop.end_pts = std::cmp::max(prev_gop.end_pts, pts); prev_gop.end_pts = std::cmp::max(prev_gop.end_pts, pts);
prev_gop.end_dts = std::cmp::max(prev_gop.end_dts, dts); prev_gop.end_dts = std::cmp::max(prev_gop.end_dts, dts);
if intra_only { if !delta_frames.requires_dts() {
prev_gop.final_end_pts = true; prev_gop.final_end_pts = true;
} }
if !prev_gop.final_earliest_pts { if !prev_gop.final_earliest_pts {
// Don't bother logging this for intra-only streams as it would be for every // Don't bother logging this for intra-only streams as it would be for every
// single buffer. // single buffer.
if !intra_only { if delta_frames.requires_dts() {
gst::debug!( gst::debug!(
CAT, CAT,
obj: stream.sinkpad, obj: stream.sinkpad,
@ -513,63 +518,59 @@ impl FMP4Mux {
} }
} }
} else if let Some(gop) = stream.queued_gops.front_mut() { } else if let Some(gop) = stream.queued_gops.front_mut() {
assert!(!intra_only); assert!(!delta_frames.intra_only());
// We require DTS for non-intra-only streams
let dts = dts.unwrap();
let end_dts = end_dts.unwrap();
gop.end_pts = std::cmp::max(gop.end_pts, end_pts); gop.end_pts = std::cmp::max(gop.end_pts, end_pts);
gop.end_dts = Some(std::cmp::max(gop.end_dts.expect("no end DTS"), end_dts)); gop.end_dts = gop.end_dts.opt_max(end_dts);
gop.buffers.push(GopBuffer { gop.buffers.push(GopBuffer { buffer, pts, dts });
buffer,
pts,
dts: Some(dts),
});
if gop.earliest_pts > pts && !gop.final_earliest_pts { if delta_frames.requires_dts() {
gst::debug!( let dts = dts.unwrap();
CAT,
obj: stream.sinkpad,
"Updating current GOP earliest PTS from {} to {}",
gop.earliest_pts,
pts
);
gop.earliest_pts = pts;
gop.earliest_pts_position = pts_position;
if let Some(prev_gop) = stream.queued_gops.get_mut(1) { if gop.earliest_pts > pts && !gop.final_earliest_pts {
if prev_gop.end_pts < pts { gst::debug!(
gst::debug!( CAT,
CAT, obj: stream.sinkpad,
obj: stream.sinkpad, "Updating current GOP earliest PTS from {} to {}",
"Updating previous GOP starting PTS {} end time from {} to {}", gop.earliest_pts,
pts, pts
prev_gop.end_pts, );
pts gop.earliest_pts = pts;
); gop.earliest_pts_position = pts_position;
prev_gop.end_pts = pts;
if let Some(prev_gop) = stream.queued_gops.get_mut(1) {
if prev_gop.end_pts < pts {
gst::debug!(
CAT,
obj: stream.sinkpad,
"Updating previous GOP starting PTS {} end time from {} to {}",
pts,
prev_gop.end_pts,
pts
);
prev_gop.end_pts = pts;
}
} }
} }
}
let gop = stream.queued_gops.front_mut().unwrap(); let gop = stream.queued_gops.front_mut().unwrap();
// The earliest PTS is known when the current DTS is bigger or equal to the first // The earliest PTS is known when the current DTS is bigger or equal to the first
// PTS that was observed in this GOP. If there was another frame later that had a // PTS that was observed in this GOP. If there was another frame later that had a
// lower PTS then it wouldn't be possible to display it in time anymore, i.e. the // lower PTS then it wouldn't be possible to display it in time anymore, i.e. the
// stream would be invalid. // stream would be invalid.
if gop.start_pts <= dts && !gop.final_earliest_pts { if gop.start_pts <= dts && !gop.final_earliest_pts {
gst::debug!( gst::debug!(
CAT, CAT,
obj: stream.sinkpad, obj: stream.sinkpad,
"GOP has final earliest PTS at {}", "GOP has final earliest PTS at {}",
gop.earliest_pts gop.earliest_pts
); );
gop.final_earliest_pts = true; gop.final_earliest_pts = true;
if let Some(prev_gop) = stream.queued_gops.get_mut(1) { if let Some(prev_gop) = stream.queued_gops.get_mut(1) {
prev_gop.final_end_pts = true; prev_gop.final_end_pts = true;
}
} }
} }
} else { } else {
@ -801,7 +802,7 @@ impl FMP4Mux {
.unwrap_or(gst::ClockTime::ZERO) .unwrap_or(gst::ClockTime::ZERO)
); );
let start_time = if stream.intra_only { let start_time = if !stream.delta_frames.requires_dts() {
earliest_pts earliest_pts
} else { } else {
start_dts.unwrap() start_dts.unwrap()
@ -812,7 +813,7 @@ impl FMP4Mux {
for gop in gops { for gop in gops {
let mut gop_buffers = gop.buffers.into_iter().peekable(); let mut gop_buffers = gop.buffers.into_iter().peekable();
while let Some(buffer) = gop_buffers.next() { while let Some(buffer) = gop_buffers.next() {
let timestamp = if stream.intra_only { let timestamp = if !stream.delta_frames.requires_dts() {
buffer.pts buffer.pts
} else { } else {
buffer.dts.unwrap() buffer.dts.unwrap()
@ -820,14 +821,14 @@ impl FMP4Mux {
let end_timestamp = match gop_buffers.peek() { let end_timestamp = match gop_buffers.peek() {
Some(buffer) => { Some(buffer) => {
if stream.intra_only { if !stream.delta_frames.requires_dts() {
buffer.pts buffer.pts
} else { } else {
buffer.dts.unwrap() buffer.dts.unwrap()
} }
} }
None => { None => {
if stream.intra_only { if !stream.delta_frames.requires_dts() {
gop.end_pts gop.end_pts
} else { } else {
gop.end_dts.unwrap() gop.end_dts.unwrap()
@ -840,7 +841,7 @@ impl FMP4Mux {
.checked_sub(timestamp) .checked_sub(timestamp)
.expect("Timestamps going backwards"); .expect("Timestamps going backwards");
let composition_time_offset = if stream.intra_only { let composition_time_offset = if !stream.delta_frames.requires_dts() {
None None
} else { } else {
let pts = buffer.pts; let pts = buffer.pts;
@ -874,7 +875,7 @@ impl FMP4Mux {
stream.caps.clone(), stream.caps.clone(),
Some(super::FragmentTimingInfo { Some(super::FragmentTimingInfo {
start_time, start_time,
intra_only: stream.intra_only, delta_frames: stream.delta_frames,
}), }),
buffers, buffers,
)); ));
@ -1446,41 +1447,39 @@ impl FMP4Mux {
let s = caps.structure(0).unwrap(); let s = caps.structure(0).unwrap();
let mut intra_only = false; let mut delta_frames = DeltaFrames::IntraOnly;
match s.name() { match s.name() {
"video/x-h264" | "video/x-h265" => { "video/x-h264" | "video/x-h265" => {
if !s.has_field_with_type("codec_data", gst::Buffer::static_type()) { if !s.has_field_with_type("codec_data", gst::Buffer::static_type()) {
gst::error!(CAT, obj: pad, "Received caps without codec_data"); gst::error!(CAT, obj: pad, "Received caps without codec_data");
return Err(gst::FlowError::NotNegotiated); return Err(gst::FlowError::NotNegotiated);
} }
delta_frames = DeltaFrames::Bidirectional;
} }
"video/x-vp9" => (), "video/x-vp9" => {
"image/jpeg" => { if !s.has_field_with_type("colorimetry", str::static_type()) {
intra_only = true; gst::error!(CAT, obj: pad, "Received caps without colorimetry");
return Err(gst::FlowError::NotNegotiated);
}
delta_frames = DeltaFrames::PredictiveOnly;
} }
"image/jpeg" => (),
"audio/mpeg" => { "audio/mpeg" => {
if !s.has_field_with_type("codec_data", gst::Buffer::static_type()) { if !s.has_field_with_type("codec_data", gst::Buffer::static_type()) {
gst::error!(CAT, obj: pad, "Received caps without codec_data"); gst::error!(CAT, obj: pad, "Received caps without codec_data");
return Err(gst::FlowError::NotNegotiated); return Err(gst::FlowError::NotNegotiated);
} }
intra_only = true;
}
"audio/x-alaw" | "audio/x-mulaw" => {
intra_only = true;
}
"audio/x-adpcm" => {
intra_only = true;
}
"application/x-onvif-metadata" => {
intra_only = true;
} }
"audio/x-alaw" | "audio/x-mulaw" => (),
"audio/x-adpcm" => (),
"application/x-onvif-metadata" => (),
_ => unreachable!(), _ => unreachable!(),
} }
state.streams.push(Stream { state.streams.push(Stream {
sinkpad: pad, sinkpad: pad,
caps, caps,
intra_only, delta_frames,
queued_gops: VecDeque::new(), queued_gops: VecDeque::new(),
fragment_filled: false, fragment_filled: false,
dts_offset: None, dts_offset: None,

View file

@ -90,7 +90,28 @@ pub(crate) struct FragmentTimingInfo {
/// Start time of this fragment /// Start time of this fragment
start_time: gst::ClockTime, start_time: gst::ClockTime,
/// Set if this is an intra-only stream /// Set if this is an intra-only stream
intra_only: bool, delta_frames: DeltaFrames,
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum DeltaFrames {
/// Only single completely decodable frames
IntraOnly,
/// Frames may depend on past frames
PredictiveOnly,
/// Frames may depend on past or future frames
Bidirectional,
}
impl DeltaFrames {
/// Whether dts is required to order buffers differently from presentation order
pub(crate) fn requires_dts(&self) -> bool {
matches!(self, Self::Bidirectional)
}
/// Whether this coding structure does not allow delta flags on buffers
pub(crate) fn intra_only(&self) -> bool {
matches!(self, Self::IntraOnly)
}
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -18,7 +18,7 @@ fn init() {
}); });
} }
fn test_buffer_flags_single_stream(cmaf: bool) { fn test_buffer_flags_single_stream(cmaf: bool, set_dts: bool, caps: gst::Caps) {
let mut h = if cmaf { let mut h = if cmaf {
gst_check::Harness::new("cmafmux") gst_check::Harness::new("cmafmux")
} else { } else {
@ -30,16 +30,7 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
.unwrap() .unwrap()
.set_property("fragment-duration", 5.seconds()); .set_property("fragment-duration", 5.seconds());
h.set_src_caps( h.set_src_caps(caps);
gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build(),
);
h.play(); h.play();
let output_offset = if cmaf { let output_offset = if cmaf {
@ -54,7 +45,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
{ {
let buffer = buffer.get_mut().unwrap(); let buffer = buffer.get_mut().unwrap();
buffer.set_pts(i.seconds()); buffer.set_pts(i.seconds());
buffer.set_dts(i.seconds()); if set_dts {
buffer.set_dts(i.seconds());
}
buffer.set_duration(gst::ClockTime::SECOND); buffer.set_duration(gst::ClockTime::SECOND);
if i != 0 && i != 5 { if i != 0 && i != 5 {
buffer.set_flags(gst::BufferFlags::DELTA_UNIT); buffer.set_flags(gst::BufferFlags::DELTA_UNIT);
@ -90,7 +83,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
gst::BufferFlags::HEADER | gst::BufferFlags::DISCONT gst::BufferFlags::HEADER | gst::BufferFlags::DISCONT
); );
assert_eq!(header.pts(), Some(gst::ClockTime::ZERO + output_offset)); assert_eq!(header.pts(), Some(gst::ClockTime::ZERO + output_offset));
assert_eq!(header.dts(), Some(gst::ClockTime::ZERO + output_offset)); if set_dts {
assert_eq!(header.dts(), Some(gst::ClockTime::ZERO + output_offset));
}
let fragment_header = h.pull().unwrap(); let fragment_header = h.pull().unwrap();
assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER); assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER);
@ -98,10 +93,12 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
fragment_header.pts(), fragment_header.pts(),
Some(gst::ClockTime::ZERO + output_offset) Some(gst::ClockTime::ZERO + output_offset)
); );
assert_eq!( if set_dts {
fragment_header.dts(), assert_eq!(
Some(gst::ClockTime::ZERO + output_offset) fragment_header.dts(),
); Some(gst::ClockTime::ZERO + output_offset)
);
}
assert_eq!(fragment_header.duration(), Some(5.seconds())); assert_eq!(fragment_header.duration(), Some(5.seconds()));
for i in 0..5 { for i in 0..5 {
@ -115,7 +112,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT); assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
} }
assert_eq!(buffer.pts(), Some(i.seconds() + output_offset)); assert_eq!(buffer.pts(), Some(i.seconds() + output_offset));
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset)); if set_dts {
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset));
}
assert_eq!(buffer.duration(), Some(gst::ClockTime::SECOND)); assert_eq!(buffer.duration(), Some(gst::ClockTime::SECOND));
} }
@ -124,7 +123,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
let fragment_header = h.pull().unwrap(); let fragment_header = h.pull().unwrap();
assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER); assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER);
assert_eq!(fragment_header.pts(), Some(5.seconds() + output_offset)); assert_eq!(fragment_header.pts(), Some(5.seconds() + output_offset));
assert_eq!(fragment_header.dts(), Some(5.seconds() + output_offset)); if set_dts {
assert_eq!(fragment_header.dts(), Some(5.seconds() + output_offset));
}
assert_eq!(fragment_header.duration(), Some(2.seconds())); assert_eq!(fragment_header.duration(), Some(2.seconds()));
for i in 5..7 { for i in 5..7 {
@ -138,7 +139,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT); assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
} }
assert_eq!(buffer.pts(), Some(i.seconds() + output_offset)); assert_eq!(buffer.pts(), Some(i.seconds() + output_offset));
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset)); if set_dts {
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset));
}
assert_eq!(buffer.duration(), Some(gst::ClockTime::SECOND)); assert_eq!(buffer.duration(), Some(gst::ClockTime::SECOND));
} }
@ -153,17 +156,71 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
} }
#[test] #[test]
fn test_buffer_flags_single_stream_cmaf() { fn test_buffer_flags_single_h264_stream_cmaf() {
init(); init();
test_buffer_flags_single_stream(true); let caps = gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build();
test_buffer_flags_single_stream(true, true, caps);
} }
#[test] #[test]
fn test_buffer_flags_single_stream_iso() { fn test_buffer_flags_single_h264_stream_iso() {
init(); init();
test_buffer_flags_single_stream(false); let caps = gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build();
test_buffer_flags_single_stream(false, true, caps);
}
#[test]
fn test_buffer_flags_single_vp9_stream_cmaf() {
init();
let caps = gst::Caps::builder("video/x-vp9")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("profile", "0")
.field("chroma-format", "4:2:0")
.field("bit-depth-luma", 8u32)
.field("bit-depth-chroma", 8u32)
.field("colorimetry", "bt709")
.build();
test_buffer_flags_single_stream(true, false, caps);
}
#[test]
fn test_buffer_flags_single_vp9_stream_iso() {
init();
let caps = gst::Caps::builder("video/x-vp9")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("profile", "0")
.field("chroma-format", "4:2:0")
.field("bit-depth-luma", 8u32)
.field("bit-depth-chroma", 8u32)
.field("colorimetry", "bt709")
.build();
test_buffer_flags_single_stream(false, false, caps);
} }
#[test] #[test]