204 lines
7.5 KiB
Rust
204 lines
7.5 KiB
Rust
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<Self> {
|
|
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<u32> {
|
|
self.event_slot_at(pts).map(|slot| slot.code)
|
|
}
|
|
|
|
fn event_slot_at(&self, pts: Duration) -> Option<ProbeEventSlot> {
|
|
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<u32> {
|
|
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<Duration> {
|
|
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<Vec<u32>> {
|
|
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::<u32>()
|
|
.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::<Result<Vec<_>>>()?;
|
|
if codes.is_empty() {
|
|
bail!("event_width_codes must contain at least one code");
|
|
}
|
|
Ok(codes)
|
|
}
|