139 lines
4.5 KiB
Rust
139 lines
4.5 KiB
Rust
|
|
use super::*;
|
||
|
|
use lesavka_common::lesavka::AudioEncoding;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn opus_decoder_roundtrips_to_pcm_when_plugins_are_available() {
|
||
|
|
let _ = gst::init();
|
||
|
|
if gst::ElementFactory::find("opusenc").is_none()
|
||
|
|
|| gst::ElementFactory::find("opusdec").is_none()
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let Some(opus_payload) = encode_silent_opus_payload() else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let packet = AudioPacket {
|
||
|
|
pts: 42_000,
|
||
|
|
encoding: AudioEncoding::Opus as i32,
|
||
|
|
sample_rate: 48_000,
|
||
|
|
channels: 2,
|
||
|
|
frame_duration_us: 20_000,
|
||
|
|
data: opus_payload,
|
||
|
|
..AudioPacket::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut decoder = OpusPacketDecoder::new().expect("opus decoder");
|
||
|
|
let decoded = decoder
|
||
|
|
.decode_packet(&packet)
|
||
|
|
.expect("decode")
|
||
|
|
.expect("decoded pcm");
|
||
|
|
|
||
|
|
assert_eq!(decoded.pts, 42_000);
|
||
|
|
assert_eq!(decoded.encoding, AudioEncoding::PcmS16le as i32);
|
||
|
|
assert_eq!(decoded.sample_rate, 48_000);
|
||
|
|
assert_eq!(decoded.channels, 2);
|
||
|
|
assert!(
|
||
|
|
decoded.data.len() >= 1_000,
|
||
|
|
"decoded PCM should be far larger than one compressed Opus frame"
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
decoded.data.chunks_exact(2).any(|sample| {
|
||
|
|
i16::from_le_bytes([sample[0], sample[1]]).unsigned_abs() > 250
|
||
|
|
}),
|
||
|
|
"decoded Opus payload should preserve non-silent waveform energy"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn packet_metadata_keeps_timing_and_clears_payload() {
|
||
|
|
let packet = AudioPacket {
|
||
|
|
pts: 123_000,
|
||
|
|
encoding: AudioEncoding::Opus as i32,
|
||
|
|
sample_rate: 48_000,
|
||
|
|
channels: 2,
|
||
|
|
frame_duration_us: 20_000,
|
||
|
|
data: vec![1, 2, 3, 4],
|
||
|
|
..AudioPacket::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let metadata = packet_metadata(&packet);
|
||
|
|
|
||
|
|
assert_eq!(metadata.pts, 123_000);
|
||
|
|
assert_eq!(metadata.encoding, AudioEncoding::Opus as i32);
|
||
|
|
assert_eq!(metadata.sample_rate, 48_000);
|
||
|
|
assert_eq!(metadata.channels, 2);
|
||
|
|
assert_eq!(metadata.frame_duration_us, 20_000);
|
||
|
|
assert!(metadata.data.is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn pending_packet_metadata_is_bounded_to_recent_frames() {
|
||
|
|
let mut pending = VecDeque::new();
|
||
|
|
|
||
|
|
for pts in 0..20 {
|
||
|
|
push_pending_packet(
|
||
|
|
&mut pending,
|
||
|
|
AudioPacket {
|
||
|
|
pts,
|
||
|
|
..AudioPacket::default()
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
assert_eq!(pending.len(), MAX_PENDING_OPUS_METADATA);
|
||
|
|
assert_eq!(pending.front().expect("oldest retained").pts, 4);
|
||
|
|
assert_eq!(pending.back().expect("newest retained").pts, 19);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn encode_silent_opus_payload() -> Option<Vec<u8>> {
|
||
|
|
let desc = "\
|
||
|
|
appsrc name=src is-live=true block=false format=time \
|
||
|
|
caps=audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=48000 ! \
|
||
|
|
opusenc audio-type=restricted-lowdelay bitrate=96000 bitrate-type=cbr complexity=7 frame-size=20 perfect-timestamp=true hard-resync=true ! \
|
||
|
|
appsink name=sink emit-signals=false sync=false max-buffers=8 drop=true";
|
||
|
|
let pipeline: gst::Pipeline = gst::parse::launch(desc).ok()?.downcast().ok()?;
|
||
|
|
let appsrc = pipeline
|
||
|
|
.by_name("src")?
|
||
|
|
.downcast::<gst_app::AppSrc>()
|
||
|
|
.ok()?;
|
||
|
|
let appsink = pipeline
|
||
|
|
.by_name("sink")?
|
||
|
|
.downcast::<gst_app::AppSink>()
|
||
|
|
.ok()?;
|
||
|
|
pipeline.set_state(gst::State::Playing).ok()?;
|
||
|
|
|
||
|
|
for index in 0..4u64 {
|
||
|
|
let mut buffer = gst::Buffer::from_slice(sine_pcm_packet(index, 3_840));
|
||
|
|
if let Some(meta) = buffer.get_mut() {
|
||
|
|
let pts = gst::ClockTime::from_useconds(index * 20_000);
|
||
|
|
meta.set_pts(Some(pts));
|
||
|
|
meta.set_dts(Some(pts));
|
||
|
|
meta.set_duration(Some(gst::ClockTime::from_useconds(20_000)));
|
||
|
|
}
|
||
|
|
appsrc.push_buffer(buffer).ok()?;
|
||
|
|
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(50)) {
|
||
|
|
let payload = sample.buffer()?.map_readable().ok()?.to_vec();
|
||
|
|
let _ = pipeline.set_state(gst::State::Null);
|
||
|
|
if !payload.is_empty() {
|
||
|
|
return Some(payload);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let _ = pipeline.set_state(gst::State::Null);
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sine_pcm_packet(packet_index: u64, len: usize) -> Vec<u8> {
|
||
|
|
let mut out = Vec::with_capacity(len);
|
||
|
|
let frames = len / 4;
|
||
|
|
for frame in 0..frames {
|
||
|
|
let absolute = packet_index as usize * frames + frame;
|
||
|
|
let phase = (absolute as f32 * 440.0 * std::f32::consts::TAU) / 48_000.0;
|
||
|
|
let sample = (phase.sin() * 12_000.0) as i16;
|
||
|
|
out.extend_from_slice(&sample.to_le_bytes());
|
||
|
|
out.extend_from_slice(&sample.to_le_bytes());
|
||
|
|
}
|
||
|
|
out
|
||
|
|
}
|