impl OutputDelayProbeTimeline { fn new(config: &ProbeConfig, camera: &CameraConfig, server_start_unix_ns: u128) -> Self { let event_count = config.event_count(); let events = (0..event_count) .map(|event_id| { let slot = config.event_slot_by_id(event_id as usize); OutputDelayProbeEventTimeline { event_id: event_id as usize, code: slot.code, planned_start_us: slot.planned_start_us, planned_end_us: slot.planned_end_us, video_seq: None, audio_seq: None, video_feed_monotonic_us: None, audio_push_monotonic_us: None, video_feed_unix_ns: None, audio_push_unix_ns: None, server_feed_delta_ms: None, } }) .collect(); Self { schema: "lesavka.output-delay-server-timeline.v1", origin: "theia-server-generated", media_path: "server generator -> UVC/UAC sinks", injection_scope: "server-final-output-handoff", server_pipeline_reference: "generated media PTS before intentional sync delay", sink_handoff_path: "video CameraRelay::feed; audio Voice::push", client_uplink_included: false, camera_width: camera.width, camera_height: camera.height, camera_fps: camera.fps, audio_sample_rate: AUDIO_SAMPLE_RATE, audio_channels: AUDIO_CHANNELS, audio_chunk_ms: AUDIO_CHUNK_MS, audio_delay_us: duration_us(config.audio_delay), video_delay_us: duration_us(config.video_delay), server_start_unix_ns, pulse_period_ms: config.pulse_period.as_millis() as u64, pulse_width_ms: config.pulse_width.as_millis() as u64, warmup_us: duration_us(config.warmup), duration_us: duration_us(config.duration), events, } } fn mark_audio(&mut self, slot: ProbeEventSlot, seq: u64, monotonic_us: u64, unix_ns: u128) { let Some(event) = self.events.get_mut(slot.event_id) else { return; }; if event.audio_push_monotonic_us.is_none() { event.audio_seq = Some(seq); event.audio_push_monotonic_us = Some(monotonic_us); event.audio_push_unix_ns = Some(unix_ns); event.update_delta(); } } fn mark_video(&mut self, slot: ProbeEventSlot, seq: u64, monotonic_us: u64, unix_ns: u128) { let Some(event) = self.events.get_mut(slot.event_id) else { return; }; if event.video_feed_monotonic_us.is_none() { event.video_seq = Some(seq); event.video_feed_monotonic_us = Some(monotonic_us); event.video_feed_unix_ns = Some(unix_ns); event.update_delta(); } } } impl OutputDelayProbeEventTimeline { fn update_delta(&mut self) { let (Some(audio_us), Some(video_us)) = (self.audio_push_monotonic_us, self.video_feed_monotonic_us) else { return; }; self.server_feed_delta_ms = Some((audio_us as f64 - video_us as f64) / 1000.0); } } impl ProbeConfig { fn from_request(request: &OutputDelayProbeRequest) -> Result { let duration_seconds = non_zero_or_default( request.duration_seconds, DEFAULT_DURATION_SECONDS, "duration_seconds", )?; let warmup_seconds = if request.warmup_seconds == 0 { DEFAULT_WARMUP_SECONDS } else { request.warmup_seconds }; let pulse_period_ms = non_zero_or_default( request.pulse_period_ms, DEFAULT_PULSE_PERIOD_MS, "pulse_period_ms", )?; let pulse_width_ms = non_zero_or_default( request.pulse_width_ms, DEFAULT_PULSE_WIDTH_MS, "pulse_width_ms", )?; if pulse_width_ms >= pulse_period_ms { bail!("pulse_width_ms must stay smaller than pulse_period_ms"); } let event_width_codes = parse_event_width_codes(&request.event_width_codes)?; Ok(Self { duration: Duration::from_secs(u64::from(duration_seconds)), warmup: Duration::from_secs(u64::from(warmup_seconds)), pulse_period: Duration::from_millis(u64::from(pulse_period_ms)), pulse_width: Duration::from_millis(u64::from(pulse_width_ms)), event_width_codes, audio_delay: positive_delay(request.audio_delay_us, "audio_delay_us")?, video_delay: positive_delay(request.video_delay_us, "video_delay_us")?, }) } fn event_code_at(&self, pts: Duration) -> Option { self.event_slot_at(pts).map(|slot| slot.code) } fn event_slot_at(&self, pts: Duration) -> Option { if pts < self.warmup { return None; } let since_warmup = pts.saturating_sub(self.warmup); let period_ns = self.pulse_period.as_nanos().max(1); let pulse_index = (since_warmup.as_nanos() / period_ns) as usize; let pulse_offset_ns = since_warmup.as_nanos() % period_ns; let active_ns = self.pulse_width.as_nanos(); (pulse_offset_ns < active_ns).then(|| self.event_slot_by_id(pulse_index)) } fn event_slot_by_id(&self, event_id: usize) -> ProbeEventSlot { let code = self.event_width_codes[event_id % self.event_width_codes.len()]; let planned_start = self .warmup .saturating_add(duration_mul(self.pulse_period, event_id as u64)); let planned_end = planned_start.saturating_add(self.pulse_width); ProbeEventSlot { event_id, code, planned_start_us: duration_us(planned_start), planned_end_us: duration_us(planned_end), } } fn event_count(&self) -> u64 { if self.duration <= self.warmup { return 0; } let active = self.duration - self.warmup; let count = active.as_nanos() / self.pulse_period.as_nanos().max(1); count.try_into().unwrap_or(u64::MAX) } } fn non_zero_or_default(value: u32, default: u32, name: &str) -> Result { if value == 0 { return Ok(default); } if value == u32::MAX { bail!("{name} is too large"); } Ok(value) } fn positive_delay(value_us: i64, name: &str) -> Result { if value_us < 0 { bail!("{name} must be zero or positive for the direct output-delay probe"); } Ok(Duration::from_micros(value_us as u64)) } fn parse_event_width_codes(raw: &str) -> Result> { let trimmed = raw.trim(); if trimmed.is_empty() { return Ok(DEFAULT_EVENT_WIDTH_CODES.to_vec()); } let codes = trimmed .split(',') .filter_map(|part| { let part = part.trim(); (!part.is_empty()).then_some(part) }) .map(|part| { let code = part .parse::() .with_context(|| format!("parsing event width code `{part}`"))?; if !(1..=16).contains(&code) { bail!("event signature code {code} is unsupported; use values 1..16"); } Ok(code) }) .collect::>>()?; if codes.is_empty() { bail!("event_width_codes must contain at least one code"); } Ok(codes) }