media: bake mjpeg opus calibration defaults
This commit is contained in:
parent
ef9701f6e9
commit
0e5de9d21b
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.17"
|
||||
version = "0.22.18"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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))]
|
||||
|
||||
@ -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()
|
||||
};
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.17"
|
||||
version = "0.22.18"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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)"
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.17"
|
||||
version = "0.22.18"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
BIN
server/src/bin/lesavka_uvc/idle_1280x720_black.jpg
Normal file
BIN
server/src/bin/lesavka_uvc/idle_1280x720_black.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@ -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!(
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user