diff --git a/.codex/skills/run-headless-simulator/SKILL.md b/.codex/skills/run-headless-simulator/SKILL.md index efde94b..421fb5c 100644 --- a/.codex/skills/run-headless-simulator/SKILL.md +++ b/.codex/skills/run-headless-simulator/SKILL.md @@ -227,6 +227,4 @@ Noise suppression: - `none` - `hyper-voice` -Cloudflare participants run with `none` only. - Run `just dev --logging info headless --help` or `hyper-client-simulator headless --help` if a flag fails. diff --git a/browser/src/participant/cloudflare/mod.rs b/browser/src/participant/cloudflare/mod.rs index 4e19efb..dad625a 100644 --- a/browser/src/participant/cloudflare/mod.rs +++ b/browser/src/participant/cloudflare/mod.rs @@ -20,7 +20,6 @@ use client_simulator_config::{ CloudflareConfig, NoiseSuppression, TransportMode, - VideoConstraint, VideoMaxConcurrentTracks, }; use cloudflare_worker_client::{ @@ -119,12 +118,8 @@ fn map_worker_browser_entry(participant_name: &str, entry: &types::BrowserLogEnt ) } -fn store_cached_state( - participant_name: &str, - cached_state: &Arc>, - state: &types::ParticipantState, -) { - *cached_state.lock().unwrap() = map_state(participant_name, state); +fn store_cached_state(cached_state: &Arc>, state: &types::ParticipantState) { + *cached_state.lock().unwrap() = map_state(state); } impl CloudflareSession { @@ -231,20 +226,6 @@ impl CloudflareSession { settings.transport = TransportMode::WebRTC; } - match settings.noise_suppression { - NoiseSuppression::Disabled => {} - NoiseSuppression::HyperVoice => { - self.log_message( - "warn", - format!( - "Cloudflare backend only supports noise suppression none; normalizing configured {} to none", - settings.noise_suppression - ), - ); - settings.noise_suppression = NoiseSuppression::Disabled; - } - } - settings } @@ -266,8 +247,8 @@ impl CloudflareSession { }) } - fn command_request(message: ParticipantMessage) -> Result { - Ok(match message { + fn command_request(message: ParticipantMessage) -> types::SessionCommandRequest { + match message { ParticipantMessage::Join => types::SessionCommandRequest::Join, ParticipantMessage::Leave => types::SessionCommandRequest::Leave, ParticipantMessage::Close => types::SessionCommandRequest::Leave, @@ -276,7 +257,7 @@ impl CloudflareSession { ParticipantMessage::ToggleScreenshare => types::SessionCommandRequest::ToggleScreenshare, ParticipantMessage::ToggleAutoGainControl => types::SessionCommandRequest::ToggleAutoGainControl, ParticipantMessage::SetNoiseSuppression(value) => types::SessionCommandRequest::SetNoiseSuppression { - noise_suppression: map_command_noise_suppression(value)?, + noise_suppression: map_command_noise_suppression(value), }, ParticipantMessage::SetVideoConstraintPublishWebcam(value) => { types::SessionCommandRequest::SetVideoConstraintPublishWebcam { @@ -290,11 +271,20 @@ impl CloudflareSession { } ParticipantMessage::SetVideoMaxConcurrentTracks(value) => { types::SessionCommandRequest::SetVideoMaxConcurrentTracks { - video_max_concurrent_tracks: value.limit().map(|value| value as u64), + video_max_concurrent_tracks: match value { + VideoMaxConcurrentTracks::Unlimited => { + types::SessionCommandRequestVideoMaxConcurrentTracks::String( + types::SessionCommandRequestVideoMaxConcurrentTracksString::Unlimited, + ) + } + VideoMaxConcurrentTracks::Limit(tracks) => { + types::SessionCommandRequestVideoMaxConcurrentTracks::Integer(tracks as u64) + } + }, } } ParticipantMessage::ToggleBackgroundBlur => types::SessionCommandRequest::ToggleBackgroundBlur, - }) + } } fn cached_state(&self) -> ParticipantState { @@ -302,7 +292,7 @@ impl CloudflareSession { } fn update_cached_state(&self, state: &types::ParticipantState) { - store_cached_state(&self.launch_spec.username, &self.cached_state, state); + store_cached_state(&self.cached_state, state); } fn effective_health_poll_interval(&self) -> Duration { @@ -355,7 +345,7 @@ impl CloudflareSession { forward_worker_entries(&participant_name, &response.log); forward_worker_browser_entries(&participant_name, &response.browser_log); - store_cached_state(&participant_name, &cached_state, &response.state); + store_cached_state(&cached_state, &response.state); if !response.state.running { let _ = termination_tx.send(Some(DriverTermination::new( @@ -455,7 +445,7 @@ impl CloudflareSession { .session_id .clone() .ok_or_else(|| eyre!("Cloudflare session is not started"))?; - let request = Self::command_request(message)?; + let request = Self::command_request(message); let response = self.worker_client()?.command_session(&session_id, &request).await?; self.log_worker_entries(&response.log); self.log_worker_browser_entries(&response.browser_log); @@ -511,46 +501,40 @@ fn map_frontend_kind(frontend_kind: ResolvedFrontendKind) -> types::SessionCreat /// Maps the settings that [`CloudflareSession::normalized_settings`] returns onto the worker's. /// -/// The worker's schema lacks `hyper-voice`, which `normalized_settings` replaces with `none`, so it maps to `none`. -/// The schema also has no way to leave a setting to hyper.video, so a setting that the participant leaves to -/// hyper.video sends hyper.video's default, and the track limit no limit, `null`. +/// A setting that the participant leaves to hyper.video stays out of the request. The worker then unsets it, so that +/// the session's value applies, else hyper.video's default. fn map_settings(settings: &crate::participant::shared::ParticipantSettings) -> types::ParticipantSettings { types::ParticipantSettings { audio_enabled: settings.audio_enabled, - auto_gain_control: settings.auto_gain_control.own().unwrap_or(true), - blur: settings.blur.own().unwrap_or(false), - noise_suppression: match settings.noise_suppression { - NoiseSuppression::Disabled | NoiseSuppression::HyperVoice => { - types::ParticipantSettingsNoiseSuppression::None - } - }, + auto_gain_control: settings.auto_gain_control.own(), + blur: settings.blur.own(), + noise_suppression: map_settings_noise_suppression(settings.noise_suppression), screenshare_enabled: settings.screenshare_enabled, transport: match settings.transport { client_simulator_config::TransportMode::WebRTC => types::ParticipantSettingsTransport::Webrtc, client_simulator_config::TransportMode::WebTransport => types::ParticipantSettingsTransport::Webtransport, }, - video_constraint_publish_webcam: map_video_constraint_publish_webcam( - settings - .video_constraint_publish_webcam - .own() - .unwrap_or(VideoConstraint::None), - ), - video_constraint_subscribe: map_video_constraint_subscribe( - settings - .video_constraint_subscribe - .own() - .unwrap_or(VideoConstraint::None), - ), - video_max_concurrent_tracks: settings - .video_max_concurrent_tracks + video_constraint_publish_webcam: settings + .video_constraint_publish_webcam + .own() + .map(map_video_constraint_publish_webcam), + video_constraint_subscribe: settings + .video_constraint_subscribe .own() - .and_then(VideoMaxConcurrentTracks::limit) - .map(|value| value as u64), + .map(map_video_constraint_subscribe), + video_max_concurrent_tracks: settings.video_max_concurrent_tracks.own().map(|value| match value { + VideoMaxConcurrentTracks::Unlimited => types::ParticipantSettingsVideoMaxConcurrentTracks::String( + types::ParticipantSettingsVideoMaxConcurrentTracksString::Unlimited, + ), + VideoMaxConcurrentTracks::Limit(tracks) => { + types::ParticipantSettingsVideoMaxConcurrentTracks::Integer(tracks as u64) + } + }), video_enabled: settings.video_enabled, } } -fn map_state(participant_name: &str, state: &types::ParticipantState) -> ParticipantState { +fn map_state(state: &types::ParticipantState) -> ParticipantState { ParticipantState { username: String::new(), running: state.running, @@ -559,7 +543,7 @@ fn map_state(participant_name: &str, state: &types::ParticipantState) -> Partici muted: state.muted, video_activated: state.video_activated, auto_gain_control: state.auto_gain_control, - noise_suppression: map_state_noise_suppression(participant_name, state.noise_suppression), + noise_suppression: map_state_noise_suppression(state.noise_suppression), transport_mode: match state.transport_mode { types::ParticipantStateTransportMode::Webrtc => client_simulator_config::TransportMode::WebRTC, types::ParticipantStateTransportMode::Webtransport => client_simulator_config::TransportMode::WebTransport, @@ -578,63 +562,27 @@ fn map_state(participant_name: &str, state: &types::ParticipantState) -> Partici } } -/// Maps a runtime noise suppression change onto the worker's, refusing a value that the worker can't select. -/// -/// The worker's schema takes `none` and 16 engines that hyper.video no longer offers, but not `hyper-voice`, so -/// Cloudflare participants run without noise suppression until the worker takes `hyper-voice`. -fn map_command_noise_suppression(value: NoiseSuppression) -> Result { +fn map_settings_noise_suppression(value: NoiseSuppression) -> types::ParticipantSettingsNoiseSuppression { match value { - NoiseSuppression::Disabled => Ok(types::SessionCommandRequestNoiseSuppression::None), - NoiseSuppression::HyperVoice => Err(eyre!( - "Cloudflare backend only supports noise suppression none, not {value}" - )), + NoiseSuppression::Disabled => types::ParticipantSettingsNoiseSuppression::None, + NoiseSuppression::HyperVoice => types::ParticipantSettingsNoiseSuppression::HyperVoice, } } -/// Maps the worker's reported noise suppression, reporting `none`, with a warning, for an engine that hyper.video no -/// longer offers. -/// -/// The backend only requests `none` (see [`map_command_noise_suppression`]), so the worker reports such an engine only -/// if the page runs one that the simulator did not select. -fn map_state_noise_suppression( - participant_name: &str, - value: types::ParticipantStateNoiseSuppression, -) -> NoiseSuppression { +fn map_command_noise_suppression(value: NoiseSuppression) -> types::SessionCommandRequestNoiseSuppression { + match value { + NoiseSuppression::Disabled => types::SessionCommandRequestNoiseSuppression::None, + NoiseSuppression::HyperVoice => types::SessionCommandRequestNoiseSuppression::HyperVoice, + } +} + +fn map_state_noise_suppression(value: types::ParticipantStateNoiseSuppression) -> NoiseSuppression { match value { types::ParticipantStateNoiseSuppression::None => NoiseSuppression::Disabled, - types::ParticipantStateNoiseSuppression::Deepfilternet - | types::ParticipantStateNoiseSuppression::Rnnoise - | types::ParticipantStateNoiseSuppression::IrisCarthy - | types::ParticipantStateNoiseSuppression::KrispHigh - | types::ParticipantStateNoiseSuppression::KrispMedium - | types::ParticipantStateNoiseSuppression::KrispLow - | types::ParticipantStateNoiseSuppression::KrispHighWithBvc - | types::ParticipantStateNoiseSuppression::KrispMediumWithBvc - | types::ParticipantStateNoiseSuppression::AiCousticsSparrowXxs - | types::ParticipantStateNoiseSuppression::AiCousticsSparrowXs - | types::ParticipantStateNoiseSuppression::AiCousticsSparrowS - | types::ParticipantStateNoiseSuppression::AiCousticsSparrowL - | types::ParticipantStateNoiseSuppression::AiCousticsSparrowXxs48khz - | types::ParticipantStateNoiseSuppression::AiCousticsSparrowXs48khz - | types::ParticipantStateNoiseSuppression::AiCousticsRookS48khz - | types::ParticipantStateNoiseSuppression::AiCousticsRookL48khz => { - emit_log_message( - participant_name, - "warn", - format!( - "Cloudflare worker reports noise suppression {value}, which hyper.video no longer offers; reporting {}", - NoiseSuppression::Disabled - ), - ); - NoiseSuppression::Disabled - } + types::ParticipantStateNoiseSuppression::HyperVoice => NoiseSuppression::HyperVoice, } } -/// Maps the outgoing webcam constraint to the worker's. -/// -/// The worker's schema has no 4320p. hyper.video has no resolution level above it, so a 4320p limit leaves every -/// resolution, as `none` does. fn map_video_constraint_publish_webcam( value: client_simulator_config::VideoConstraint, ) -> types::ParticipantSettingsVideoConstraintPublishWebcam { @@ -655,12 +603,12 @@ fn map_video_constraint_publish_webcam( client_simulator_config::VideoConstraint::P2160 => { types::ParticipantSettingsVideoConstraintPublishWebcam::X2160p } - client_simulator_config::VideoConstraint::P4320 => types::ParticipantSettingsVideoConstraintPublishWebcam::None, + client_simulator_config::VideoConstraint::P4320 => { + types::ParticipantSettingsVideoConstraintPublishWebcam::X4320p + } } } -/// Maps the incoming video constraint to the worker's, 4320p to `none`, as [`map_video_constraint_publish_webcam`] -/// says. fn map_video_constraint_subscribe( value: client_simulator_config::VideoConstraint, ) -> types::ParticipantSettingsVideoConstraintSubscribe { @@ -675,7 +623,7 @@ fn map_video_constraint_subscribe( client_simulator_config::VideoConstraint::P1080 => types::ParticipantSettingsVideoConstraintSubscribe::X1080p, client_simulator_config::VideoConstraint::P1440 => types::ParticipantSettingsVideoConstraintSubscribe::X1440p, client_simulator_config::VideoConstraint::P2160 => types::ParticipantSettingsVideoConstraintSubscribe::X2160p, - client_simulator_config::VideoConstraint::P4320 => types::ParticipantSettingsVideoConstraintSubscribe::None, + client_simulator_config::VideoConstraint::P4320 => types::ParticipantSettingsVideoConstraintSubscribe::X4320p, } } @@ -693,6 +641,7 @@ fn map_state_video_constraint_publish_webcam( types::ParticipantStateVideoConstraintPublishWebcam::X1080p => client_simulator_config::VideoConstraint::P1080, types::ParticipantStateVideoConstraintPublishWebcam::X1440p => client_simulator_config::VideoConstraint::P1440, types::ParticipantStateVideoConstraintPublishWebcam::X2160p => client_simulator_config::VideoConstraint::P2160, + types::ParticipantStateVideoConstraintPublishWebcam::X4320p => client_simulator_config::VideoConstraint::P4320, } } @@ -710,11 +659,10 @@ fn map_state_video_constraint_subscribe( types::ParticipantStateVideoConstraintSubscribe::X1080p => client_simulator_config::VideoConstraint::P1080, types::ParticipantStateVideoConstraintSubscribe::X1440p => client_simulator_config::VideoConstraint::P1440, types::ParticipantStateVideoConstraintSubscribe::X2160p => client_simulator_config::VideoConstraint::P2160, + types::ParticipantStateVideoConstraintSubscribe::X4320p => client_simulator_config::VideoConstraint::P4320, } } -/// Maps the outgoing webcam constraint to the worker's command, 4320p to `none`, as -/// [`map_video_constraint_publish_webcam`] says. fn map_command_video_constraint_publish_webcam( value: client_simulator_config::VideoConstraint, ) -> types::SessionCommandRequestVideoConstraintPublishWebcam { @@ -748,13 +696,11 @@ fn map_command_video_constraint_publish_webcam( types::SessionCommandRequestVideoConstraintPublishWebcam::X2160p } client_simulator_config::VideoConstraint::P4320 => { - types::SessionCommandRequestVideoConstraintPublishWebcam::None + types::SessionCommandRequestVideoConstraintPublishWebcam::X4320p } } } -/// Maps the incoming video constraint to the worker's command, 4320p to `none`, as -/// [`map_video_constraint_publish_webcam`] says. fn map_command_video_constraint_subscribe( value: client_simulator_config::VideoConstraint, ) -> types::SessionCommandRequestVideoConstraintSubscribe { @@ -769,7 +715,7 @@ fn map_command_video_constraint_subscribe( client_simulator_config::VideoConstraint::P1080 => types::SessionCommandRequestVideoConstraintSubscribe::X1080p, client_simulator_config::VideoConstraint::P1440 => types::SessionCommandRequestVideoConstraintSubscribe::X1440p, client_simulator_config::VideoConstraint::P2160 => types::SessionCommandRequestVideoConstraintSubscribe::X2160p, - client_simulator_config::VideoConstraint::P4320 => types::SessionCommandRequestVideoConstraintSubscribe::None, + client_simulator_config::VideoConstraint::P4320 => types::SessionCommandRequestVideoConstraintSubscribe::X4320p, } } @@ -787,6 +733,8 @@ pub(crate) fn take_spawned_participants_for_test() -> Vec { #[cfg(test)] mod tests { use super::{ + map_settings, + map_state, map_worker_browser_entry, CloudflareLaunchOptions, CloudflareSession, @@ -997,8 +945,6 @@ mod tests { "screenshareEnabled": false, "transport": "webrtc", "videoConstraintPublishWebcam": "720p", - "videoConstraintSubscribe": "none", - "videoMaxConcurrentTracks": null, "videoEnabled": true } }) @@ -1008,70 +954,6 @@ mod tests { assert_eq!(requests[1].path, "/sessions/cf-session-123/close"); } - /// The backend reports `none`, with a warning, when the worker reports an engine that hyper.video no longer offers. - #[tokio::test] - async fn start_reports_none_for_old_worker_engine() { - let captured_logs = CapturedLogs::new(); - let responses = VecDeque::from(vec![ - MockResponse::json( - 200, - json!({ - "ok": true, - "sessionId": "cf-session-old-engine", - "state": { - "running": true, - "joined": true, - "muted": false, - "videoActivated": true, - "screenshareActivated": false, - "autoGainControl": true, - "noiseSuppression": "ai-coustics-rook-s-48khz", - "transportMode": "webrtc", - "videoConstraintPublishWebcam": "720p", - "videoConstraintSubscribe": "none", - "videoMaxConcurrentTracks": null, - "backgroundBlur": true - }, - "log": [], - }), - ), - MockResponse::json( - 200, - json!({ - "ok": true, - "sessionId": "cf-session-old-engine", - "log": [], - }), - ), - ]); - let (base_url, _requests, server) = spawn_http_server(responses).await; - let mut session = CloudflareSession::new_for_test( - launch_spec(ResolvedFrontendKind::HyperLite, &format!("{base_url}/room/demo")), - launch_options(false, FakeMedia::None), - CloudflareConfig { - base_url: Url::parse(&base_url).unwrap(), - request_timeout_seconds: 5, - session_timeout_ms: 120_000, - navigation_timeout_ms: 30_000, - selector_timeout_ms: 10_000, - debug: false, - health_poll_interval_ms: 60_000, - }, - ); - - session.start().await.unwrap(); - - let state = session.refresh_state().await.unwrap(); - assert!(state.auto_gain_control); - assert_eq!(state.noise_suppression, NoiseSuppression::Disabled); - assert!(captured_logs.output().contains( - "reports noise suppression ai-coustics-rook-s-48khz, which hyper.video no longer offers; reporting none" - )); - - session.close().await.unwrap(); - server.abort(); - } - #[tokio::test] async fn start_forwards_worker_browser_logs() { let captured_logs = CapturedLogs::new(); @@ -1525,119 +1407,88 @@ mod tests { assert!(logs.contains("normalizing configured")); } - #[tokio::test] - async fn start_normalizes_noise_suppression_to_none_for_cloudflare() { - let captured_logs = CapturedLogs::new(); - let responses = VecDeque::from(vec![ - MockResponse::json( - 200, - json!({ - "ok": true, - "sessionId": "cf-session-noise-suppression", - "state": worker_state_json(false, false, false, false, true, "none", "720p", true), - "log": [], - }), - ), - MockResponse::json( - 200, - json!({ - "ok": true, - "sessionId": "cf-session-noise-suppression", - "log": [], - }), - ), - ]); - let (base_url, requests, server) = spawn_http_server(responses).await; - let mut spec = launch_spec(ResolvedFrontendKind::HyperCore, &format!("{base_url}/room/demo")); - spec.settings.noise_suppression = NoiseSuppression::HyperVoice; - let mut session = CloudflareSession::new_for_test( - spec, - launch_options(false, FakeMedia::None), - CloudflareConfig { - base_url: Url::parse(&base_url).unwrap(), - request_timeout_seconds: 5, - session_timeout_ms: 120_000, - navigation_timeout_ms: 30_000, - selector_timeout_ms: 10_000, - debug: false, - health_poll_interval_ms: 60_000, - }, - ); - - session.start().await.unwrap(); - - let state = session.refresh_state().await.unwrap(); - assert_eq!(state.noise_suppression, NoiseSuppression::Disabled); - - session.close().await.unwrap(); - server.abort(); - - let requests = requests.lock().unwrap().clone(); - assert_eq!(requests[0].path, "/sessions"); - assert_eq!( - serde_json::from_str::(&requests[0].body).unwrap()["settings"]["noiseSuppression"], - json!("none") - ); - - let logs = captured_logs.output(); - assert!(logs.contains("only supports noise suppression none; normalizing configured hyper-voice to none")); + /// A participant's own settings reach the worker as they are, and the ones it leaves to hyper.video stay out of the + /// request. + #[test] + fn settings_map_to_worker_schema() { + let mut settings = launch_spec(ResolvedFrontendKind::HyperCore, "https://example.com/room/demo").settings; + settings.noise_suppression = NoiseSuppression::HyperVoice; + settings.auto_gain_control = OwnOrDefault::Own(false); + settings.blur = OwnOrDefault::Own(false); + settings.video_constraint_publish_webcam = OwnOrDefault::Own(VideoConstraint::P4320); + settings.video_constraint_subscribe = OwnOrDefault::Own(VideoConstraint::P4320); + settings.video_max_concurrent_tracks = OwnOrDefault::Own(VideoMaxConcurrentTracks::Limit(3)); + + let request = serde_json::to_value(map_settings(&settings)).unwrap(); + assert_eq!(request["noiseSuppression"], json!("hyper-voice")); + assert_eq!(request["autoGainControl"], json!(false)); + assert_eq!(request["blur"], json!(false)); + assert_eq!(request["videoConstraintPublishWebcam"], json!("4320p")); + assert_eq!(request["videoConstraintSubscribe"], json!("4320p")); + assert_eq!(request["videoMaxConcurrentTracks"], json!(3)); + + settings.video_max_concurrent_tracks = OwnOrDefault::Own(VideoMaxConcurrentTracks::Unlimited); + let request = serde_json::to_value(map_settings(&settings)).unwrap(); + assert_eq!(request["videoMaxConcurrentTracks"], json!("unlimited")); + + settings.auto_gain_control = OwnOrDefault::Default; + settings.blur = OwnOrDefault::Default; + settings.video_constraint_publish_webcam = OwnOrDefault::Default; + settings.video_constraint_subscribe = OwnOrDefault::Default; + settings.video_max_concurrent_tracks = OwnOrDefault::Default; + let request = serde_json::to_value(map_settings(&settings)).unwrap(); + for key in [ + "autoGainControl", + "blur", + "videoConstraintPublishWebcam", + "videoConstraintSubscribe", + "videoMaxConcurrentTracks", + ] { + assert_eq!(request.get(key), None, "{key}"); + } } - #[tokio::test] - async fn set_noise_suppression_refuses_engines_for_cloudflare() { - let responses = VecDeque::from(vec![ - MockResponse::json( - 200, - json!({ - "ok": true, - "sessionId": "cf-session-set-noise-suppression", - "state": worker_state_json(true, false, false, false, true, "none", "720p", true), - "log": [], - }), + #[test] + fn commands_map_to_worker_schema() { + let cases = [ + ( + ParticipantMessage::SetNoiseSuppression(NoiseSuppression::HyperVoice), + json!({ "type": "set-noise-suppression", "noiseSuppression": "hyper-voice" }), ), - MockResponse::json( - 200, - json!({ - "ok": true, - "sessionId": "cf-session-set-noise-suppression", - "log": [], - }), + ( + ParticipantMessage::SetVideoConstraintPublishWebcam(VideoConstraint::P4320), + json!({ "type": "set-video-constraint-publish-webcam", "videoConstraintPublishWebcam": "4320p" }), ), - ]); - let (base_url, requests, server) = spawn_http_server(responses).await; - let mut session = CloudflareSession::new_for_test( - launch_spec(ResolvedFrontendKind::HyperCore, &format!("{base_url}/room/demo")), - launch_options(false, FakeMedia::None), - CloudflareConfig { - base_url: Url::parse(&base_url).unwrap(), - request_timeout_seconds: 5, - session_timeout_ms: 120_000, - navigation_timeout_ms: 30_000, - selector_timeout_ms: 10_000, - debug: false, - health_poll_interval_ms: 60_000, - }, - ); + ( + ParticipantMessage::SetVideoConstraintSubscribe(VideoConstraint::P4320), + json!({ "type": "set-video-constraint-subscribe", "videoConstraintSubscribe": "4320p" }), + ), + ( + ParticipantMessage::SetVideoMaxConcurrentTracks(VideoMaxConcurrentTracks::Unlimited), + json!({ "type": "set-video-max-concurrent-tracks", "videoMaxConcurrentTracks": "unlimited" }), + ), + ( + ParticipantMessage::SetVideoMaxConcurrentTracks(VideoMaxConcurrentTracks::Limit(2)), + json!({ "type": "set-video-max-concurrent-tracks", "videoMaxConcurrentTracks": 2 }), + ), + ]; - session.start().await.unwrap(); + for (message, expected) in cases { + let request = serde_json::to_value(CloudflareSession::command_request(message)).unwrap(); + assert_eq!(request, expected); + } + } - let error = session - .handle_command(ParticipantMessage::SetNoiseSuppression(NoiseSuppression::HyperVoice)) - .await - .unwrap_err(); - assert_eq!( - error.to_string(), - "Cloudflare backend only supports noise suppression none, not hyper-voice" - ); + #[test] + fn state_maps_hyper_voice_and_4320p_from_worker() { + let mut worker_state = worker_state_json(true, false, true, false, true, "hyper-voice", "4320p", false); + worker_state["videoConstraintSubscribe"] = json!("4320p"); - session.close().await.unwrap(); - server.abort(); + let state = map_state(&serde_json::from_value(worker_state).unwrap()); - // Only the create and close requests reach the worker. - let requests = requests.lock().unwrap().clone(); - assert_eq!(requests.len(), 2); - assert_eq!(requests[0].path, "/sessions"); - assert_eq!(requests[1].path, "/sessions/cf-session-set-noise-suppression/close"); + assert_eq!(state.noise_suppression, NoiseSuppression::HyperVoice); + assert_eq!(state.video_constraint_publish_webcam, VideoConstraint::P4320); + assert_eq!(state.video_constraint_subscribe, VideoConstraint::P4320); } #[tokio::test] diff --git a/browser/tests/cloudflare_driver.rs b/browser/tests/cloudflare_driver.rs index 411fc97..2607c83 100644 --- a/browser/tests/cloudflare_driver.rs +++ b/browser/tests/cloudflare_driver.rs @@ -269,7 +269,7 @@ async fn cloudflare_runtime_delegates_hyper_core_authentication_to_the_worker() "videoActivated": true, "screenshareActivated": false, "autoGainControl": true, - "noiseSuppression": "none", + "noiseSuppression": "hyper-voice", "transportMode": "webrtc", "videoConstraintPublishWebcam": "1080p", "videoConstraintSubscribe": "none", @@ -317,7 +317,7 @@ async fn cloudflare_runtime_delegates_hyper_core_authentication_to_the_worker() assert!(started.joined); assert_eq!( started.noise_suppression, - client_simulator_config::NoiseSuppression::Disabled + client_simulator_config::NoiseSuppression::HyperVoice ); participant.close().await; @@ -331,6 +331,17 @@ async fn cloudflare_runtime_delegates_hyper_core_authentication_to_the_worker() assert_eq!(requests[0].path, "/sessions"); assert!(request_json(&requests[0])["displayName"].is_string()); assert!(request_json(&requests[0]).get("hyperSessionCookie").is_none()); + // A participant that leaves its settings to hyper.video leaves them out of the request. + let settings = &request_json(&requests[0])["settings"]; + for key in [ + "autoGainControl", + "blur", + "videoConstraintPublishWebcam", + "videoConstraintSubscribe", + "videoMaxConcurrentTracks", + ] { + assert!(settings.get(key).is_none(), "{key}"); + } assert_eq!(requests[1].path, "/sessions/cf-runtime-core/commands"); assert_eq!(request_json(&requests[1]), json!({ "type": "leave" })); assert_eq!(requests[2].path, "/sessions/cf-runtime-core/close"); diff --git a/cloudflare-browser-simulator b/cloudflare-browser-simulator index 1043190..4a7f2fb 160000 --- a/cloudflare-browser-simulator +++ b/cloudflare-browser-simulator @@ -1 +1 @@ -Subproject commit 1043190a19344ccb935d1f6997aef43529450d75 +Subproject commit 4a7f2fba5bf965e37b766e418b5575ed89fed983 diff --git a/cloudflare-worker-client/src/client.rs b/cloudflare-worker-client/src/client.rs index 10f75cf..07498dc 100644 --- a/cloudflare-worker-client/src/client.rs +++ b/cloudflare-worker-client/src/client.rs @@ -247,8 +247,6 @@ mod tests { ParticipantSettings, ParticipantSettingsNoiseSuppression, ParticipantSettingsTransport, - ParticipantSettingsVideoConstraintPublishWebcam, - ParticipantSettingsVideoConstraintSubscribe, SessionCreateRequest, SessionCreateRequestDisplayName, SessionCreateRequestFrontendKind, @@ -364,13 +362,13 @@ mod tests { session_timeout_ms: Some(600_000.0), settings: ParticipantSettings { audio_enabled: true, - auto_gain_control: true, - blur: false, + auto_gain_control: None, + blur: None, noise_suppression: ParticipantSettingsNoiseSuppression::None, screenshare_enabled: false, transport: ParticipantSettingsTransport::Webrtc, - video_constraint_publish_webcam: ParticipantSettingsVideoConstraintPublishWebcam::None, - video_constraint_subscribe: ParticipantSettingsVideoConstraintSubscribe::None, + video_constraint_publish_webcam: None, + video_constraint_subscribe: None, video_max_concurrent_tracks: None, video_enabled: true, }, diff --git a/docs/browser-driver.md b/docs/browser-driver.md index f608727..9e1e9ff 100644 --- a/docs/browser-driver.md +++ b/docs/browser-driver.md @@ -93,7 +93,6 @@ For parity with the current local backend, `start()` should apply those launch s - Hyper Core applies noise suppression, blur, camera resolution, and transport before join, then applies audio/video in the lobby, and applies initial screenshare after joining. See [`browser/src/participant/local/core.rs`](../browser/src/participant/local/core.rs) `:143-171`. - Hyper Lite currently only applies audio/video/screenshare and ignores advanced media settings. See [`browser/src/participant/local/lite.rs`](../browser/src/participant/local/lite.rs) `:90-106`, `:153-175`. -- The Cloudflare backend applies noise suppression `none` only, because the worker accepts no other value of hyper.video's setting. It sends `none` in place of `hyper-voice`, with a warning, and refuses `SetNoiseSuppression` with it. See [`browser/src/participant/cloudflare/mod.rs`](../browser/src/participant/cloudflare/mod.rs). There is currently no runtime command for: diff --git a/src/headless.rs b/src/headless.rs index 72407bc..75a3120 100644 --- a/src/headless.rs +++ b/src/headless.rs @@ -55,7 +55,7 @@ pub struct HeadlessArgs { #[clap(long = "auto-gain-control", value_name = "ON")] pub auto_gain_control: Option>, - /// Noise suppression model: none or hyper-voice. Cloudflare participants always run with none. + /// Noise suppression model: none or hyper-voice. #[clap(long = "noise-suppression", value_name = "MODEL")] pub noise_suppression: Option,