From eb494599378cbb6d133b11748527402f59a97982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Philipp=20M=C3=BCller?= Date: Sun, 10 Mar 2024 13:01:52 +0000 Subject: [PATCH] rtp: m2pt: add some unit tests Part-of: --- net/rtp/src/mp2t/mod.rs | 4 + net/rtp/src/mp2t/tests/mod.rs | 3 + net/rtp/src/mp2t/tests/tests.rs | 640 ++++++++++++++++++ .../mp2t/tests/videotestsrc-80x60-h264.m2ts | Bin 0 -> 3648 bytes .../src/mp2t/tests/videotestsrc-80x60-h264.ts | Bin 0 -> 4324 bytes 5 files changed, 647 insertions(+) create mode 100644 net/rtp/src/mp2t/tests/mod.rs create mode 100644 net/rtp/src/mp2t/tests/tests.rs create mode 100644 net/rtp/src/mp2t/tests/videotestsrc-80x60-h264.m2ts create mode 100644 net/rtp/src/mp2t/tests/videotestsrc-80x60-h264.ts diff --git a/net/rtp/src/mp2t/mod.rs b/net/rtp/src/mp2t/mod.rs index 7ec136ac..493e2b1e 100644 --- a/net/rtp/src/mp2t/mod.rs +++ b/net/rtp/src/mp2t/mod.rs @@ -2,3 +2,7 @@ pub mod depay; pub mod pay; + +#[allow(clippy::module_inception)] +#[cfg(test)] +mod tests; diff --git a/net/rtp/src/mp2t/tests/mod.rs b/net/rtp/src/mp2t/tests/mod.rs new file mode 100644 index 00000000..a79d7e29 --- /dev/null +++ b/net/rtp/src/mp2t/tests/mod.rs @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MPL-2.0 + +mod tests; diff --git a/net/rtp/src/mp2t/tests/tests.rs b/net/rtp/src/mp2t/tests/tests.rs new file mode 100644 index 00000000..b6282dd5 --- /dev/null +++ b/net/rtp/src/mp2t/tests/tests.rs @@ -0,0 +1,640 @@ +// GStreamer RTP MPEG-TS Payloader / Depayloader - unit tests +// +// Copyright (C) 2023-2024 Tim-Philipp Müller +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use crate::tests::{run_test_pipeline, ExpectedBuffer, ExpectedPacket, Source}; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + crate::plugin_register_static().expect("rtpmp2t test"); + }); +} + +// gst-launch-1.0 videotestsrc num-buffers=3 \ +// ! video/x-raw,format=I420,width=80,height=60,framerate=25/1 \ +// ! x264enc tune=zerolatency \ +// ! mpegtsmux \ +// ! filesink location=videotestsrc-80x60-h264.ts +// +// - 15 packets with timestamp 0ms +// - 4 packets with timestamp 40ms +// - 4 packets with timestamp 80ms +// +const MPEGTS_DATA: &[u8] = include_bytes!("videotestsrc-80x60-h264.ts").as_slice(); + +// PACKET_SIZE = 188 +fn make_mp2t_buffer( + packet_number: usize, + n_packets: usize, + pts: gst::ClockTime, + flags: gst::BufferFlags, +) -> gst::Buffer { + const PACKET_SIZE: usize = 188; + + let mut ts_packet = MPEGTS_DATA[(packet_number * PACKET_SIZE)..].to_vec(); + + assert!(ts_packet.starts_with(&[0x47])); + + ts_packet.truncate(n_packets * PACKET_SIZE); + + // Add filler packets if needed + while ts_packet.len() < (n_packets * PACKET_SIZE) { + ts_packet.extend_from_slice(&[0x47, 0x1f, 0xff, 0x10]); + + while ts_packet.len() % PACKET_SIZE != 0 { + ts_packet.extend_from_slice(&[0, 0, 0, 0]); + } + } + + let mut buf = gst::Buffer::from_mut_slice(ts_packet); + + if let Some(buf_ref) = buf.get_mut() { + buf_ref.set_pts(pts); + buf_ref.set_flags(flags); + } + + buf +} + +#[test] +fn test_mp2t_pay_depay_single_ts_packets() { + init(); + + // mpegtsmux would first push these caps and then update with a streamheader + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", 188i32) + .build(); + + let mut input_buffers = Vec::with_capacity(24); + + // No DISCONT flag on first buffer for some reason.. + // - 15 packets with timestamp 0ms + // - 4 packets with timestamp 40ms + // - 4 packets with timestamp 80ms + for i in 0..23 { + input_buffers.push(make_mp2t_buffer( + 0, + 1, + match i { + 0..=14 => gst::ClockTime::ZERO, + 15..=18 => gst::ClockTime::from_mseconds(40), + 19..=23 => gst::ClockTime::from_mseconds(80), + _ => unreachable!(), + }, + if i == 0 { + gst::BufferFlags::empty() + } else { + gst::BufferFlags::DELTA_UNIT + }, + )); + } + + let expected_pay = vec![ + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(7200) // 80ms, 0.080 * 90000 + .marker_bit(false) + .build()], + ]; + + let expected_depay = vec![ + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .size(376) + .flags(gst::BufferFlags::empty()) + .build()], + ]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2", + "rtpmp2tdepay2", + expected_pay, + expected_depay, + ); +} + +#[test] +fn test_mp2t_pay_depay_7ts_packets() { + init(); + + // mpegtsmux would first push these caps and then update with a streamheader + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", 188i32) + .build(); + + let input_buffers = vec![ + // This time we're not feeding single ts packets, but 7 per buffer (like alignment=7) + make_mp2t_buffer(0, 7, gst::ClockTime::ZERO, gst::BufferFlags::empty()), + make_mp2t_buffer(7, 7, gst::ClockTime::ZERO, gst::BufferFlags::DELTA_UNIT), + make_mp2t_buffer(14, 7, gst::ClockTime::ZERO, gst::BufferFlags::DELTA_UNIT), + make_mp2t_buffer( + 21, + 7, + gst::ClockTime::from_mseconds(80), + gst::BufferFlags::DELTA_UNIT, + ), + ]; + + let expected_pay = vec![ + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(7200) // 80ms, 0.080 * 90000 + .marker_bit(false) + .build()], + ]; + + let expected_depay = vec![ + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .size(1316) // includes padding packets + .flags(gst::BufferFlags::empty()) + .build()], + ]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2", + "rtpmp2tdepay2", + expected_pay, + expected_depay, + ); +} + +#[test] +fn test_mp2t_pay_depay_7ts_packets_mtu_split() { + init(); + + // mpegtsmux would first push these caps and then update with a streamheader + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", 188i32) + .build(); + + let input_buffers = vec![ + // This time we're not feeding single ts packets, but 7 per buffer (like alignment=7) + make_mp2t_buffer(0, 7, gst::ClockTime::ZERO, gst::BufferFlags::empty()), + ]; + + let expected_pay = vec![vec![ + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ]]; + + let expected_depay = vec![ + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(188) + .flags(gst::BufferFlags::empty()) + .build()], + ]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2 mtu=300", + "rtpmp2tdepay2", + expected_pay, + expected_depay, + ); +} + +#[test] +fn test_mp2t_pay_depay_au_ts_packets() { + init(); + + // mpegtsmux would first push these caps and then update with a streamheader + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", 188i32) + .build(); + + let input_buffers = vec![ + // This time we're not feeding single ts packets, but chunk per timestamp/AU + make_mp2t_buffer(0, 15, gst::ClockTime::ZERO, gst::BufferFlags::empty()), + make_mp2t_buffer( + 15, + 4, + gst::ClockTime::from_mseconds(40), + gst::BufferFlags::DELTA_UNIT, + ), + make_mp2t_buffer( + 19, + 4, + gst::ClockTime::from_mseconds(80), + gst::BufferFlags::DELTA_UNIT, + ), + ]; + + let expected_pay = vec![ + // Since the first input buffer contains 15 ts packets, the payloader can push out + // two rtp packets of 7 ts packets immediately, with 1 ts packet queued for later. + vec![ + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build()], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(7200) // 80ms, 0.080 * 90000 + .marker_bit(false) + .build()], + ]; + + let expected_depay = vec![ + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(1316) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .size(376) + .flags(gst::BufferFlags::empty()) + .build()], + ]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2", + "rtpmp2tdepay2", + expected_pay, + expected_depay, + ); +} + +// gst-launch-1.0 videotestsrc num-buffers=3 is-live=true \ +// ! video/x-raw,format=I420,width=80,height=60,framerate=25/1 \ +// ! x264enc tune=zerolatency \ +// ! mpegtsmux m2ts-mode=true \ +// ! filesink location=videotestsrc-80x60-h264.m2ts \ +// && truncate -s 3648 videotestsrc-80x60-h264.m2ts +// +// Same as above, just with 192-byte packets, and it seems like we only get 19 ts packets +const M2TS_DATA: &[u8] = include_bytes!("videotestsrc-80x60-h264.m2ts").as_slice(); + +#[test] +fn test_mp2t_pay_depay_m2ts_variant() { + init(); + + // mpegtsmux would first push these caps and then update with a streamheader + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", 192i32) + .build(); + + // Send as single input buffer, we just want to make sure the + // depayloader can automatically detect the packet size. + let mut input_buf = gst::Buffer::from_slice(M2TS_DATA); + + if let Some(buf_ref) = input_buf.get_mut() { + buf_ref.set_pts(gst::ClockTime::ZERO); + buf_ref.set_flags(gst::BufferFlags::empty()); + } + + let input_buffers = vec![input_buf]; + + let expected_pay = vec![ + // Since the first input buffer contains 21 ts packets, the payloader can push out + // 3 rtp packets immediately, with 2 ts packets queued for later. + vec![ + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build(), + ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::empty()) + .pt(33) + .rtp_time(0) + .marker_bit(false) + .build(), + ], + vec![ExpectedPacket::builder() + .pts(gst::ClockTime::from_mseconds(80)) + .flags(gst::BufferFlags::empty()) + .pt(33) + .pts(gst::ClockTime::ZERO) + .marker_bit(false) + .build()], + ]; + + let expected_depay = vec![ + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(7 * 192) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(7 * 192) + .flags(gst::BufferFlags::empty()) + .build()], + vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(5 * 192) + .flags(gst::BufferFlags::empty()) + .build()], + ]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2", + "rtpmp2tdepay2", + expected_pay, + expected_depay, + ); +} + +#[test] +fn test_mp2t_pay_depay_single_packet() { + let inputs = [(188u8, MPEGTS_DATA), (192u8, M2TS_DATA)]; + + init(); + + for (packet_size, data) in inputs { + // mpegtsmux would first push these caps and then update with a streamheader + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", packet_size as i32) + .build(); + + let packet_size = packet_size as usize; + + // Send a single TS packet as input buffer, to make sure the + // depayloader can still automatically detect the packet size. + let mut input_buf = gst::Buffer::from_slice(&data[0..][..packet_size]); + + if let Some(buf_ref) = input_buf.get_mut() { + buf_ref.set_pts(gst::ClockTime::ZERO); + buf_ref.set_flags(gst::BufferFlags::empty()); + } + + let input_buffers = vec![input_buf]; + + let expected_pay = vec![vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build()]]; + + let expected_depay = vec![vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(packet_size) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()]]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2", + "rtpmp2tdepay2", + expected_pay, + expected_depay, + ); + } +} + +#[test] +fn test_mp2t_depay_skip_bytes() { + init(); + + let input_caps = gst::Caps::builder("video/mpegts") + .field("systemstream", true) + .field("packetsize", 192i32) + .build(); + + // Send a single 192-byte TS packet as input buffer, and strip off the first 4 bytes + // (extra timestamp), which should yield a normal 188-byte TS packet. Need to send + // a single one since skip-first-bytes only applies to the whole payload buffer, + // not each individual TS packet, so it wouldn't work right if there were multiple + // TS packets (because it's made for stripping off other things, not TS packet prefixes). + let mut input_buf = gst::Buffer::from_slice(&M2TS_DATA[0..][..192]); + + if let Some(buf_ref) = input_buf.get_mut() { + buf_ref.set_pts(gst::ClockTime::ZERO); + buf_ref.set_flags(gst::BufferFlags::empty()); + } + + let input_buffers = vec![input_buf]; + + let expected_pay = vec![vec![ExpectedPacket::builder() + .pts(gst::ClockTime::ZERO) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER) + .pt(33) + .rtp_time(0) + .marker_bit(true) + .build()]]; + + let expected_depay = vec![vec![ExpectedBuffer::builder() + .pts(gst::ClockTime::ZERO) + .size(192 - 4) + .flags(gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC) + .build()]]; + + run_test_pipeline( + Source::Buffers(input_caps, input_buffers), + "rtpmp2tpay2", + "rtpmp2tdepay2 skip-first-bytes=4", + expected_pay, + expected_depay, + ); +} diff --git a/net/rtp/src/mp2t/tests/videotestsrc-80x60-h264.m2ts b/net/rtp/src/mp2t/tests/videotestsrc-80x60-h264.m2ts new file mode 100644 index 0000000000000000000000000000000000000000..bb890badb7c5cc1c4716b36810d4f1a69b499381 GIT binary patch literal 3648 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 KH@?3n*!>IL=WR~_ literal 0 HcmV?d00001