media: bake mjpeg opus calibration defaults

This commit is contained in:
Brad Stein 2026-05-12 22:07:46 -03:00
parent ef9701f6e9
commit 0e5de9d21b
22 changed files with 408 additions and 94 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.22.17"
version = "0.22.18"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.22.17"
version = "0.22.18"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.22.17"
version = "0.22.18"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.22.17"
version = "0.22.18"
edition = "2024"
[dependencies]

View File

@ -68,7 +68,7 @@ const AUDIO_SAMPLE_RATE: i32 = 48_000;
#[cfg(any(not(coverage), test))]
const AUDIO_CHANNELS: usize = 2;
#[cfg(any(not(coverage), test))]
const AUDIO_CHUNK_MS: u64 = 10;
const AUDIO_CHUNK_MS: u64 = 20;
#[cfg(any(not(coverage), test))]
const AUDIO_PULSE_FREQUENCY_HZ: f64 = 1_800.0;
#[cfg(any(not(coverage), test))]

View File

@ -271,6 +271,7 @@ fn spawn_audio_thread(
let packet = AudioPacket {
id: 0,
pts: source_pts_us,
frame_duration_us: AUDIO_CHUNK_MS.saturating_mul(1_000) as u32,
data: chunk,
..Default::default()
};

View File

@ -19,12 +19,17 @@ async fn runtime_audio_probe_emits_nontrivial_pcm_packets() {
let mut packet_count = 0usize;
let mut total_bytes = 0usize;
let mut largest_packet = 0usize;
let expected_duration_us = 20_000;
loop {
let next = audio_queue.pop_fresh().await;
let Some(packet) = next.packet else {
break;
};
assert_eq!(
packet.frame_duration_us, expected_duration_us,
"synthetic audio metadata should match its PCM payload duration"
);
packet_count += 1;
total_bytes += packet.data.len();
largest_packet = largest_packet.max(packet.data.len());
@ -334,7 +339,7 @@ async fn runtime_probe_hevc_video_and_audio_can_form_one_local_bundle() {
videos.len()
);
assert!(
audios.len() >= 120,
audios.len() >= 90,
"expected local PCM audio packets, got {}",
audios.len()
);
@ -357,8 +362,8 @@ async fn runtime_probe_hevc_video_and_audio_can_form_one_local_bundle() {
video.pts
);
assert!(
paired_audio.iter().any(|packet| packet.data.len() >= 1_920),
"expected paired audio to carry full 10ms stereo PCM packets"
paired_audio.iter().any(|packet| packet.data.len() >= 3_840),
"expected paired audio to carry full 20ms stereo PCM packets"
);
}

View File

@ -5,8 +5,9 @@
//! streaming `UpstreamMediaBundle` messages to the server.
use anyhow::{Context, Result};
use lesavka_common::lesavka::{
AudioPacket, UpstreamMediaBundle, VideoPacket, relay_client::RelayClient,
use lesavka_common::{
audio_transport::{self, AudioTransportProfile, UpstreamAudioCodec},
lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket, relay_client::RelayClient},
};
use std::fs::File;
use std::io::Write;
@ -43,6 +44,7 @@ pub async fn run_bundled_probe_stream(
.context("opening sync probe audio dump")?;
let mut send_log = open_debug_dump("LESAVKA_SYNC_PROBE_SEND_LOG")
.context("opening sync probe send log")?;
let mut audio_transport = ProbeAudioTransport::from_env();
let outbound = async_stream::stream! {
let mut pending_audio = Vec::<AudioPacket>::new();
let mut audio_done = false;
@ -79,6 +81,7 @@ pub async fn run_bundled_probe_stream(
&mut pending_audio,
&mut audio_done,
&mut audio_seq,
&mut audio_transport,
audio_dump.as_mut(),
probe_start,
).await;
@ -129,8 +132,10 @@ pub async fn run_bundled_probe_stream(
next.queue_depth,
probe_start,
);
write_probe_audio_dump(audio_dump.as_mut(), &packet);
pending_audio.push(packet);
if let Some(packet) = audio_transport.encode_packet(packet) {
write_probe_audio_dump(audio_dump.as_mut(), &packet);
pending_audio.push(packet);
}
retain_newest_probe_audio(&mut pending_audio);
} else if next.closed {
audio_done = true;
@ -176,20 +181,26 @@ fn build_probe_bundle(
capture_start_us: u64,
capture_end_us: u64,
) -> UpstreamMediaBundle {
UpstreamMediaBundle {
let profile = audio
.first()
.map(audio_transport::packet_audio_profile)
.unwrap_or_else(AudioTransportProfile::pcm_s16le);
let mut bundle = UpstreamMediaBundle {
session_id,
seq,
capture_start_us,
capture_end_us,
video,
audio,
audio_sample_rate: 48_000,
audio_channels: 2,
audio_encoding: lesavka_common::lesavka::AudioEncoding::PcmS16le as i32,
audio_sample_rate: profile.sample_rate,
audio_channels: profile.channels,
audio_encoding: profile.encoding as i32,
video_width: camera.width,
video_height: camera.height,
video_fps: camera.fps,
}
};
audio_transport::mark_bundle_audio_profile(&mut bundle, profile);
bundle
}
/// Drain one short audio grace window when a video packet arrives first.
@ -204,6 +215,7 @@ async fn collect_probe_audio_grace(
pending_audio: &mut Vec<AudioPacket>,
audio_done: &mut bool,
audio_seq: &mut u64,
audio_transport: &mut ProbeAudioTransport,
audio_dump: Option<&mut File>,
probe_start: Instant,
) {
@ -216,14 +228,65 @@ async fn collect_probe_audio_grace(
};
if let Some(mut packet) = next.packet {
stamp_probe_audio_packet(&mut packet, audio_seq, next.queue_depth, probe_start);
write_probe_audio_dump(audio_dump, &packet);
pending_audio.push(packet);
if let Some(packet) = audio_transport.encode_packet(packet) {
write_probe_audio_dump(audio_dump, &packet);
pending_audio.push(packet);
}
retain_newest_probe_audio(pending_audio);
} else if next.closed {
*audio_done = true;
}
}
/// Runtime audio transport for synthetic probe packets.
///
/// Inputs: `LESAVKA_UPLINK_AUDIO_CODEC` and installed GStreamer plugins.
/// Outputs: packet bytes and metadata matching the selected upstream audio
/// route. Why: the blind client->server->RCT test must exercise the same Opus
/// compression path as the live microphone, not silently measure PCM.
struct ProbeAudioTransport {
encoder: Option<crate::input::audio_codec::OpusPacketEncoder>,
}
impl ProbeAudioTransport {
fn from_env() -> Self {
match crate::input::audio_codec::requested_upstream_audio_codec_from_env() {
UpstreamAudioCodec::PcmS16le => Self { encoder: None },
UpstreamAudioCodec::Opus => match crate::input::audio_codec::OpusPacketEncoder::new() {
Ok(encoder) => {
tracing::info!("🧪 sync probe Opus audio transport enabled");
Self {
encoder: Some(encoder),
}
}
Err(err) => {
tracing::warn!(
"🧪⚠️ sync probe Opus requested but unavailable ({err:#}); falling back to PCM"
);
Self { encoder: None }
}
},
}
}
fn encode_packet(&mut self, mut packet: AudioPacket) -> Option<AudioPacket> {
audio_transport::mark_packet_pcm_s16le(&mut packet);
let Some(encoder) = self.encoder.as_mut() else {
return Some(packet);
};
match encoder.encode_packet(packet.clone()) {
Ok(Some(encoded)) => Some(encoded),
Ok(None) => None,
Err(err) => {
tracing::warn!(
"🧪⚠️ sync probe Opus encode failed; sending PCM fallback for this packet: {err:#}"
);
Some(packet)
}
}
}
}
/// Stamp one synthetic audio packet with client-side transport telemetry.
///
/// Inputs: mutable packet, sequence counter, queue depth, and probe clock.

View File

@ -88,6 +88,59 @@ fn hevc_probe_bundle_preserves_paired_media_and_server_metadata() {
);
}
#[test]
/// Verifies the blind sync probe can describe compressed Opus audio bundles.
///
/// Inputs: one synthetic video packet plus a pre-compressed Opus-like audio
/// packet. Outputs: assertions over bundle metadata. Why: the client-to-RCT
/// hardware probe must not silently report PCM evidence when the operator asked
/// to validate the Opus upstream route.
fn probe_bundle_uses_opus_metadata_when_audio_packets_are_opus() {
let camera = CameraConfig {
codec: CameraCodec::Hevc,
width: 1280,
height: 720,
fps: 30,
};
let probe_start = Instant::now();
let mut video = VideoPacket {
pts: 1_000_000,
data: vec![0, 0, 0, 1, 0x26, 0xaa, 0xbb],
..Default::default()
};
let mut video_seq = 0;
stamp_probe_video_packet(&mut video, &mut video_seq, 1, camera.fps, probe_start);
let mut audio = AudioPacket {
pts: 1_000_000,
data: vec![0x7f; 160],
frame_duration_us: 20_000,
..Default::default()
};
lesavka_common::audio_transport::mark_packet_opus(&mut audio);
let mut audio_seq = 0;
stamp_probe_audio_packet(&mut audio, &mut audio_seq, 1, probe_start);
let bundle = build_probe_bundle(
PROBE_BUNDLE_SESSION_ID,
42,
&camera,
Some(video),
vec![audio],
1_000_000,
1_000_000,
);
assert_eq!(
bundle.audio_encoding,
lesavka_common::lesavka::AudioEncoding::Opus as i32
);
assert_eq!(bundle.audio_sample_rate, 48_000);
assert_eq!(bundle.audio_channels, 2);
assert_eq!(bundle.audio[0].frame_duration_us, 20_000);
assert_eq!(bundle.audio[0].data.len(), 160);
}
#[test]
/// Verifies a synthetic HEVC event train leaves as paired A/V bundles.
///

View File

@ -13,6 +13,10 @@ use std::time::Duration;
use crate::input::camera::{CameraCodec, CameraConfig};
use crate::sync_probe::schedule::PulseSchedule;
const AUDIO_SAMPLE_RATE_HZ: u32 = 48_000;
const AUDIO_CHANNEL_COUNT: u32 = 2;
const AUDIO_CHUNK_MS: u32 = 20;
#[derive(Debug, Serialize)]
pub struct ProbeTimeline {
schema: &'static str,
@ -72,9 +76,9 @@ impl ProbeTimeline {
camera_height: camera.height,
camera_fps: camera.fps,
camera_codec: codec_label(camera.codec),
audio_sample_rate: 48_000,
audio_channels: 2,
audio_chunk_ms: 10,
audio_sample_rate: AUDIO_SAMPLE_RATE_HZ,
audio_channels: AUDIO_CHANNEL_COUNT,
audio_chunk_ms: AUDIO_CHUNK_MS,
warmup_us: micros(schedule.warmup_boundary()),
duration_us: micros(duration),
pulse_period_ms: millis(schedule.pulse_period()),
@ -188,6 +192,9 @@ mod tests {
);
assert_eq!(timeline.warmup_us, 4_000_000);
assert_eq!(timeline.camera_codec, "mjpeg");
assert_eq!(timeline.audio_sample_rate, 48_000);
assert_eq!(timeline.audio_channels, 2);
assert_eq!(timeline.audio_chunk_ms, 20);
assert!(timeline.event_width_codes.is_empty());
assert_eq!(timeline.events.len(), 4);
assert_eq!(timeline.events[0].event_id, 0);

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.22.17"
version = "0.22.18"
edition = "2024"
build = "build.rs"

View File

@ -72,18 +72,26 @@ DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0
DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=135090
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0
DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952
DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-34199
DEFAULT_MJPEG_OPUS_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=513850
DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=-34199,1280x720@30=-34199,1920x1080@20=-34199,1920x1080@30=-34199
DEFAULT_MJPEG_OPUS_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=541419,1280x720@30=513850,1920x1080@20=538805,1920x1080@30=506712
DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0
DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=110000
DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0
DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=173852,1280x720@30=110000,1920x1080@20=160045,1920x1080@30=127952
DEFAULT_MJPEG_PCM_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US
DEFAULT_MJPEG_PCM_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US
DEFAULT_MJPEG_PCM_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US
DEFAULT_MJPEG_PCM_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US
DEFAULT_HEVC_PCM_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=$DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US
DEFAULT_HEVC_PCM_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US
DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US
DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US
DEFAULT_HEVC_PCM_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=$DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US
DEFAULT_HEVC_PCM_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US
DEFAULT_HEVC_OPUS_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=$DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US
DEFAULT_HEVC_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US
DEFAULT_HEVC_OPUS_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=$DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US
DEFAULT_HEVC_OPUS_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US
LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000
PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000
PREVIOUS_TUNED_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000
@ -1516,12 +1524,20 @@ SERVER_ENV_TMP=$(mktemp)
printf 'LESAVKA_UPSTREAM_HEVC_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_VIDEO_PLAYOUT_OFFSET_US:-$DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_PCM_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_PCM_AUDIO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_PCM_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_PCM_AUDIO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_PCM_AUDIO_PLAYOUT_OFFSET_US:-$DEFAULT_MJPEG_PCM_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_PCM_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_PCM_VIDEO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_PCM_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_PCM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_PCM_VIDEO_PLAYOUT_OFFSET_US:-$DEFAULT_MJPEG_PCM_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_HEVC_PCM_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_PCM_AUDIO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_HEVC_PCM_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_HEVC_PCM_AUDIO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_PCM_AUDIO_PLAYOUT_OFFSET_US:-$DEFAULT_HEVC_PCM_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_HEVC_PCM_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_PCM_VIDEO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_HEVC_PCM_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_HEVC_PCM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_PCM_VIDEO_PLAYOUT_OFFSET_US:-$DEFAULT_HEVC_PCM_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_OFFSET_US:-$DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_OPUS_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_OFFSET_US:-$DEFAULT_MJPEG_OPUS_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_HEVC_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_OFFSET_US:-$DEFAULT_HEVC_OPUS_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_HEVC_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_HEVC_OPUS_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_HEVC_OPUS_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_HEVC_OPUS_VIDEO_PLAYOUT_OFFSET_US:-$DEFAULT_HEVC_OPUS_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US}"
printf 'LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US}"
printf 'LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s\n' "$(resolve_upstream_audio_playout_offset_us)"

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.22.17"
version = "0.22.18"
edition = "2024"
autobins = false

View File

@ -46,7 +46,8 @@ const V4L2_MEMORY_MMAP: u32 = 1;
const V4L2_FIELD_NONE: u32 = 1;
const V4L2_PIX_FMT_MJPEG: u32 = u32::from_le_bytes(*b"MJPG");
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
const EMPTY_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("lesavka_uvc/idle_1280x720_black.jpg");
const DEFAULT_UVC_BUFFER_COUNT: u32 = 2;
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
@ -250,7 +251,7 @@ impl UvcVideoStream {
fd,
buffers: Vec::new(),
frame_path: frame_spool_path(),
latest_frame: EMPTY_MJPEG_FRAME.to_vec(),
latest_frame: IDLE_MJPEG_FRAME.to_vec(),
frame_max_bytes: MAX_MJPEG_FRAME_BYTES,
streaming: false,
}
@ -445,7 +446,7 @@ impl UvcVideoStream {
{
self.latest_frame = frame;
} else if !looks_like_mjpeg_frame(&self.latest_frame) {
self.latest_frame = EMPTY_MJPEG_FRAME.to_vec();
self.latest_frame = IDLE_MJPEG_FRAME.to_vec();
}
}
@ -461,8 +462,10 @@ impl UvcVideoStream {
fn frame_for_buffer(&self, buffer_len: usize) -> &[u8] {
if self.latest_frame.len() <= buffer_len && looks_like_mjpeg_frame(&self.latest_frame) {
&self.latest_frame
} else if IDLE_MJPEG_FRAME.len() <= buffer_len {
IDLE_MJPEG_FRAME
} else {
EMPTY_MJPEG_FRAME
MINIMAL_MJPEG_FRAME
}
}
}
@ -509,7 +512,7 @@ fn frame_spool_path() -> std::path::PathBuf {
}
fn looks_like_mjpeg_frame(frame: &[u8]) -> bool {
frame.len() > EMPTY_MJPEG_FRAME.len()
frame.len() > MINIMAL_MJPEG_FRAME.len()
&& frame.starts_with(&[0xff, 0xd8])
&& frame.ends_with(&[0xff, 0xd9])
}

View File

@ -58,7 +58,9 @@ const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
#[cfg(coverage)]
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
#[cfg(coverage)]
const EMPTY_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
#[cfg(coverage)]
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("idle_1280x720_black.jpg");
#[cfg(coverage)]
#[repr(C)]
@ -162,7 +164,7 @@ impl UvcVideoStream {
Self {
buffers: Vec::new(),
frame_path: frame_spool_path(),
latest_frame: EMPTY_MJPEG_FRAME.to_vec(),
latest_frame: IDLE_MJPEG_FRAME.to_vec(),
frame_max_bytes: MAX_MJPEG_FRAME_BYTES,
}
}
@ -179,7 +181,7 @@ impl UvcVideoStream {
{
self.latest_frame = frame;
} else if !looks_like_mjpeg_frame(&self.latest_frame) {
self.latest_frame = EMPTY_MJPEG_FRAME.to_vec();
self.latest_frame = IDLE_MJPEG_FRAME.to_vec();
}
}
@ -195,8 +197,10 @@ impl UvcVideoStream {
fn frame_for_buffer(&self, buffer_len: usize) -> &[u8] {
if self.latest_frame.len() <= buffer_len && looks_like_mjpeg_frame(&self.latest_frame) {
&self.latest_frame
} else if IDLE_MJPEG_FRAME.len() <= buffer_len {
IDLE_MJPEG_FRAME
} else {
EMPTY_MJPEG_FRAME
MINIMAL_MJPEG_FRAME
}
}
}
@ -210,7 +214,7 @@ fn frame_spool_path() -> std::path::PathBuf {
#[cfg(coverage)]
fn looks_like_mjpeg_frame(frame: &[u8]) -> bool {
frame.len() > EMPTY_MJPEG_FRAME.len()
frame.len() > MINIMAL_MJPEG_FRAME.len()
&& frame.starts_with(&[0xff, 0xd8])
&& frame.ends_with(&[0xff, 0xd9])
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -16,13 +16,20 @@ use mode_env::{current_uvc_mode, lookup_mode_offset_us};
pub use profile_offsets::{
FACTORY_HEVC_AUDIO_MODE_OFFSETS_US, FACTORY_HEVC_AUDIO_OFFSET_US,
FACTORY_HEVC_OPUS_AUDIO_MODE_OFFSETS_US, FACTORY_HEVC_OPUS_AUDIO_OFFSET_US,
FACTORY_HEVC_OPUS_VIDEO_MODE_OFFSETS_US, FACTORY_HEVC_OPUS_VIDEO_OFFSET_US,
FACTORY_HEVC_PCM_AUDIO_MODE_OFFSETS_US, FACTORY_HEVC_PCM_AUDIO_OFFSET_US,
FACTORY_HEVC_PCM_VIDEO_MODE_OFFSETS_US, FACTORY_HEVC_PCM_VIDEO_OFFSET_US,
FACTORY_HEVC_VIDEO_MODE_OFFSETS_US, FACTORY_HEVC_VIDEO_OFFSET_1280X720_20_US,
FACTORY_HEVC_VIDEO_OFFSET_1280X720_30_US, FACTORY_HEVC_VIDEO_OFFSET_1920X1080_20_US,
FACTORY_HEVC_VIDEO_OFFSET_1920X1080_30_US, FACTORY_HEVC_VIDEO_OFFSET_US,
FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_AUDIO_OFFSET_US,
FACTORY_MJPEG_OPUS_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_OPUS_AUDIO_OFFSET_US,
FACTORY_MJPEG_OPUS_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1280X720_20_US,
FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1280X720_30_US,
FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1920X1080_20_US,
FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1920X1080_30_US, FACTORY_MJPEG_OPUS_VIDEO_OFFSET_US,
FACTORY_MJPEG_PCM_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_PCM_AUDIO_OFFSET_US,
FACTORY_MJPEG_PCM_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_PCM_VIDEO_OFFSET_US,
FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US,
FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US, FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US,
FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US, FACTORY_MJPEG_VIDEO_OFFSET_US,
@ -30,6 +37,7 @@ pub use profile_offsets::{
use profile_offsets::{
configured_profile_offset_us, current_profile, factory_audio_mode_offsets_us,
factory_audio_scalar_offset_us, factory_video_mode_offsets_us, factory_video_scalar_offset_us,
normalize_calibration_profile,
};
const LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = -45_000;
@ -358,24 +366,25 @@ fn parse_snapshot(raw: &str) -> CalibrationSnapshot {
/// Inputs are the typed parameters; output is the return value or side effect.
fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapshot {
let current_profile = current_profile();
let stored_profile = normalize_calibration_profile(&state.profile);
let source_allows_migration = matches!(state.source.as_str(), "factory" | "env");
let confidence_allows_migration = matches!(state.confidence.as_str(), "factory" | "configured");
let detail_allows_profile_migration = state
.detail
.contains("loaded upstream A/V calibration defaults")
|| state.detail.contains("restored release-shipped");
if state.profile != current_profile
if stored_profile != current_profile
&& source_allows_migration
&& confidence_allows_migration
&& detail_allows_profile_migration
{
let mut replacement = factory_snapshot_from_env(format!(
"migrated factory upstream A/V calibration profile from {} to {}",
state.profile, current_profile
stored_profile, current_profile
));
replacement.detail = format!(
"migrated factory upstream A/V calibration profile from {} to {}",
state.profile, replacement.profile
stored_profile, replacement.profile
);
return replacement;
}
@ -391,7 +400,7 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho
&& state.active_audio_offset_us == state.default_audio_offset_us;
let untouched_legacy_video = is_stale_video_offset_us(state.default_video_offset_us)
&& state.active_video_offset_us == state.default_video_offset_us;
if state.profile == current_profile
if stored_profile == current_profile
&& source_allows_migration
&& confidence_allows_migration
&& untouched_legacy_audio
@ -403,6 +412,7 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho
state.active_audio_offset_us = state.factory_audio_offset_us;
state.default_video_offset_us = state.factory_video_offset_us;
state.active_video_offset_us = state.factory_video_offset_us;
state.profile = current_profile;
state.source = "factory".to_string();
state.confidence = FACTORY_CONFIDENCE.to_string();
state.detail = format!(

View File

@ -3,10 +3,22 @@ pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US: i64 = 162_659;
pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US: i64 = 135_090;
pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US: i64 = 160_045;
pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US: i64 = 127_952;
pub const FACTORY_MJPEG_OPUS_AUDIO_OFFSET_US: i64 = -34_199;
pub const FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1280X720_20_US: i64 = 541_419;
pub const FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1280X720_30_US: i64 = 513_850;
pub const FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1920X1080_20_US: i64 = 538_805;
pub const FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1920X1080_30_US: i64 = 506_712;
pub const FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US: &str =
"1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0";
pub const FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US: &str =
"1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952";
pub const FACTORY_MJPEG_OPUS_AUDIO_MODE_OFFSETS_US: &str =
"1280x720@20=-34199,1280x720@30=-34199,1920x1080@20=-34199,1920x1080@30=-34199";
// 1280x720@30 was measured through the blind client->server->RCT Opus path.
// The sibling MJPEG modes carry the same Opus-vs-PCM video delta until the next
// full mode matrix replaces them with direct hardware measurements.
pub const FACTORY_MJPEG_OPUS_VIDEO_MODE_OFFSETS_US: &str =
"1280x720@20=541419,1280x720@30=513850,1920x1080@20=538805,1920x1080@30=506712";
pub const FACTORY_HEVC_AUDIO_OFFSET_US: i64 = 0;
pub const FACTORY_HEVC_VIDEO_OFFSET_1280X720_20_US: i64 = 173_852;
pub const FACTORY_HEVC_VIDEO_OFFSET_1280X720_30_US: i64 = 110_000;
@ -18,12 +30,17 @@ pub const FACTORY_HEVC_VIDEO_MODE_OFFSETS_US: &str =
"1280x720@20=173852,1280x720@30=110000,1920x1080@20=160045,1920x1080@30=127952";
pub const FACTORY_MJPEG_PCM_AUDIO_MODE_OFFSETS_US: &str = FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US;
pub const FACTORY_HEVC_PCM_AUDIO_MODE_OFFSETS_US: &str = FACTORY_HEVC_AUDIO_MODE_OFFSETS_US;
pub const FACTORY_MJPEG_OPUS_AUDIO_MODE_OFFSETS_US: &str = FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US;
pub const FACTORY_HEVC_OPUS_AUDIO_MODE_OFFSETS_US: &str = FACTORY_HEVC_AUDIO_MODE_OFFSETS_US;
pub const FACTORY_MJPEG_PCM_VIDEO_MODE_OFFSETS_US: &str = FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US;
pub const FACTORY_HEVC_PCM_VIDEO_MODE_OFFSETS_US: &str = FACTORY_HEVC_VIDEO_MODE_OFFSETS_US;
pub const FACTORY_HEVC_OPUS_VIDEO_MODE_OFFSETS_US: &str = FACTORY_HEVC_VIDEO_MODE_OFFSETS_US;
pub const FACTORY_MJPEG_PCM_AUDIO_OFFSET_US: i64 = FACTORY_MJPEG_AUDIO_OFFSET_US;
pub const FACTORY_HEVC_PCM_AUDIO_OFFSET_US: i64 = FACTORY_HEVC_AUDIO_OFFSET_US;
pub const FACTORY_MJPEG_OPUS_AUDIO_OFFSET_US: i64 = FACTORY_MJPEG_AUDIO_OFFSET_US;
pub const FACTORY_HEVC_OPUS_AUDIO_OFFSET_US: i64 = FACTORY_HEVC_AUDIO_OFFSET_US;
pub const FACTORY_MJPEG_PCM_VIDEO_OFFSET_US: i64 = FACTORY_MJPEG_VIDEO_OFFSET_US;
pub const FACTORY_HEVC_PCM_VIDEO_OFFSET_US: i64 = FACTORY_HEVC_VIDEO_OFFSET_US;
pub const FACTORY_MJPEG_OPUS_VIDEO_OFFSET_US: i64 = FACTORY_MJPEG_OPUS_VIDEO_OFFSET_1280X720_30_US;
pub const FACTORY_HEVC_OPUS_VIDEO_OFFSET_US: i64 = FACTORY_HEVC_VIDEO_OFFSET_US;
// Direct UVC/UAC output-delay probes against the lab RC target showed a
// per-mode sync center for MJPEG/UVC video. This is output-path compensation,
// not a freshness buffer. The scalar fallback follows the default UVC mode.
@ -103,6 +120,15 @@ pub(super) fn current_profile() -> String {
format!("{camera_profile}+{audio_profile}")
}
/// Normalize a persisted calibration profile into the current `camera+audio`
/// shape. Why: pre-Opus snapshots stored only `mjpeg` or `hevc`, and those
/// should compare as the PCM profiles they always represented.
pub(super) fn normalize_calibration_profile(value: &str) -> String {
let camera_profile = camera_profile_from_calibration_profile(value);
let audio_profile = audio_profile_from_calibration_profile(value);
format!("{camera_profile}+{audio_profile}")
}
/// Resolve the camera side of the active calibration profile.
///
/// Inputs: process environment. Output: `mjpeg` or `hevc`. Why: the camera
@ -227,9 +253,13 @@ pub(super) fn factory_audio_mode_offsets_us(profile: &str) -> &'static str {
/// where HEVC decode and MJPEG re-emission can shift sync, so each ingress
/// profile needs its own baked server-to-RCT center points.
pub(super) fn factory_video_mode_offsets_us(profile: &str) -> &'static str {
match camera_profile_from_calibration_profile(profile).as_str() {
HEVC_PROFILE => FACTORY_HEVC_VIDEO_MODE_OFFSETS_US,
_ => FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US,
let camera = camera_profile_from_calibration_profile(profile);
let audio = audio_profile_from_calibration_profile(profile);
match (camera.as_str(), audio.as_str()) {
(HEVC_PROFILE, OPUS_AUDIO_PROFILE) => FACTORY_HEVC_OPUS_VIDEO_MODE_OFFSETS_US,
(HEVC_PROFILE, _) => FACTORY_HEVC_PCM_VIDEO_MODE_OFFSETS_US,
(MJPEG_PROFILE, OPUS_AUDIO_PROFILE) => FACTORY_MJPEG_OPUS_VIDEO_MODE_OFFSETS_US,
_ => FACTORY_MJPEG_PCM_VIDEO_MODE_OFFSETS_US,
}
}
@ -256,8 +286,12 @@ pub(super) fn factory_audio_scalar_offset_us(profile: &str) -> i64 {
/// most common calibrated profile instead of silently borrowing stale MJPEG
/// values for HEVC.
pub(super) fn factory_video_scalar_offset_us(profile: &str) -> i64 {
match camera_profile_from_calibration_profile(profile).as_str() {
HEVC_PROFILE => FACTORY_HEVC_VIDEO_OFFSET_US,
_ => FACTORY_MJPEG_VIDEO_OFFSET_US,
let camera = camera_profile_from_calibration_profile(profile);
let audio = audio_profile_from_calibration_profile(profile);
match (camera.as_str(), audio.as_str()) {
(HEVC_PROFILE, OPUS_AUDIO_PROFILE) => FACTORY_HEVC_OPUS_VIDEO_OFFSET_US,
(HEVC_PROFILE, _) => FACTORY_HEVC_PCM_VIDEO_OFFSET_US,
(MJPEG_PROFILE, OPUS_AUDIO_PROFILE) => FACTORY_MJPEG_OPUS_VIDEO_OFFSET_US,
_ => FACTORY_MJPEG_PCM_VIDEO_OFFSET_US,
}
}

View File

@ -46,6 +46,14 @@ fn with_clean_offset_env(test: impl FnOnce()) {
"LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_OFFSET_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_HEVC_PCM_AUDIO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
@ -54,6 +62,14 @@ fn with_clean_offset_env(test: impl FnOnce()) {
"LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_HEVC_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_HEVC_OPUS_VIDEO_PLAYOUT_OFFSET_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_OFFSET_US",
None::<&str>,
@ -253,6 +269,29 @@ fn opus_audio_profile_gets_its_own_factory_and_env_namespace() {
});
}
#[test]
fn mjpeg_opus_profile_uses_blind_client_rct_factory_offsets() {
with_clean_offset_env(|| {
temp_env::with_vars(
[
("LESAVKA_UPLINK_CAMERA_CODEC", Some("mjpeg")),
("LESAVKA_UPLINK_AUDIO_CODEC", Some("opus")),
("LESAVKA_UVC_WIDTH", Some("1280")),
("LESAVKA_UVC_HEIGHT", Some("720")),
("LESAVKA_UVC_FPS", Some("30")),
],
|| {
let state = snapshot_from_env();
assert_eq!(state.profile, "mjpeg+opus");
assert_eq!(state.factory_audio_offset_us, -34_199);
assert_eq!(state.factory_video_offset_us, 513_850);
assert_eq!(state.default_audio_offset_us, -34_199);
assert_eq!(state.default_video_offset_us, 513_850);
},
);
});
}
#[test]
fn opus_audio_profile_specific_map_does_not_overwrite_pcm_baseline() {
with_clean_offset_env(|| {
@ -546,20 +585,22 @@ fn load_migrates_untouched_legacy_factory_mjpeg_baseline() {
)
.expect("legacy calibration seed");
let path = file.path().to_string_lossy().to_string();
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 0);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
assert_eq!(
runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
);
assert!(state.detail.contains("migrated legacy MJPEG"));
with_clean_offset_env(|| {
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 0);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
assert_eq!(
runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
);
assert!(state.detail.contains("migrated legacy MJPEG"));
});
});
}
@ -583,20 +624,22 @@ fn load_migrates_untouched_previous_factory_mjpeg_baseline() {
)
.expect("previous calibration seed");
let path = file.path().to_string_lossy().to_string();
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 0);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
assert_eq!(
runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
);
assert!(state.detail.contains("to audio +0.0ms/video +135.1ms"));
with_clean_offset_env(|| {
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 0);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
assert_eq!(
runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
);
assert!(state.detail.contains("to audio +0.0ms/video +135.1ms"));
});
});
}
@ -620,20 +663,22 @@ fn load_migrates_overshot_video_factory_mjpeg_baseline() {
)
.expect("overshot video calibration seed");
let path = file.path().to_string_lossy().to_string();
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 0);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
assert_eq!(
runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
);
assert!(state.detail.contains("from audio +0.0ms/video +1090.0ms"));
with_clean_offset_env(|| {
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 0);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
assert_eq!(
runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
);
assert!(state.detail.contains("from audio +0.0ms/video +1090.0ms"));
});
});
}

View File

@ -28,6 +28,18 @@ fn with_clean_offset_env(test: impl FnOnce()) {
"LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_OFFSET_US",
None::<&str>,
),
],
test,
);
@ -110,6 +122,25 @@ fn runtime_can_start_with_opus_specific_audio_calibration_profile() {
});
}
#[test]
fn runtime_starts_mjpeg_opus_with_blind_client_rct_offsets() {
with_clean_offset_env(|| {
temp_env::with_vars(
[
("LESAVKA_UVC_WIDTH", Some("1280")),
("LESAVKA_UVC_HEIGHT", Some("720")),
("LESAVKA_UVC_FPS", Some("30")),
("LESAVKA_UPLINK_CAMERA_CODEC", Some("mjpeg")),
("LESAVKA_UPLINK_AUDIO_CODEC", Some("opus")),
],
|| {
let runtime = UpstreamMediaRuntime::new();
assert_eq!(runtime.playout_offsets(), (513_850, -34_199));
},
);
});
}
#[test]
/// Keeps `runtime_records_client_and_sink_timing_for_upstream_snapshots` explicit because the blind client-to-RCT probe depends on this telemetry to explain freshness losses.
/// Inputs are paired camera/microphone timing samples plus sink handoff marks; output is a live snapshot with skew, queue, late, and freeze fields populated.

