media: reset bundled upstream sessions
This commit is contained in:
parent
602974b04e
commit
692c3a6545
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.22.26"
|
version = "0.22.27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.26"
|
version = "0.22.27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.26"
|
version = "0.22.27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.22.26"
|
version = "0.22.27"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -64,6 +64,8 @@ impl InputAggregator {
|
|||||||
self.pending_release_started_at = Some(Instant::now());
|
self.pending_release_started_at = Some(Instant::now());
|
||||||
self.last_keyboard_report = [0; 8];
|
self.last_keyboard_report = [0; 8];
|
||||||
self.capture_pending_keys();
|
self.capture_pending_keys();
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
self.publish_routing_state_if_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_local_release(&mut self, focus_launcher: bool) {
|
fn finish_local_release(&mut self, focus_launcher: bool) {
|
||||||
@ -271,7 +273,7 @@ impl InputAggregator {
|
|||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn publish_routing_state_if_changed(&mut self) {
|
fn publish_routing_state_if_changed(&mut self) {
|
||||||
let remote_capture = !self.released;
|
let remote_capture = self.remote_capture_active();
|
||||||
if self.published_remote_capture == Some(remote_capture) {
|
if self.published_remote_capture == Some(remote_capture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,27 +58,38 @@
|
|||||||
let transport_combo = widgets.webcam_transport_combo.clone();
|
let transport_combo = widgets.webcam_transport_combo.clone();
|
||||||
let transport_combo_read = transport_combo.clone();
|
let transport_combo_read = transport_combo.clone();
|
||||||
transport_combo.connect_changed(move |_| {
|
transport_combo.connect_changed(move |_| {
|
||||||
|
if widgets.webcam_transport_syncing.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let selected = transport_combo_read
|
let selected = transport_combo_read
|
||||||
.active_id()
|
.active_id()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(WebcamTransport::from_id)
|
.and_then(WebcamTransport::from_id)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
state.borrow_mut().select_webcam_transport(selected);
|
let Ok(mut state_mut) = state.try_borrow_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
state_mut.select_webcam_transport(selected);
|
||||||
|
drop(state_mut);
|
||||||
let relay_live = child_proc.borrow().is_some();
|
let relay_live = child_proc.borrow().is_some();
|
||||||
if relay_live {
|
if relay_live {
|
||||||
apply_live_media_device_change(
|
if let Ok(state_ref) = state.try_borrow() {
|
||||||
&state.borrow(),
|
apply_live_media_device_change(
|
||||||
&widgets,
|
&state_ref,
|
||||||
&child_proc,
|
&widgets,
|
||||||
"Webcam transport",
|
&child_proc,
|
||||||
);
|
"Webcam transport",
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
widgets.status_label.set_text(&format!(
|
widgets.status_label.set_text(&format!(
|
||||||
"Webcam transport set to {} for the next relay launch.",
|
"Webcam transport set to {} for the next relay launch.",
|
||||||
selected.label()
|
selected.label()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
refresh_launcher_ui(&widgets, &state.borrow(), relay_live);
|
if let Ok(state_ref) = state.try_borrow() {
|
||||||
|
refresh_launcher_ui(&widgets, &state_ref, relay_live);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,29 +100,38 @@
|
|||||||
let audio_combo = widgets.upstream_audio_transport_combo.clone();
|
let audio_combo = widgets.upstream_audio_transport_combo.clone();
|
||||||
let audio_combo_read = audio_combo.clone();
|
let audio_combo_read = audio_combo.clone();
|
||||||
audio_combo.connect_changed(move |_| {
|
audio_combo.connect_changed(move |_| {
|
||||||
|
if widgets.upstream_audio_transport_syncing.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let selected = audio_combo_read
|
let selected = audio_combo_read
|
||||||
.active_id()
|
.active_id()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(UpstreamAudioTransport::from_id)
|
.and_then(UpstreamAudioTransport::from_id)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
state
|
let Ok(mut state_mut) = state.try_borrow_mut() else {
|
||||||
.borrow_mut()
|
return;
|
||||||
.select_upstream_audio_transport(selected);
|
};
|
||||||
|
state_mut.select_upstream_audio_transport(selected);
|
||||||
|
drop(state_mut);
|
||||||
let relay_live = child_proc.borrow().is_some();
|
let relay_live = child_proc.borrow().is_some();
|
||||||
if relay_live {
|
if relay_live {
|
||||||
apply_live_media_device_change(
|
if let Ok(state_ref) = state.try_borrow() {
|
||||||
&state.borrow(),
|
apply_live_media_device_change(
|
||||||
&widgets,
|
&state_ref,
|
||||||
&child_proc,
|
&widgets,
|
||||||
"Microphone transport",
|
&child_proc,
|
||||||
);
|
"Microphone transport",
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
widgets.status_label.set_text(&format!(
|
widgets.status_label.set_text(&format!(
|
||||||
"Upstream microphone transport set to {} for the next relay launch.",
|
"Upstream microphone transport set to {} for the next relay launch.",
|
||||||
selected.label()
|
selected.label()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
refresh_launcher_ui(&widgets, &state.borrow(), relay_live);
|
if let Ok(state_ref) = state.try_borrow() {
|
||||||
|
refresh_launcher_ui(&widgets, &state_ref, relay_live);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
use evdev::Device;
|
use evdev::Device;
|
||||||
use gtk::{glib, pango, prelude::*};
|
use gtk::{glib, pango, prelude::*};
|
||||||
|
|||||||
@ -162,7 +162,9 @@
|
|||||||
camera_test_button: camera_test_button.clone(),
|
camera_test_button: camera_test_button.clone(),
|
||||||
camera_preview_stack: camera_preview_stack.clone(),
|
camera_preview_stack: camera_preview_stack.clone(),
|
||||||
webcam_transport_combo: webcam_transport_combo.clone(),
|
webcam_transport_combo: webcam_transport_combo.clone(),
|
||||||
|
webcam_transport_syncing: Rc::new(Cell::new(false)),
|
||||||
upstream_audio_transport_combo: upstream_audio_transport_combo.clone(),
|
upstream_audio_transport_combo: upstream_audio_transport_combo.clone(),
|
||||||
|
upstream_audio_transport_syncing: Rc::new(Cell::new(false)),
|
||||||
camera_mirror_button: camera_mirror_button.clone(),
|
camera_mirror_button: camera_mirror_button.clone(),
|
||||||
camera_mirror_revealer: camera_mirror_revealer.clone(),
|
camera_mirror_revealer: camera_mirror_revealer.clone(),
|
||||||
microphone_test_button: microphone_test_button.clone(),
|
microphone_test_button: microphone_test_button.clone(),
|
||||||
|
|||||||
@ -169,7 +169,9 @@ pub struct LauncherWidgets {
|
|||||||
pub camera_test_button: gtk::Button,
|
pub camera_test_button: gtk::Button,
|
||||||
pub camera_preview_stack: gtk::Stack,
|
pub camera_preview_stack: gtk::Stack,
|
||||||
pub webcam_transport_combo: gtk::ComboBoxText,
|
pub webcam_transport_combo: gtk::ComboBoxText,
|
||||||
|
pub webcam_transport_syncing: Rc<Cell<bool>>,
|
||||||
pub upstream_audio_transport_combo: gtk::ComboBoxText,
|
pub upstream_audio_transport_combo: gtk::ComboBoxText,
|
||||||
|
pub upstream_audio_transport_syncing: Rc<Cell<bool>>,
|
||||||
pub camera_mirror_button: gtk::ToggleButton,
|
pub camera_mirror_button: gtk::ToggleButton,
|
||||||
pub camera_mirror_revealer: gtk::Revealer,
|
pub camera_mirror_revealer: gtk::Revealer,
|
||||||
pub microphone_test_button: gtk::Button,
|
pub microphone_test_button: gtk::Button,
|
||||||
|
|||||||
@ -275,9 +275,11 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
if widgets.webcam_transport_combo.active_id().as_deref()
|
if widgets.webcam_transport_combo.active_id().as_deref()
|
||||||
!= Some(state.effective_webcam_transport().as_id())
|
!= Some(state.effective_webcam_transport().as_id())
|
||||||
{
|
{
|
||||||
|
widgets.webcam_transport_syncing.set(true);
|
||||||
widgets
|
widgets
|
||||||
.webcam_transport_combo
|
.webcam_transport_combo
|
||||||
.set_active_id(Some(state.effective_webcam_transport().as_id()));
|
.set_active_id(Some(state.effective_webcam_transport().as_id()));
|
||||||
|
widgets.webcam_transport_syncing.set(false);
|
||||||
}
|
}
|
||||||
widgets
|
widgets
|
||||||
.webcam_transport_combo
|
.webcam_transport_combo
|
||||||
@ -295,9 +297,11 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
!= Some(state.upstream_audio_transport.as_id())
|
!= Some(state.upstream_audio_transport.as_id())
|
||||||
{
|
{
|
||||||
|
widgets.upstream_audio_transport_syncing.set(true);
|
||||||
widgets
|
widgets
|
||||||
.upstream_audio_transport_combo
|
.upstream_audio_transport_combo
|
||||||
.set_active_id(Some(state.upstream_audio_transport.as_id()));
|
.set_active_id(Some(state.upstream_audio_transport.as_id()));
|
||||||
|
widgets.upstream_audio_transport_syncing.set(false);
|
||||||
}
|
}
|
||||||
widgets
|
widgets
|
||||||
.upstream_audio_transport_combo
|
.upstream_audio_transport_combo
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.26"
|
version = "0.22.27"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.26"
|
version = "0.22.27"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,9 @@ impl Handler {
|
|||||||
let rpc_id = runtime_support::next_stream_id();
|
let rpc_id = runtime_support::next_stream_id();
|
||||||
let request = req.into_inner();
|
let request = req.into_inner();
|
||||||
let camera_cfg = camera::current_camera_config();
|
let camera_cfg = camera::current_camera_config();
|
||||||
let microphone_lease = self.upstream_media_rt.activate_microphone();
|
let bundled_leases = self.upstream_media_rt.activate_bundled_session();
|
||||||
let camera_lease = self.upstream_media_rt.activate_camera();
|
let microphone_lease = bundled_leases.microphone;
|
||||||
|
let camera_lease = bundled_leases.camera;
|
||||||
info!(
|
info!(
|
||||||
rpc_id,
|
rpc_id,
|
||||||
session_id = camera_lease.session_id,
|
session_id = camera_lease.session_id,
|
||||||
|
|||||||
@ -6,8 +6,9 @@ impl Handler {
|
|||||||
) -> Result<Response<ReceiverStream<Result<Empty, Status>>>, Status> {
|
) -> Result<Response<ReceiverStream<Result<Empty, Status>>>, Status> {
|
||||||
let rpc_id = runtime_support::next_stream_id();
|
let rpc_id = runtime_support::next_stream_id();
|
||||||
let camera_cfg = camera::current_camera_config();
|
let camera_cfg = camera::current_camera_config();
|
||||||
let microphone_lease = self.upstream_media_rt.activate_microphone();
|
let bundled_leases = self.upstream_media_rt.activate_bundled_session();
|
||||||
let camera_lease = self.upstream_media_rt.activate_camera();
|
let microphone_lease = bundled_leases.microphone;
|
||||||
|
let camera_lease = bundled_leases.camera;
|
||||||
info!(
|
info!(
|
||||||
rpc_id,
|
rpc_id,
|
||||||
session_id = camera_lease.session_id,
|
session_id = camera_lease.session_id,
|
||||||
|
|||||||
@ -149,8 +149,9 @@ impl Relay for Handler {
|
|||||||
&self,
|
&self,
|
||||||
req: Request<tonic::Streaming<UpstreamMediaBundle>>,
|
req: Request<tonic::Streaming<UpstreamMediaBundle>>,
|
||||||
) -> Result<Response<Self::StreamWebcamMediaStream>, Status> {
|
) -> Result<Response<Self::StreamWebcamMediaStream>, Status> {
|
||||||
let microphone_lease = self.upstream_media_rt.activate_microphone();
|
let bundled_leases = self.upstream_media_rt.activate_bundled_session();
|
||||||
let camera_lease = self.upstream_media_rt.activate_camera();
|
let microphone_lease = bundled_leases.microphone;
|
||||||
|
let camera_lease = bundled_leases.camera;
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||||
let upstream_media_rt = self.upstream_media_rt.clone();
|
let upstream_media_rt = self.upstream_media_rt.clone();
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,12 @@ pub struct UpstreamStreamLease {
|
|||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct UpstreamBundledLeases {
|
||||||
|
pub camera: UpstreamStreamLease,
|
||||||
|
pub microphone: UpstreamStreamLease,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct PlannedUpstreamPacket {
|
pub struct PlannedUpstreamPacket {
|
||||||
pub local_pts_us: u64,
|
pub local_pts_us: u64,
|
||||||
|
|||||||
@ -33,6 +33,36 @@ impl UpstreamMediaRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts a bundled camera+microphone generation with a fresh shared playout epoch.
|
||||||
|
fn activate_bundled(&self) -> UpstreamBundledLeases {
|
||||||
|
let camera_generation = self.next_camera_generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
let microphone_generation =
|
||||||
|
self.next_microphone_generation
|
||||||
|
.fetch_add(1, Ordering::SeqCst)
|
||||||
|
+ 1;
|
||||||
|
let mut state = self
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.expect("upstream media state mutex poisoned");
|
||||||
|
state.session_id = self.next_session_id.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
reset_session_state(&mut state);
|
||||||
|
state.session_started_at = Some(Instant::now());
|
||||||
|
state.phase = UpstreamSyncPhase::Acquiring;
|
||||||
|
state.last_reason = "v2 bundled upstream session acquiring media".to_string();
|
||||||
|
state.active_camera_generation = Some(camera_generation);
|
||||||
|
state.active_microphone_generation = Some(microphone_generation);
|
||||||
|
UpstreamBundledLeases {
|
||||||
|
camera: UpstreamStreamLease {
|
||||||
|
session_id: state.session_id,
|
||||||
|
generation: camera_generation,
|
||||||
|
},
|
||||||
|
microphone: UpstreamStreamLease {
|
||||||
|
session_id: state.session_id,
|
||||||
|
generation: microphone_generation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps `is_active` explicit because it sits on server upstream media scheduling, where timing choices directly affect lip sync.
|
/// Keeps `is_active` explicit because it sits on server upstream media scheduling, where timing choices directly affect lip sync.
|
||||||
/// Inputs are the typed parameters; output is the return value or side effect.
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||||||
fn is_active(&self, kind: UpstreamMediaKind, generation: u64) -> bool {
|
fn is_active(&self, kind: UpstreamMediaKind, generation: u64) -> bool {
|
||||||
|
|||||||
@ -41,6 +41,11 @@ pub fn new() -> Self {
|
|||||||
self.activate(UpstreamMediaKind::Microphone)
|
self.activate(UpstreamMediaKind::Microphone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn activate_bundled_session(&self) -> UpstreamBundledLeases {
|
||||||
|
self.activate_bundled()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn reserve_microphone_sink(&self, generation: u64) -> Option<OwnedSemaphorePermit> {
|
pub async fn reserve_microphone_sink(&self, generation: u64) -> Option<OwnedSemaphorePermit> {
|
||||||
let permit = self
|
let permit = self
|
||||||
.microphone_sink_gate
|
.microphone_sink_gate
|
||||||
|
|||||||
@ -225,6 +225,58 @@ fn runtime_public_mapping_helpers_cover_legacy_and_bundled_paths() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Keeps `bundled_session_activation_resets_stale_playout_epoch` explicit because reconnecting a v2 webcam+mic stream must not inherit stale clocks from a superseded stream.
|
||||||
|
/// Inputs are two bundled activations with an old presented frame; output proves the new stream starts at local PTS zero and retires the old generations.
|
||||||
|
fn bundled_session_activation_resets_stale_playout_epoch() {
|
||||||
|
with_clean_offset_env(|| {
|
||||||
|
let runtime = UpstreamMediaRuntime::new();
|
||||||
|
let first = runtime.activate_bundled_session();
|
||||||
|
assert_eq!(first.camera.session_id, first.microphone.session_id);
|
||||||
|
|
||||||
|
let first_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10);
|
||||||
|
let first_decision = runtime.plan_bundled_pts(
|
||||||
|
UpstreamMediaKind::Camera,
|
||||||
|
500_000,
|
||||||
|
33_333,
|
||||||
|
100_000,
|
||||||
|
first_epoch,
|
||||||
|
);
|
||||||
|
let first_plan = match first_decision {
|
||||||
|
UpstreamPlanDecision::Play(plan) => plan,
|
||||||
|
other => panic!("expected first bundled play decision, got {other:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(first_plan.local_pts_us, 400_000);
|
||||||
|
runtime.mark_video_presented(first_plan.local_pts_us, first_plan.due_at);
|
||||||
|
assert_eq!(
|
||||||
|
runtime.snapshot().last_video_presented_pts_us,
|
||||||
|
Some(400_000)
|
||||||
|
);
|
||||||
|
|
||||||
|
let second = runtime.activate_bundled_session();
|
||||||
|
assert_ne!(first.camera.session_id, second.camera.session_id);
|
||||||
|
assert_eq!(second.camera.session_id, second.microphone.session_id);
|
||||||
|
assert!(!runtime.is_camera_active(first.camera.generation));
|
||||||
|
assert!(!runtime.is_microphone_active(first.microphone.generation));
|
||||||
|
assert!(runtime.is_camera_active(second.camera.generation));
|
||||||
|
assert!(runtime.is_microphone_active(second.microphone.generation));
|
||||||
|
assert_eq!(runtime.snapshot().last_video_presented_pts_us, None);
|
||||||
|
|
||||||
|
let second_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10);
|
||||||
|
let second_decision = runtime.plan_bundled_pts(
|
||||||
|
UpstreamMediaKind::Camera,
|
||||||
|
1_000,
|
||||||
|
33_333,
|
||||||
|
1_000,
|
||||||
|
second_epoch,
|
||||||
|
);
|
||||||
|
match second_decision {
|
||||||
|
UpstreamPlanDecision::Play(plan) => assert_eq!(plan.local_pts_us, 0),
|
||||||
|
other => panic!("expected fresh bundled play decision, got {other:?}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Keeps `runtime_soft_microphone_recovery_cycles_only_the_microphone_generation` explicit because a failed UAC handoff should not disturb active camera playout.
|
/// Keeps `runtime_soft_microphone_recovery_cycles_only_the_microphone_generation` explicit because a failed UAC handoff should not disturb active camera playout.
|
||||||
/// Inputs are an active camera lease and a soft microphone recovery request; output keeps camera active while cycling microphone state.
|
/// Inputs are an active camera lease and a soft microphone recovery request; output keeps camera active while cycling microphone state.
|
||||||
|
|||||||
@ -226,6 +226,14 @@ mod server_upstream_media_bundle_normal_mode {
|
|||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/server/src/main/relay_stream_lifecycle.rs"
|
"/server/src/main/relay_stream_lifecycle.rs"
|
||||||
));
|
));
|
||||||
|
const UPSTREAM_RUNTIME: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/server/src/upstream_media_runtime.rs"
|
||||||
|
));
|
||||||
|
const UPSTREAM_RUNTIME_LIFECYCLE: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/server/src/upstream_media_runtime/stream_lifecycle_methods.rs"
|
||||||
|
));
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bundled_rpc_preserves_one_media_unit_at_server_ingress() {
|
fn bundled_rpc_preserves_one_media_unit_at_server_ingress() {
|
||||||
@ -233,5 +241,8 @@ mod server_upstream_media_bundle_normal_mode {
|
|||||||
assert!(RELAY_RPC.contains("UpstreamMediaBundle"));
|
assert!(RELAY_RPC.contains("UpstreamMediaBundle"));
|
||||||
assert!(RELAY_LIFECYCLE.contains("client_capture_pts_us"));
|
assert!(RELAY_LIFECYCLE.contains("client_capture_pts_us"));
|
||||||
assert!(RELAY_RPC.contains("record_client_timing"));
|
assert!(RELAY_RPC.contains("record_client_timing"));
|
||||||
|
assert!(RELAY_RPC.contains("activate_bundled_session()"));
|
||||||
|
assert!(UPSTREAM_RUNTIME.contains("pub struct UpstreamBundledLeases"));
|
||||||
|
assert!(UPSTREAM_RUNTIME_LIFECYCLE.contains("pub fn activate_bundled_session"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -381,6 +381,30 @@ mod inputs_contract {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn begin_local_release_publishes_local_before_ungrab_finishes() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let state_path = dir.path().join("routing-state");
|
||||||
|
let mut agg = new_aggregator();
|
||||||
|
agg.routing_state_path = Some(state_path.clone());
|
||||||
|
|
||||||
|
agg.publish_routing_state_if_changed();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(&state_path).expect("initial routing state"),
|
||||||
|
"remote\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
agg.begin_local_release();
|
||||||
|
|
||||||
|
assert!(agg.pending_release);
|
||||||
|
assert!(!agg.remote_capture_active());
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(&state_path).expect("local routing state"),
|
||||||
|
"local\n",
|
||||||
|
"launcher polling must see local as soon as remote sends are disabled, not only after final ungrab"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_release_timeout_helpers_are_stable() {
|
fn local_release_timeout_helpers_are_stable() {
|
||||||
let mut agg = new_aggregator();
|
let mut agg = new_aggregator();
|
||||||
|
|||||||
@ -68,7 +68,9 @@ fn webcam_transport_selector_exposes_real_hevc_and_mjpeg_choices() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn webcam_transport_changes_are_staged_when_relay_is_live() {
|
fn webcam_transport_changes_are_staged_when_relay_is_live() {
|
||||||
for marker in [
|
for marker in [
|
||||||
"state.borrow_mut().select_webcam_transport(selected);",
|
"if widgets.webcam_transport_syncing.get()",
|
||||||
|
"state.try_borrow_mut()",
|
||||||
|
"state_mut.select_webcam_transport(selected);",
|
||||||
"child_proc.borrow().is_some()",
|
"child_proc.borrow().is_some()",
|
||||||
"apply_live_media_device_change(",
|
"apply_live_media_device_change(",
|
||||||
"\"Webcam transport\"",
|
"\"Webcam transport\"",
|
||||||
@ -81,6 +83,8 @@ fn webcam_transport_changes_are_staged_when_relay_is_live() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for marker in [
|
for marker in [
|
||||||
|
"widgets.webcam_transport_syncing.set(true);",
|
||||||
|
"widgets.webcam_transport_syncing.set(false);",
|
||||||
".webcam_transport_combo\n .set_sensitive(state.channels.camera);",
|
".webcam_transport_combo\n .set_sensitive(state.channels.camera);",
|
||||||
"Changing upstream webcam transport restarts the live camera path; the picture may pause briefly.",
|
"Changing upstream webcam transport restarts the live camera path; the picture may pause briefly.",
|
||||||
"Use the server-advertised upstream webcam transport for the next relay launch; MJPEG is the safe calibrated default.",
|
"Use the server-advertised upstream webcam transport for the next relay launch; MJPEG is the safe calibrated default.",
|
||||||
|
|||||||
@ -135,6 +135,14 @@ const UI_COMPONENTS_SRC: &str = concat!(
|
|||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/client/src/launcher/ui_components.rs"
|
"/client/src/launcher/ui_components.rs"
|
||||||
)),
|
)),
|
||||||
|
include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/client/src/launcher/ui_components/types.rs"
|
||||||
|
)),
|
||||||
|
include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/client/src/launcher/ui_components/assemble_view.rs"
|
||||||
|
)),
|
||||||
include_str!(concat!(
|
include_str!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/client/src/launcher/ui_components/build_shell.rs"
|
"/client/src/launcher/ui_components/build_shell.rs"
|
||||||
@ -264,6 +272,10 @@ fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() {
|
|||||||
assert!(UI_SRC.contains("let camera_quality_syncing = Rc::new(Cell::new(false));"));
|
assert!(UI_SRC.contains("let camera_quality_syncing = Rc::new(Cell::new(false));"));
|
||||||
assert!(UI_SRC.contains("camera_quality_syncing.set(true);"));
|
assert!(UI_SRC.contains("camera_quality_syncing.set(true);"));
|
||||||
assert!(UI_SRC.contains("if camera_quality_syncing.get()"));
|
assert!(UI_SRC.contains("if camera_quality_syncing.get()"));
|
||||||
|
assert!(UI_COMPONENTS_SRC.contains("webcam_transport_syncing: Rc<Cell<bool>>"));
|
||||||
|
assert!(UI_COMPONENTS_SRC.contains("upstream_audio_transport_syncing: Rc<Cell<bool>>"));
|
||||||
|
assert!(UI_RUNTIME_SRC.contains("widgets.webcam_transport_syncing.set(true);"));
|
||||||
|
assert!(UI_SRC.contains("if widgets.webcam_transport_syncing.get()"));
|
||||||
assert!(UI_SRC.contains("state.try_borrow_mut()"));
|
assert!(UI_SRC.contains("state.try_borrow_mut()"));
|
||||||
assert!(UI_SRC.contains("tests.set_camera_quality"));
|
assert!(UI_SRC.contains("tests.set_camera_quality"));
|
||||||
assert!(DEVICE_TEST_SRC.contains("pub fn set_camera_quality"));
|
assert!(DEVICE_TEST_SRC.contains("pub fn set_camera_quality"));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user