diff --git a/client/src/input/audio_codec.rs b/client/src/input/audio_codec.rs index 5474409..afe1187 100644 --- a/client/src/input/audio_codec.rs +++ b/client/src/input/audio_codec.rs @@ -199,6 +199,30 @@ mod tests { UpstreamAudioCodec::PcmS16le ); }); + temp_env::with_var(AUDIO_CODEC_ENV, Some("opus"), || { + temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("pcm"), || { + assert_eq!( + requested_upstream_audio_codec_from_env(), + UpstreamAudioCodec::Opus + ); + }); + }); + temp_env::with_var(AUDIO_CODEC_ENV, None::<&str>, || { + temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("compressed"), || { + assert_eq!( + requested_upstream_audio_codec_from_env(), + UpstreamAudioCodec::Opus + ); + }); + }); + temp_env::with_var(AUDIO_CODEC_ENV, Some("aac"), || { + temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("opus"), || { + assert_eq!( + requested_upstream_audio_codec_from_env(), + UpstreamAudioCodec::PcmS16le + ); + }); + }); } #[test] @@ -275,6 +299,47 @@ mod tests { ); } + #[test] + fn packet_metadata_keeps_capture_timing_without_pcm_payload() { + let packet = AudioPacket { + pts: 987_000, + encoding: AudioEncoding::PcmS16le as i32, + sample_rate: 48_000, + channels: 2, + frame_duration_us: 20_000, + data: vec![0x11; 3_840], + ..AudioPacket::default() + }; + + let metadata = packet_metadata(&packet); + + assert_eq!(metadata.pts, 987_000); + assert_eq!(metadata.encoding, AudioEncoding::PcmS16le 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_drops_oldest_when_encoder_lags() { + let mut pending = VecDeque::new(); + + for pts in 100..120 { + 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, 104); + assert_eq!(pending.back().expect("newest retained").pts, 119); + } + fn sine_pcm_packet(freq_hz: f32, len: usize) -> Vec { let mut out = Vec::with_capacity(len); let frames = len / 4; diff --git a/server/src/audio/opus_decode.rs b/server/src/audio/opus_decode.rs index 41ab81a..30a6e3a 100644 --- a/server/src/audio/opus_decode.rs +++ b/server/src/audio/opus_decode.rs @@ -195,6 +195,47 @@ mod tests { ); } + #[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> { let desc = "\ appsrc name=src is-live=true block=false format=time \ diff --git a/tests/unit/common/audio/common_audio_transport_unit.rs b/tests/unit/common/audio/common_audio_transport_unit.rs index 78448d0..9a85c46 100644 --- a/tests/unit/common/audio/common_audio_transport_unit.rs +++ b/tests/unit/common/audio/common_audio_transport_unit.rs @@ -96,14 +96,42 @@ fn marking_helpers_keep_current_pcm_path_explicit() { #[test] fn upstream_audio_codec_parser_keeps_opus_and_pcm_names_stable() { + assert_eq!(UpstreamAudioCodec::PcmS16le.as_id(), "pcm"); + assert_eq!(UpstreamAudioCodec::PcmS16le.label(), "PCM"); + assert_eq!( + UpstreamAudioCodec::PcmS16le.profile(), + AudioTransportProfile::pcm_s16le() + ); + assert_eq!(UpstreamAudioCodec::Opus.as_id(), "opus"); + assert_eq!(UpstreamAudioCodec::Opus.label(), "Opus"); + assert_eq!( + UpstreamAudioCodec::Opus.profile(), + AudioTransportProfile::opus_voice() + ); assert_eq!( parse_upstream_audio_codec("opus"), Some(UpstreamAudioCodec::Opus) ); + assert_eq!( + parse_upstream_audio_codec(" compressed "), + Some(UpstreamAudioCodec::Opus) + ); + assert_eq!( + parse_upstream_audio_codec("voice"), + Some(UpstreamAudioCodec::Opus) + ); assert_eq!( parse_upstream_audio_codec("raw"), Some(UpstreamAudioCodec::PcmS16le) ); + assert_eq!( + parse_upstream_audio_codec("PCM_S16LE"), + Some(UpstreamAudioCodec::PcmS16le) + ); + assert_eq!( + parse_upstream_audio_codec("uncompressed"), + Some(UpstreamAudioCodec::PcmS16le) + ); assert_eq!(parse_upstream_audio_codec("aac"), None); let mut packet = AudioPacket { @@ -117,3 +145,64 @@ fn upstream_audio_codec_parser_keeps_opus_and_pcm_names_stable() { AudioTransportProfile::opus_voice() ); } + +#[test] +fn packet_profiles_preserve_explicit_nonzero_pcm_metadata() { + let packet = AudioPacket { + encoding: AudioEncoding::PcmS16le as i32, + sample_rate: 44_100, + channels: 1, + frame_duration_us: 10_000, + ..AudioPacket::default() + }; + + assert_eq!( + packet_audio_profile(&packet), + AudioTransportProfile { + encoding: AudioEncoding::PcmS16le, + sample_rate: 44_100, + channels: 1, + frame_duration_us: 10_000, + target_bitrate_bps: None, + } + ); +} + +#[test] +fn bundle_profiles_fallback_from_envelope_to_first_packet() { + let packet = AudioPacket { + encoding: AudioEncoding::Opus as i32, + frame_duration_us: 40_000, + ..AudioPacket::default() + }; + let bundle = UpstreamMediaBundle { + audio: vec![packet], + ..UpstreamMediaBundle::default() + }; + + assert_eq!( + bundle_audio_profile(&bundle), + AudioTransportProfile { + encoding: AudioEncoding::Opus, + sample_rate: 48_000, + channels: 2, + frame_duration_us: 40_000, + target_bitrate_bps: Some(96_000), + } + ); + assert_eq!( + bundle_audio_profile(&UpstreamMediaBundle::default()), + AudioTransportProfile::pcm_s16le() + ); +} + +#[test] +fn marking_helpers_fill_default_frame_duration_when_absent() { + let mut pcm_packet = AudioPacket::default(); + mark_packet_pcm_s16le(&mut pcm_packet); + assert_eq!(pcm_packet.frame_duration_us, 20_000); + + let mut opus_packet = AudioPacket::default(); + mark_packet_opus(&mut opus_packet); + assert_eq!(opus_packet.frame_duration_us, 20_000); +}