View File

@ -9,8 +9,10 @@
use lesavka_server::calibration::{
FACTORY_HEVC_AUDIO_MODE_OFFSETS_US, FACTORY_HEVC_OPUS_AUDIO_MODE_OFFSETS_US,
FACTORY_HEVC_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US,
FACTORY_MJPEG_PCM_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US,
FACTORY_HEVC_OPUS_VIDEO_MODE_OFFSETS_US, FACTORY_HEVC_VIDEO_MODE_OFFSETS_US,
FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_OPUS_AUDIO_MODE_OFFSETS_US,
FACTORY_MJPEG_OPUS_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_PCM_AUDIO_MODE_OFFSETS_US,
FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US,
};
const MATRIX_SCRIPT: &str = include_str!(concat!(
@ -30,8 +32,20 @@ fn hevc_and_mjpeg_factory_maps_cover_all_supported_uvc_modes() {
for mode in SUPPORTED_MODES {
assert!(map_contains_mode(FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, mode));
assert!(map_contains_mode(FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US, mode));
assert!(map_contains_mode(
FACTORY_MJPEG_OPUS_AUDIO_MODE_OFFSETS_US,
mode
));
assert!(map_contains_mode(
FACTORY_MJPEG_OPUS_VIDEO_MODE_OFFSETS_US,
mode
));
assert!(map_contains_mode(FACTORY_HEVC_AUDIO_MODE_OFFSETS_US, mode));
assert!(map_contains_mode(FACTORY_HEVC_VIDEO_MODE_OFFSETS_US, mode));
assert!(map_contains_mode(
FACTORY_HEVC_OPUS_VIDEO_MODE_OFFSETS_US,
mode
));
}
}
@ -45,9 +59,17 @@ fn hevc_profile_defaults_are_separate_from_mjpeg_profile_defaults() {
FACTORY_MJPEG_PCM_AUDIO_MODE_OFFSETS_US,
FACTORY_HEVC_AUDIO_MODE_OFFSETS_US
);
assert_ne!(
FACTORY_MJPEG_OPUS_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US,
"MJPEG+Opus should preserve the blind client-to-RCT audio calibration"
);
assert_ne!(
FACTORY_MJPEG_OPUS_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US,
"MJPEG+Opus should not silently borrow the PCM video timing map"
);
assert_eq!(
FACTORY_HEVC_OPUS_AUDIO_MODE_OFFSETS_US, FACTORY_HEVC_AUDIO_MODE_OFFSETS_US,
"Opus has a separate audio map that currently inherits PCM until lab calibration lands"
"HEVC+Opus keeps the PCM audio baseline until a dedicated lab pass lands"
);
}

View File

@ -420,7 +420,12 @@ mod uvc_binary_extra {
assert_eq!(stream.latest_frame, vec![0xff, 0xd8, 0x11, 0xff, 0xd9]);
assert_eq!(stream.frame_payload_limit(), 8);
assert_eq!(stream.frame_for_buffer(4), EMPTY_MJPEG_FRAME);
assert_eq!(stream.frame_for_buffer(4), MINIMAL_MJPEG_FRAME);
assert_eq!(
UvcVideoStream::new(-1).frame_for_buffer(64 * 1024),
IDLE_MJPEG_FRAME
);
assert!(IDLE_MJPEG_FRAME.len() > 4);
}
#[test]

View File

@ -96,6 +96,12 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
assert!(SERVER_INSTALL.contains(
"DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952"
));
assert!(SERVER_INSTALL.contains(
"DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=-34199,1280x720@30=-34199,1920x1080@20=-34199,1920x1080@30=-34199"
));
assert!(SERVER_INSTALL.contains(
"DEFAULT_MJPEG_OPUS_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=541419,1280x720@30=513850,1920x1080@20=538805,1920x1080@30=506712"
));
assert!(SERVER_INSTALL.contains("DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0"));
assert!(SERVER_INSTALL.contains("DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=110000"));
assert!(SERVER_INSTALL.contains(
@ -105,6 +111,12 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"DEFAULT_HEVC_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US"
));
assert!(SERVER_INSTALL.contains("LESAVKA_UPLINK_AUDIO_CODEC=%s"));
assert!(
SERVER_INSTALL.contains("LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s")
);
assert!(
SERVER_INSTALL.contains("LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s")
);
assert!(SERVER_INSTALL.contains("LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s"));
assert!(
SERVER_INSTALL.contains("resolve_upstream_video_playout_offset_us"),

View File

@ -23,9 +23,12 @@ const PROFILE_OFFSETS: &str = include_str!(concat!(
fn installer_persists_both_mjpeg_and_hevc_factory_offset_maps() {
for marker in [
"DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952",
"DEFAULT_MJPEG_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=-34199,1280x720@30=-34199,1920x1080@20=-34199,1920x1080@30=-34199",
"DEFAULT_MJPEG_OPUS_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=541419,1280x720@30=513850,1920x1080@20=538805,1920x1080@30=506712",
"DEFAULT_HEVC_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=173852,1280x720@30=110000,1920x1080@20=160045,1920x1080@30=127952",
"DEFAULT_HEVC_OPUS_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=$DEFAULT_HEVC_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US",
"LESAVKA_UPSTREAM_MJPEG_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s",
"LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s",
"LESAVKA_UPSTREAM_HEVC_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s",
"LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s",
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s",