From 9574ab71c83eca9426c733a05caf8039b3242117 Mon Sep 17 00:00:00 2001 From: Julian Ostarek Date: Thu, 22 Oct 2026 12:28:30 +0200 Subject: [PATCH] Follow the Cloudflare worker's new settings contract cloudflare-browser-simulator #7 changes the worker's create and command requests to take hyper.video's settings values, and makes the settings that hyper.video can decide optional. The worker then rejects the create request the simulator sends today, whose track limit is null for no limit. - The submodule moves to the worker's main at #7's merge, 4a7f2fb. - A participant that leaves auto gain control, blur, a video constraint or the track limit to hyper.video leaves the field out of the create request, so that the worker unsets the setting. "unlimited" and a number pass through, in the create request and in the command. - Cloudflare participants take none and hyper-voice as they are, instead of turning hyper-voice into none. - The video constraints map 4320p to the worker's 4320p, both ways. --- .codex/skills/run-headless-simulator/SKILL.md | 2 - browser/src/participant/cloudflare/mod.rs | 423 ++++++------------ browser/tests/cloudflare_driver.rs | 15 +- cloudflare-browser-simulator | 2 +- cloudflare-worker-client/src/client.rs | 10 +- docs/browser-driver.md | 1 - src/headless.rs | 2 +- 7 files changed, 156 insertions(+), 299 deletions(-) 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,