174 lines
5.4 KiB
Rust
174 lines
5.4 KiB
Rust
//! Runtime entrypoint for the shared-clock upstream sync probe.
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
|
|
use crate::app_support;
|
|
use crate::handshake;
|
|
use crate::sync_probe::capture::SyncProbeCapture;
|
|
use crate::sync_probe::config::{ParseOutcome, ProbeConfig, parse_args_outcome_from, usage};
|
|
use crate::sync_probe::schedule::PulseSchedule;
|
|
|
|
#[cfg(not(coverage))]
|
|
use lesavka_common::lesavka::relay_client::RelayClient;
|
|
#[cfg(not(coverage))]
|
|
use tonic::{Request, transport::Channel};
|
|
|
|
pub async fn run_sync_probe_from_args<I, S>(args: I) -> Result<()>
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: Into<String>,
|
|
{
|
|
match parse_args_outcome_from(args)? {
|
|
ParseOutcome::Run(config) => run_sync_probe(config).await,
|
|
ParseOutcome::Help => {
|
|
println!("{}", usage());
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(coverage))]
|
|
async fn run_sync_probe(config: ProbeConfig) -> Result<()> {
|
|
let caps = handshake::negotiate(config.server.as_str()).await;
|
|
if !caps.camera || !caps.microphone {
|
|
bail!("server does not advertise both camera and microphone support");
|
|
}
|
|
let camera = app_support::camera_config_from_caps(&caps)
|
|
.context("server handshake did not include a complete camera profile")?;
|
|
|
|
let schedule = PulseSchedule::new(
|
|
config.warmup,
|
|
config.pulse_period,
|
|
config.pulse_width,
|
|
config.marker_tick_period,
|
|
);
|
|
let capture = SyncProbeCapture::new(camera, schedule, config.duration)?;
|
|
|
|
tracing::info!(
|
|
server = %config.server,
|
|
duration_s = config.duration.as_secs(),
|
|
codec = ?camera.codec,
|
|
width = camera.width,
|
|
height = camera.height,
|
|
fps = camera.fps,
|
|
"🧪 A/V sync probe starting"
|
|
);
|
|
|
|
let video_channel = connect(config.server.as_str()).await?;
|
|
let audio_channel = connect(config.server.as_str()).await?;
|
|
let video_queue = capture.video_queue();
|
|
let audio_queue = capture.audio_queue();
|
|
|
|
let video_task = tokio::spawn(async move {
|
|
let mut client = RelayClient::new(video_channel);
|
|
let outbound = async_stream::stream! {
|
|
loop {
|
|
let next = video_queue.pop_fresh().await;
|
|
if let Some(packet) = next.packet {
|
|
yield packet;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
let mut response = client
|
|
.stream_camera(Request::new(outbound))
|
|
.await
|
|
.context("starting sync probe camera stream")?;
|
|
while response.get_mut().message().await.transpose().is_some() {}
|
|
Ok::<(), anyhow::Error>(())
|
|
});
|
|
|
|
let audio_task = tokio::spawn(async move {
|
|
let mut client = RelayClient::new(audio_channel);
|
|
let outbound = async_stream::stream! {
|
|
loop {
|
|
let next = audio_queue.pop_fresh().await;
|
|
if let Some(packet) = next.packet {
|
|
yield packet;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
let mut response = client
|
|
.stream_microphone(Request::new(outbound))
|
|
.await
|
|
.context("starting sync probe microphone stream")?;
|
|
while response.get_mut().message().await.transpose().is_some() {}
|
|
Ok::<(), anyhow::Error>(())
|
|
});
|
|
|
|
let (video_result, audio_result) =
|
|
tokio::try_join!(video_task, audio_task).context("joining sync probe streams")?;
|
|
video_result.context("sync probe camera task failed")?;
|
|
audio_result.context("sync probe microphone task failed")?;
|
|
|
|
tracing::info!("🧪 A/V sync probe finished");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(coverage))]
|
|
async fn connect(server_addr: &str) -> Result<Channel> {
|
|
Channel::from_shared(server_addr.to_string())
|
|
.context("invalid relay server address")?
|
|
.tcp_nodelay(true)
|
|
.connect()
|
|
.await
|
|
.with_context(|| format!("connecting to relay at {server_addr}"))
|
|
}
|
|
|
|
#[cfg(coverage)]
|
|
async fn run_sync_probe(_config: ProbeConfig) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::parse_args_outcome_from;
|
|
#[cfg(coverage)]
|
|
use super::run_sync_probe_from_args;
|
|
use crate::sync_probe::config::ParseOutcome;
|
|
|
|
#[test]
|
|
fn help_passthrough_stays_stable() {
|
|
let outcome = parse_args_outcome_from(["--help"]).expect("help");
|
|
assert_eq!(outcome, ParseOutcome::Help);
|
|
}
|
|
|
|
#[cfg(coverage)]
|
|
#[test]
|
|
fn coverage_run_path_accepts_default_probe_args() {
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
rt.block_on(async {
|
|
run_sync_probe_from_args(std::iter::empty::<&str>())
|
|
.await
|
|
.expect("coverage run path");
|
|
});
|
|
}
|
|
|
|
#[cfg(coverage)]
|
|
#[test]
|
|
fn coverage_run_path_accepts_custom_probe_args() {
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
rt.block_on(async {
|
|
run_sync_probe_from_args([
|
|
"--server",
|
|
"http://lab:50051",
|
|
"--duration-seconds",
|
|
"2",
|
|
"--warmup-seconds",
|
|
"5",
|
|
"--pulse-period-ms",
|
|
"700",
|
|
"--pulse-width-ms",
|
|
"80",
|
|
"--marker-tick-period",
|
|
"4",
|
|
])
|
|
.await
|
|
.expect("configured coverage run path");
|
|
});
|
|
}
|
|
}
|