use crate::adaptive_quality_constants::{
AUDIO_RED_FORMAT, AUDIO_RED_SEQ_HISTORY_SIZE, OPUS_FRAME_DURATION_MS,
};
use crate::audio::shared_audio_context::SharedAudioContext;
use crate::audio_constants::{
rms_to_intensity, AUDIO_LEVEL_DELTA_THRESHOLD, DEFAULT_VAD_THRESHOLD,
};
use crate::constants::{AUDIO_CHANNELS, AUDIO_SAMPLE_RATE};
use crate::decode::{AudioPeerDecoderTrait, DecodeStatus};
use js_sys::Float32Array;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_wasm_bindgen;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use std::sync::Arc;
use videocall_diagnostics::{global_sender, metric, now_ms, DiagEvent};
use videocall_types::protos::media_packet::MediaPacket;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{AudioContext, AudioWorkletNode, MessageEvent, Worker};
const WORKLET_CODE: &str = include_str!("../scripts/pcmPlayerWorker.js");
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")]
enum WorkerMsg {
Init {
sample_rate: u32,
channels: u8,
},
Insert {
seq: u16,
timestamp: u32,
#[serde(with = "serde_bytes")]
payload: Vec<u8>,
},
Flush,
Clear,
Close,
Mute {
muted: bool,
},
SetDiagnostics {
enabled: bool,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum WorkerResponse {
WorkerReady {
mute_state: bool,
},
Stats {
#[serde(skip)]
stats: JsValue, },
}
#[derive(Debug)]
pub struct NetEqAudioPeerDecoder {
worker: Worker,
_audio_context: AudioContext,
decoded: bool,
peer_id: String, _pcm_player: Rc<RefCell<Option<AudioWorkletNode>>>,
pending_messages: Rc<RefCell<VecDeque<WorkerMsg>>>,
worker_ready: Rc<RefCell<bool>>,
speaking: Rc<RefCell<bool>>,
audio_level: Rc<RefCell<f32>>,
received_sequences: VecDeque<u64>,
}
impl NetEqAudioPeerDecoder {
fn send_worker_message(&self, msg: WorkerMsg) {
let is_ready = *self.worker_ready.borrow();
if is_ready {
self.send_message_immediate(msg);
} else {
log::debug!(
"🔄 Queueing message for peer {} (worker not ready)",
self.peer_id
);
self.pending_messages.borrow_mut().push_back(msg);
}
}
fn send_message_immediate(&self, msg: WorkerMsg) {
if let Err(e) =
serde_wasm_bindgen::to_value(&msg).map(|js_msg| self.worker.post_message(&js_msg))
{
log::error!("Failed to send worker message: {e:?}");
web_sys::console::error_1(&format!("Failed to send worker message: {e:?}").into());
}
}
fn create_neteq_worker() -> Result<Worker, JsValue> {
let window = web_sys::window().expect("no window");
let document = window.document().expect("no document");
let worker_url = document
.get_element_by_id("neteq-worker")
.expect("neteq-worker link tag not found")
.get_attribute("href")
.expect("link tag has no href");
Worker::new(&worker_url)
}
fn send_pcm_to_safari_worklet(pcm_player: &AudioWorkletNode, pcm: &Float32Array) {
let message = js_sys::Object::new();
js_sys::Reflect::set(&message, &"command".into(), &"play".into()).unwrap();
js_sys::Reflect::set(&message, &"pcm".into(), pcm).unwrap();
if let Err(e) = pcm_player.port().unwrap().post_message(&message) {
web_sys::console::warn_1(
&format!("Safari: Failed to send PCM to worklet: {e:?}").into(),
);
}
}
async fn create_safari_audio_context(
speaker_device_id: Option<String>,
) -> Result<(AudioContext, AudioWorkletNode), JsValue> {
let audio_context = SharedAudioContext::get_or_init(speaker_device_id.clone())?;
SharedAudioContext::ensure_pcm_worklet_ready(WORKLET_CODE).await?;
let (pcm_player, _peer_gain) = SharedAudioContext::create_peer_playback_nodes("safari")?;
Ok((audio_context, pcm_player))
}
fn calculate_rms(pcm: &Float32Array) -> f32 {
let length = pcm.length() as usize;
if length == 0 {
return 0.0;
}
let mut sum_squares: f32 = 0.0;
for i in 0..length {
let sample = pcm.get_index(i as u32);
sum_squares += sample * sample;
}
(sum_squares / length as f32).sqrt()
}
#[allow(clippy::too_many_arguments)]
fn handle_pcm_data(
pcm: Float32Array,
pcm_player: Rc<RefCell<Option<AudioWorkletNode>>>,
audio_context: &AudioContext,
speaker_device_id: Option<String>,
peer_id: String,
speaking: Rc<RefCell<bool>>,
audio_level: Rc<RefCell<f32>>,
vad_threshold: f32,
) {
let rms = Self::calculate_rms(&pcm);
let is_speaking = rms > vad_threshold;
let intensity = rms_to_intensity(rms, vad_threshold);
let prev_speaking = *speaking.borrow();
let prev_level = *audio_level.borrow();
let level_changed = (intensity - prev_level).abs() > AUDIO_LEVEL_DELTA_THRESHOLD;
if is_speaking != prev_speaking || level_changed {
*speaking.borrow_mut() = is_speaking;
*audio_level.borrow_mut() = intensity;
let _ = global_sender().try_broadcast(DiagEvent {
subsystem: "peer_speaking",
stream_id: Some(format!("speaking->{peer_id}")),
ts_ms: now_ms(),
metrics: vec![
metric!("to_peer", peer_id.clone()),
metric!("speaking", if is_speaking { 1u64 } else { 0u64 }),
metric!("audio_level", intensity as f64),
],
});
}
if let Err(e) = audio_context.resume() {
web_sys::console::warn_1(
&format!("[neteq-audio-decoder] AudioContext resume error: {e:?}").into(),
);
}
let pcm_player_clone = pcm_player.clone();
wasm_bindgen_futures::spawn_local(async move {
Self::ensure_worklet_initialized(&pcm_player_clone, speaker_device_id).await;
if let Some(ref worklet) = *pcm_player_clone.borrow() {
Self::send_pcm_to_safari_worklet(worklet, &pcm);
}
});
}
async fn ensure_worklet_initialized(
pcm_player: &Rc<RefCell<Option<AudioWorkletNode>>>,
speaker_device_id: Option<String>,
) {
if pcm_player.borrow().is_some() {
return;
}
log::info!("Initializing AudioWorklet for PCM playback");
match Self::create_safari_audio_context(speaker_device_id).await {
Ok((_, worklet)) => {
*pcm_player.borrow_mut() = Some(worklet);
log::info!("AudioWorklet initialized successfully");
}
Err(e) => {
web_sys::console::error_2(&"Failed to initialize worklet:".into(), &e);
}
}
}
fn handle_stats_message(data: &JsValue, peer_id: &str) {
let obj = match data.dyn_ref::<js_sys::Object>() {
Some(obj) => obj,
None => return,
};
let cmd =
js_sys::Reflect::get(obj, &JsValue::from_str("cmd")).unwrap_or(JsValue::UNDEFINED);
if cmd.as_string().as_deref() != Some("stats") {
return;
}
let stats_js = match js_sys::Reflect::get(obj, &JsValue::from_str("stats")) {
Ok(stats) => stats,
Err(_) => return,
};
let stats_json = match js_sys::JSON::stringify(&stats_js) {
Ok(json) => json,
Err(_) => return,
};
let json_str = match stats_json.as_string() {
Some(s) => s,
None => return,
};
Self::emit_stats_diagnostics(&json_str, peer_id);
Self::emit_parsed_metrics(&json_str, peer_id);
}
fn emit_stats_diagnostics(json_str: &str, peer_id: &str) {
let current_user = "current_user";
let _ = global_sender().try_broadcast(DiagEvent {
subsystem: "neteq",
stream_id: Some(format!("{current_user}->{peer_id}")), ts_ms: now_ms(),
metrics: vec![
metric!("stats_json", json_str.to_string()),
metric!("reporting_peer", current_user),
metric!("target_peer", peer_id.to_string()),
],
});
}
fn emit_parsed_metrics(json_str: &str, peer_id: &str) {
let parsed: Value = match serde_json::from_str(json_str) {
Ok(p) => p,
Err(_) => return,
};
Self::emit_jitter_metrics(&parsed, peer_id);
Self::emit_buffer_metrics(&parsed, peer_id);
}
fn emit_jitter_metrics(parsed: &Value, peer_id: &str) {
let lifetime = match parsed.get("lifetime") {
Some(l) => l,
None => return,
};
if let Some(jitter) = lifetime
.get("jitter_buffer_delay_ms")
.and_then(|v| v.as_u64())
{
let _ = global_sender().try_broadcast(DiagEvent {
subsystem: "neteq",
stream_id: Some(format!("current_user->{peer_id}")),
ts_ms: now_ms(),
metrics: vec![
metric!("jitter_buffer_delay_ms", jitter),
metric!("reporting_peer", "current_user"),
metric!("target_peer", peer_id.to_string()),
],
});
}
if let Some(target) = lifetime
.get("jitter_buffer_target_delay_ms")
.and_then(|v| v.as_u64())
{
let _ = global_sender().try_broadcast(DiagEvent {
subsystem: "neteq",
stream_id: Some(format!("current_user->{peer_id}")),
ts_ms: now_ms(),
metrics: vec![
metric!("jitter_buffer_target_delay_ms", target),
metric!("reporting_peer", "current_user"),
metric!("target_peer", peer_id.to_string()),
],
});
}
}
fn emit_buffer_metrics(parsed: &Value, peer_id: &str) {
let network = match parsed.get("network") {
Some(n) => n,
None => return,
};
if let Some(buf) = network
.get("current_buffer_size_ms")
.and_then(|v| v.as_u64())
{
let _ = global_sender().try_broadcast(DiagEvent {
subsystem: "neteq",
stream_id: Some(format!("current_user->{peer_id}")),
ts_ms: now_ms(),
metrics: vec![
metric!("audio_buffer_ms", buf),
metric!("reporting_peer", "current_user"),
metric!("target_peer", peer_id.to_string()),
],
});
}
if let Some(packets) = network
.get("packets_awaiting_decode")
.and_then(|v| v.as_u64())
{
let _ = global_sender().try_broadcast(DiagEvent {
subsystem: "neteq",
stream_id: Some(format!("current_user->{peer_id}")),
ts_ms: now_ms(),
metrics: vec![
metric!("packets_awaiting_decode", packets),
metric!("reporting_peer", "current_user"),
metric!("target_peer", peer_id.to_string()),
],
});
}
}
#[allow(clippy::too_many_arguments)]
fn create_message_handler(
pcm_player: Rc<RefCell<Option<AudioWorkletNode>>>,
audio_context: AudioContext,
peer_id: String,
speaker_device_id: Option<String>,
worker_ready: Rc<RefCell<bool>>,
pending_messages: Rc<RefCell<VecDeque<WorkerMsg>>>,
worker: Worker,
speaking: Rc<RefCell<bool>>,
audio_level: Rc<RefCell<f32>>,
vad_threshold: f32,
) -> Closure<dyn FnMut(MessageEvent)> {
Closure::wrap(Box::new(move |event: MessageEvent| {
let data = event.data();
if data.is_instance_of::<Float32Array>() {
let pcm = Float32Array::from(data);
Self::handle_pcm_data(
pcm,
pcm_player.clone(),
&audio_context,
speaker_device_id.clone(),
peer_id.clone(),
speaking.clone(),
audio_level.clone(),
vad_threshold,
);
} else if data.is_object() {
if let Ok(response) = serde_wasm_bindgen::from_value::<WorkerResponse>(data.clone())
{
match response {
WorkerResponse::WorkerReady { mute_state } => {
log::info!(
"✅ Worker ready for peer {peer_id} (worker mute: {mute_state})"
);
*worker_ready.borrow_mut() = true;
let mut queue = pending_messages.borrow_mut();
let queue_length = queue.len();
if queue_length > 0 {
log::info!(
"📤 Flushing {queue_length} queued messages for peer {peer_id}"
);
while let Some(msg) = queue.pop_front() {
if let Err(e) = serde_wasm_bindgen::to_value(&msg)
.map(|js_msg| worker.post_message(&js_msg))
{
log::error!("Failed to send queued message: {e:?}");
} else {
log::debug!("📤 Sent queued message: {msg:?}");
}
}
}
}
WorkerResponse::Stats { .. } => {
Self::handle_stats_message(&data, &peer_id);
}
}
} else {
Self::handle_stats_message(&data, &peer_id);
}
}
}) as Box<dyn FnMut(_)>)
}
pub fn new_with_muted_state(
speaker_device_id: Option<String>,
peer_id: String,
vad_threshold: Option<f32>,
) -> Result<Box<dyn AudioPeerDecoderTrait>, JsValue> {
Self::new_with_mute_state(speaker_device_id, peer_id, true, vad_threshold)
}
pub fn new_with_mute_state(
speaker_device_id: Option<String>,
peer_id: String,
initial_muted: bool,
vad_threshold: Option<f32>,
) -> Result<Box<dyn AudioPeerDecoderTrait>, JsValue> {
let worker = Self::create_neteq_worker()?;
let audio_context = SharedAudioContext::get_or_init(speaker_device_id.clone())?;
SharedAudioContext::ensure_pcm_worklet(WORKLET_CODE);
let pcm_player_ref = Rc::new(RefCell::new(None::<AudioWorkletNode>));
let threshold = vad_threshold.unwrap_or(DEFAULT_VAD_THRESHOLD);
let mut decoder = Self {
worker: worker.clone(),
_audio_context: audio_context.clone(),
decoded: false,
peer_id: peer_id.clone(),
_pcm_player: pcm_player_ref.clone(),
pending_messages: Rc::new(RefCell::new(VecDeque::new())),
worker_ready: Rc::new(RefCell::new(false)),
speaking: Rc::new(RefCell::new(false)),
audio_level: Rc::new(RefCell::new(0.0)),
received_sequences: VecDeque::with_capacity(AUDIO_RED_SEQ_HISTORY_SIZE),
};
let on_message_closure = Self::create_message_handler(
pcm_player_ref.clone(),
audio_context.clone(),
peer_id.clone(),
speaker_device_id.clone(),
decoder.worker_ready.clone(),
decoder.pending_messages.clone(),
worker.clone(),
decoder.speaking.clone(),
decoder.audio_level.clone(),
threshold,
);
worker.set_onmessage(Some(on_message_closure.as_ref().unchecked_ref()));
on_message_closure.forget();
let init_msg = WorkerMsg::Init {
sample_rate: AUDIO_SAMPLE_RATE,
channels: AUDIO_CHANNELS as u8,
};
let init_js = serde_wasm_bindgen::to_value(&init_msg)?;
let worker_clone = worker.clone();
let send_cb = Closure::wrap(Box::new(move || {
if let Err(e) = worker_clone.post_message(&init_js) {
web_sys::console::error_2(&"[neteq-audio-decoder] failed to post Init:".into(), &e);
}
}) as Box<dyn FnMut()>);
web_sys::window()
.expect("no window")
.set_timeout_with_callback_and_timeout_and_arguments_0(
send_cb.as_ref().unchecked_ref(),
10,
)?;
send_cb.forget();
log::info!("NetEq audio decoder initialized for peer {peer_id} (muted: {initial_muted})");
decoder.set_muted(initial_muted);
log::info!(
"✅ NetEq decoder initialized for peer {} with muted: {}",
decoder.peer_id,
initial_muted
);
decoder.send_worker_message(WorkerMsg::SetDiagnostics { enabled: true });
log::info!(
"🔧 Enabled diagnostics for NetEq worker for peer {}",
decoder.peer_id
);
Ok(Box::new(decoder))
}
fn record_sequence(&mut self, seq: u64) {
if self.received_sequences.len() >= AUDIO_RED_SEQ_HISTORY_SIZE {
self.received_sequences.pop_front();
}
self.received_sequences.push_back(seq);
}
fn has_sequence(&self, seq: u64) -> bool {
self.received_sequences.contains(&seq)
}
fn unpack_red_audio(data: &[u8]) -> Option<(Vec<u8>, u32, Vec<u8>)> {
if data.len() < 8 {
return None;
}
let primary_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
if primary_len > 10_000 {
return None;
}
let redundant_seq_offset = 4 + primary_len;
if redundant_seq_offset + 4 > data.len() {
return None;
}
let primary_data = data[4..4 + primary_len].to_vec();
let redundant_seq = u32::from_le_bytes([
data[redundant_seq_offset],
data[redundant_seq_offset + 1],
data[redundant_seq_offset + 2],
data[redundant_seq_offset + 3],
]);
let redundant_data = data[redundant_seq_offset + 4..].to_vec();
Some((primary_data, redundant_seq, redundant_data))
}
#[cfg(test)]
pub fn unpack_red_audio_public(data: &[u8]) -> Option<(Vec<u8>, u32, Vec<u8>)> {
Self::unpack_red_audio(data)
}
}
impl Drop for NetEqAudioPeerDecoder {
fn drop(&mut self) {
self.worker.terminate();
}
}
impl crate::decode::AudioPeerDecoderTrait for NetEqAudioPeerDecoder {
fn decode(&mut self, packet: &Arc<MediaPacket>) -> anyhow::Result<DecodeStatus> {
match packet.audio_metadata.as_ref() {
Some(audio_meta) => {
let seq = audio_meta.sequence;
self.record_sequence(seq);
let is_red = audio_meta.audio_format == AUDIO_RED_FORMAT;
if is_red {
if let Some((primary, redundant_seq, redundant_data)) =
Self::unpack_red_audio(&packet.data)
{
if !self.has_sequence(redundant_seq as u64) {
log::debug!(
"RED recovery: injecting lost audio seq {} for peer {}",
redundant_seq,
self.peer_id
);
self.record_sequence(redundant_seq as u64);
let recovered_insert = WorkerMsg::Insert {
seq: redundant_seq as u16,
timestamp: (packet.timestamp as u32)
.saturating_sub(OPUS_FRAME_DURATION_MS),
payload: redundant_data,
};
self.send_worker_message(recovered_insert);
}
let insert = WorkerMsg::Insert {
seq: seq as u16,
timestamp: packet.timestamp as u32,
payload: primary,
};
self.send_worker_message(insert);
} else {
log::warn!(
"RED unpack failed for peer {} seq {}, falling back to raw",
self.peer_id,
seq
);
let insert = WorkerMsg::Insert {
seq: seq as u16,
timestamp: packet.timestamp as u32,
payload: packet.data.clone(),
};
self.send_worker_message(insert);
}
} else {
let insert = WorkerMsg::Insert {
seq: seq as u16,
timestamp: packet.timestamp as u32,
payload: packet.data.clone(),
};
self.send_worker_message(insert);
}
let first_frame = !self.decoded;
self.decoded = true;
Ok(DecodeStatus {
rendered: true,
first_frame,
})
}
None => {
log::warn!(
"Received audio packet with length {} without metadata – skipping",
packet.data.len()
);
Ok(DecodeStatus {
rendered: false,
first_frame: false,
})
}
}
}
fn flush(&mut self) {
self.send_worker_message(WorkerMsg::Flush);
log::debug!(
"Sent flush message to NetEq worker for peer {}",
self.peer_id
);
}
fn set_muted(&mut self, muted: bool) {
let mute_msg = WorkerMsg::Mute { muted };
let now = js_sys::Date::now();
let is_ready = *self.worker_ready.borrow();
let queue_length = self.pending_messages.borrow().len();
log::info!(
"🔇 [MUTE DEBUG] Peer {} set_muted({}) at {:.0}ms - worker_ready: {}, queue_length: {}",
self.peer_id,
muted,
now,
is_ready,
queue_length
);
self.send_worker_message(mute_msg);
log::debug!(
"Sent mute message to NetEq worker for peer {} (muted: {})",
self.peer_id,
muted
);
log::debug!(
"✅ Mute message {} for peer {} (muted: {}) at {:.0}ms",
if is_ready {
"sent immediately"
} else {
"queued"
},
self.peer_id,
muted,
now
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn unpack_valid_red_data() {
let primary = b"primary_frame";
let redundant = b"redundant_frame";
let primary_len = (primary.len() as u32).to_le_bytes();
let redundant_seq = 42u32.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(primary);
data.extend_from_slice(&redundant_seq);
data.extend_from_slice(redundant);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_some());
let (p, seq, r) = result.unwrap();
assert_eq!(p, primary);
assert_eq!(seq, 42);
assert_eq!(r, redundant);
}
#[wasm_bindgen_test]
fn unpack_empty_input() {
let result = NetEqAudioPeerDecoder::unpack_red_audio(&[]);
assert!(result.is_none(), "empty input should return None");
}
#[wasm_bindgen_test]
fn unpack_too_short_input() {
let result = NetEqAudioPeerDecoder::unpack_red_audio(&[0, 0, 0]);
assert!(result.is_none(), "3 bytes should return None");
let result = NetEqAudioPeerDecoder::unpack_red_audio(&[0, 0, 0, 0, 0, 0, 0]);
assert!(result.is_none(), "7 bytes should return None");
}
#[wasm_bindgen_test]
fn unpack_exactly_8_bytes_zero_length_frames() {
let data = [0u8; 8];
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_some());
let (p, seq, r) = result.unwrap();
assert!(p.is_empty());
assert_eq!(seq, 0);
assert!(r.is_empty());
}
#[wasm_bindgen_test]
fn unpack_primary_len_exceeds_sanity_limit() {
let primary_len = 10_001u32.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(&[0u8; 8]);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_none(), "primary_len > 10000 should be rejected");
}
#[wasm_bindgen_test]
fn unpack_primary_len_at_sanity_limit() {
let primary_len = 10_000u32.to_le_bytes();
let primary_data = vec![0xAA; 10_000];
let redundant_seq = 5u32.to_le_bytes();
let redundant_data = b"red";
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(&primary_data);
data.extend_from_slice(&redundant_seq);
data.extend_from_slice(redundant_data);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_some(), "primary_len == 10000 should be accepted");
let (p, seq, r) = result.unwrap();
assert_eq!(p.len(), 10_000);
assert_eq!(seq, 5);
assert_eq!(r, redundant_data);
}
#[wasm_bindgen_test]
fn unpack_primary_len_exceeds_data_length() {
let primary_len = 100u32.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(&[0u8; 16]);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(
result.is_none(),
"malformed packet with primary_len > remaining data should return None"
);
}
#[wasm_bindgen_test]
fn unpack_no_room_for_redundant_seq() {
let primary_len = 10u32.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(&[0xBB; 10]); data.extend_from_slice(&[0, 0, 0]);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(
result.is_none(),
"not enough room for redundant_seq should return None"
);
}
#[wasm_bindgen_test]
fn unpack_no_redundant_data_after_seq() {
let primary_len = 5u32.to_le_bytes();
let redundant_seq = 99u32.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(b"AUDIO");
data.extend_from_slice(&redundant_seq);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_some());
let (p, seq, r) = result.unwrap();
assert_eq!(p, b"AUDIO");
assert_eq!(seq, 99);
assert!(r.is_empty());
}
#[wasm_bindgen_test]
fn unpack_preserves_binary_data() {
let primary: Vec<u8> = (0..=255).collect();
let redundant: Vec<u8> = (0..=255).rev().collect();
let primary_len = (primary.len() as u32).to_le_bytes();
let redundant_seq = 1000u32.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.extend_from_slice(&primary);
data.extend_from_slice(&redundant_seq);
data.extend_from_slice(&redundant);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_some());
let (p, seq, r) = result.unwrap();
assert_eq!(p, primary);
assert_eq!(seq, 1000);
assert_eq!(r, redundant);
}
#[wasm_bindgen_test]
fn unpack_max_valid_sequence_number() {
let primary_len = 1u32.to_le_bytes();
let redundant_seq = u32::MAX.to_le_bytes();
let mut data = Vec::new();
data.extend_from_slice(&primary_len);
data.push(0xFF); data.extend_from_slice(&redundant_seq);
data.push(0xAA);
let result = NetEqAudioPeerDecoder::unpack_red_audio(&data);
assert!(result.is_some());
let (_, seq, _) = result.unwrap();
assert_eq!(seq, u32::MAX);
}
#[wasm_bindgen_test]
fn record_and_has_sequence() {
use std::collections::VecDeque;
let capacity = crate::adaptive_quality_constants::AUDIO_RED_SEQ_HISTORY_SIZE;
let mut received: VecDeque<u64> = VecDeque::with_capacity(capacity);
let record = |buf: &mut VecDeque<u64>, seq: u64| {
if buf.len() >= capacity {
buf.pop_front();
}
buf.push_back(seq);
};
record(&mut received, 10);
record(&mut received, 11);
record(&mut received, 12);
assert!(received.contains(&10));
assert!(received.contains(&11));
assert!(received.contains(&12));
assert!(!received.contains(&13));
for i in 13..(13 + capacity as u64) {
record(&mut received, i);
}
assert!(!received.contains(&10));
assert_eq!(received.len(), capacity);
}
}