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
}