Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .codex/skills/run-headless-simulator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Headless option names use kebab-case. Participant JSON fields use snake_case.

Repeat `--participant JSON` to add participants. It has no matching JSON field.

Auto gain control, background blur, both video constraints and the track limit default to `default`, which leaves the setting to hyper.video: an admin's value for the session, else hyper.video's default. JSON `null` inherits the shared value instead.
Auto gain control, the transport, background blur, both video constraints and the track limit default to `default`, which leaves the setting to hyper.video: an admin's value for the session, else hyper.video's default. JSON `null` inherits the shared value instead.

Use a nonnegative integer, `unlimited` or `default` for `--video-max-concurrent-tracks` or `video_max_concurrent_tracks`. hyper.video's default is one track.

Expand All @@ -204,6 +204,7 @@ Backend:

Transport:

- `default`
- `webtransport`
- `webrtc`

Expand Down
11 changes: 6 additions & 5 deletions browser/src/participant/cloudflare/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use client_simulator_config::{
media::FakeMedia,
CloudflareConfig,
NoiseSuppression,
OwnOrDefault,
TransportMode,
VideoConstraint,
VideoMaxConcurrentTracks,
Expand Down Expand Up @@ -220,15 +221,15 @@ impl CloudflareSession {
fn normalized_settings(&self) -> crate::participant::shared::ParticipantSettings {
let mut settings = self.launch_spec.settings.clone();

if settings.transport != TransportMode::WebRTC {
if settings.transport != OwnOrDefault::Own(TransportMode::WebRTC) {
self.log_message(
"warn",
format!(
"Cloudflare backend only supports WebRTC transport; normalizing configured {} transport to WebRTC",
settings.transport
),
);
settings.transport = TransportMode::WebRTC;
settings.transport = OwnOrDefault::Own(TransportMode::WebRTC);
}

match settings.noise_suppression {
Expand Down Expand Up @@ -525,7 +526,7 @@ fn map_settings(settings: &crate::participant::shared::ParticipantSettings) -> t
}
},
screenshare_enabled: settings.screenshare_enabled,
transport: match settings.transport {
transport: match settings.transport.own().unwrap_or(TransportMode::WebTransport) {
client_simulator_config::TransportMode::WebRTC => types::ParticipantSettingsTransport::Webrtc,
client_simulator_config::TransportMode::WebTransport => types::ParticipantSettingsTransport::Webtransport,
},
Expand Down Expand Up @@ -1489,7 +1490,7 @@ mod tests {
]);
let (base_url, requests, server) = spawn_http_server(responses).await;
let mut spec = launch_spec(ResolvedFrontendKind::HyperLite, &format!("{base_url}/room/demo"));
spec.settings.transport = TransportMode::WebTransport;
spec.settings.transport = OwnOrDefault::Own(TransportMode::WebTransport);
let mut session = CloudflareSession::new_for_test(
spec,
launch_options(false, FakeMedia::None),
Expand Down Expand Up @@ -1764,7 +1765,7 @@ mod tests {
screenshare_enabled: false,
auto_gain_control: OwnOrDefault::Own(true),
noise_suppression: NoiseSuppression::Disabled,
transport: TransportMode::WebRTC,
transport: OwnOrDefault::Own(TransportMode::WebRTC),
video_constraint_publish_webcam: OwnOrDefault::Own(VideoConstraint::P720),
video_constraint_subscribe: OwnOrDefault::Default,
video_max_concurrent_tracks: OwnOrDefault::Default,
Expand Down
82 changes: 68 additions & 14 deletions browser/src/participant/frontend/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::driver::BrowserDriver;
use client_simulator_config::{
NoiseSuppression,
OwnOrDefault,
TransportMode,
VideoConstraint,
VideoMaxConcurrentTracks,
};
Expand All @@ -12,6 +13,7 @@ use eyre::{
};
use serde::{
de::DeserializeOwned,
Deserialize,
Serialize,
};
use serde_json::json;
Expand All @@ -29,8 +31,6 @@ const PARTICIPANT_EDIT: &str = "hyper.settings.client.edit('participant', argume
const VOLUME_NORMALIZATION_ON: &str = "const { report, edit } = hyper.settings.client; \
const levels = report.participant.audioSendVolumeNormalization ?? report.default.audioSendVolumeNormalization; \
edit('participant', { set: { audioSendVolumeNormalization: levels } });";
const FORCE_WEBRTC_GET: &str = "return hyper.settings.sessionDebug.forceWebrtc;";
const FORCE_WEBRTC_SET: &str = "hyper.settings.sessionDebug.actions.setForceWebrtc(arguments[0]);";

// The `SettingKey`s of the settings that the simulator reads and sets.
const NOISE_SUPPRESSION: &str = "audioSendNoiseSuppression";
Expand All @@ -39,16 +39,27 @@ const BACKGROUND_BLUR: &str = "videoSendWebcamBackgroundBlur";
const WEBCAM_MAX_RESOLUTION_LEVEL: &str = "videoSendWebcamMaxResolutionLevel";
const RECEIVE_MAX_RESOLUTION_LEVEL: &str = "videoReceiveMaxResolutionLevel";
const MAX_CONCURRENT_TRACKS: &str = "videoSendMaxConcurrentTracks";

async fn get_bool(driver: &dyn BrowserDriver, js: &str) -> Result<bool> {
let value = driver.eval(js, None).await?;
serde_json::from_value(value).context("failed to read bool from eval result")
const TRANSPORT: &str = "transport";

/// A value of hyper.video's `transport` setting, its `TransportPreference`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum TransportPreference {
/// hyper.video's default, which connects over WebTransport where the browser has it, and over WebRTC elsewhere.
Automatic,
/// WebTransport only, with no fallback to WebRTC.
WebTransport,
/// WebRTC data channels.
WebRtc,
}

async fn set_value<S: serde::Serialize>(driver: &dyn BrowserDriver, js: &str, value: S) -> Result<()> {
let arg = serde_json::to_value(value).context("failed to serialize eval argument")?;
driver.eval(js, Some(arg)).await?;
Ok(())
impl From<TransportMode> for TransportPreference {
fn from(mode: TransportMode) -> Self {
match mode {
TransportMode::WebTransport => Self::WebTransport,
TransportMode::WebRTC => Self::WebRtc,
}
}
}

/// Reads the value of the setting `key` that applies.
Expand Down Expand Up @@ -174,12 +185,20 @@ pub(super) async fn set_video_max_concurrent_tracks(
.await
}

pub(super) async fn get_force_webrtc(driver: &dyn BrowserDriver) -> Result<bool> {
get_bool(driver, FORCE_WEBRTC_GET).await
/// The transport that applies, reading [`TransportPreference::Automatic`] as WebTransport, which it picks where the
/// browser has it.
pub(super) async fn get_transport(driver: &dyn BrowserDriver) -> Result<TransportMode> {
let preference: TransportPreference = get_setting(driver, TRANSPORT).await?;
Ok(match preference {
TransportPreference::Automatic | TransportPreference::WebTransport => TransportMode::WebTransport,
TransportPreference::WebRtc => TransportMode::WebRTC,
})
}

pub(super) async fn set_force_webrtc(driver: &dyn BrowserDriver, value: bool) -> Result<()> {
set_value(driver, FORCE_WEBRTC_SET, value).await
/// Sets the participant's transport, or unsets it for [`OwnOrDefault::Default`], so that an admin's value for the
/// session applies, else [`TransportPreference::Automatic`].
pub(super) async fn set_transport(driver: &dyn BrowserDriver, value: OwnOrDefault<TransportMode>) -> Result<()> {
edit_setting(driver, TRANSPORT, value.map(TransportPreference::from)).await
}

#[cfg(test)]
Expand Down Expand Up @@ -373,4 +392,39 @@ pub(in crate::participant::frontend) mod tests {

assert_eq!(value, VideoMaxConcurrentTracks::Limit(2));
}

#[tokio::test]
async fn transport_maps_to_and_from_hyper_video_preferences() {
let driver = RecordingDriver::default();

set_transport(&driver, OwnOrDefault::Own(TransportMode::WebRTC))
.await
.unwrap();
set_transport(&driver, OwnOrDefault::Own(TransportMode::WebTransport))
.await
.unwrap();
set_transport(&driver, OwnOrDefault::Default).await.unwrap();

assert_eq!(
driver.calls(),
vec![
participant_edit(json!({ "transport": "webRtc" })),
participant_edit(json!({ "transport": "webTransport" })),
participant_unset("transport"),
],
);

let transports = [
("webRtc", TransportMode::WebRTC),
("webTransport", TransportMode::WebTransport),
("automatic", TransportMode::WebTransport),
];
for (setting, expected) in transports {
let driver = RecordingDriver::with_result(json!(setting));
assert_eq!(get_transport(&driver).await.unwrap(), expected);
}
assert!(get_transport(&RecordingDriver::with_result(json!("webSocket")))
.await
.is_err());
}
}
16 changes: 5 additions & 11 deletions browser/src/participant/frontend/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ use super::{
commands::{
get_auto_gain_control,
get_background_blur,
get_force_webrtc,
get_noise_suppression,
get_transport,
get_video_constraint_publish_webcam,
get_video_constraint_subscribe,
get_video_max_concurrent_tracks,
set_auto_gain_control,
set_background_blur,
set_force_webrtc,
set_noise_suppression,
set_transport,
set_video_constraint_publish_webcam,
set_video_constraint_subscribe,
set_video_max_concurrent_tracks,
Expand All @@ -32,7 +32,6 @@ use crate::auth::BorrowedCredentials;
use client_simulator_config::{
NoiseSuppression,
OwnOrDefault,
TransportMode,
VideoConstraint,
VideoMaxConcurrentTracks,
};
Expand Down Expand Up @@ -162,10 +161,7 @@ impl ParticipantInner {
"max concurrent video tracks",
set_video_max_concurrent_tracks(driver, settings.video_max_concurrent_tracks).await,
);
self.log_failure(
"transport mode",
set_force_webrtc(driver, settings.transport == TransportMode::WebRTC).await,
);
self.log_failure("transport mode", set_transport(driver, settings.transport).await);

if !settings.audio_enabled {
self.log_failure("muted microphone", self.toggle_audio_inner().await);
Expand Down Expand Up @@ -318,10 +314,8 @@ impl ParticipantInner {
}
}

if let Ok(value) = get_force_webrtc(driver).await {
if value {
state.transport_mode = TransportMode::WebRTC;
}
if let Ok(value) = get_transport(driver).await {
state.transport_mode = value;
}

if let Ok(value) = get_video_constraint_publish_webcam(driver).await {
Expand Down
10 changes: 8 additions & 2 deletions browser/src/participant/remote_stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::participant::shared::{
ParticipantState,
};
use client_simulator_config::{
TransportMode,
VideoConstraint,
VideoMaxConcurrentTracks,
};
Expand Down Expand Up @@ -48,7 +49,7 @@ impl ParticipantDriverSession for RemoteStubSession {
fn start(&mut self) -> BoxFuture<'_, Result<()>> {
async move {
// The stub applies none of hyper.video's settings, so a setting that the participant leaves to hyper.video
// reports hyper.video's default, and the track limit no limit.
// reports hyper.video's default, WebTransport for the transport, and no limit for the track limit.
self.state = ParticipantState {
username: self.launch_spec.username.clone(),
running: true,
Expand All @@ -58,7 +59,12 @@ impl ParticipantDriverSession for RemoteStubSession {
video_activated: self.launch_spec.settings.video_enabled,
auto_gain_control: self.launch_spec.settings.auto_gain_control.own().unwrap_or(true),
noise_suppression: self.launch_spec.settings.noise_suppression,
transport_mode: self.launch_spec.settings.transport,
transport_mode: self
.launch_spec
.settings
.transport
.own()
.unwrap_or(TransportMode::WebTransport),
video_constraint_publish_webcam: self
.launch_spec
.settings
Expand Down
6 changes: 3 additions & 3 deletions browser/src/participant/shared/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(in crate::participant) struct ParticipantSettings {
pub(in crate::participant) screenshare_enabled: bool,
pub(in crate::participant) auto_gain_control: OwnOrDefault<bool>,
pub(in crate::participant) noise_suppression: NoiseSuppression,
pub(in crate::participant) transport: TransportMode,
pub(in crate::participant) transport: OwnOrDefault<TransportMode>,
pub(in crate::participant) video_constraint_publish_webcam: OwnOrDefault<VideoConstraint>,
pub(in crate::participant) video_constraint_subscribe: OwnOrDefault<VideoConstraint>,
pub(in crate::participant) video_max_concurrent_tracks: OwnOrDefault<VideoMaxConcurrentTracks>,
Expand Down Expand Up @@ -112,7 +112,7 @@ mod tests {
screenshare_enabled: true,
auto_gain_control: OwnOrDefault::Own(true),
noise_suppression: NoiseSuppression::HyperVoice,
transport: TransportMode::WebRTC,
transport: OwnOrDefault::Own(TransportMode::WebRTC),
video_constraint_publish_webcam: OwnOrDefault::Own(VideoConstraint::P480),
video_constraint_subscribe: OwnOrDefault::Default,
video_max_concurrent_tracks: OwnOrDefault::Own(VideoMaxConcurrentTracks::Limit(2)),
Expand All @@ -131,7 +131,7 @@ mod tests {
assert!(spec.settings.screenshare_enabled);
assert_eq!(spec.settings.auto_gain_control, OwnOrDefault::Own(true));
assert_eq!(spec.settings.noise_suppression, NoiseSuppression::HyperVoice);
assert_eq!(spec.settings.transport, TransportMode::WebRTC);
assert_eq!(spec.settings.transport, OwnOrDefault::Own(TransportMode::WebRTC));
assert_eq!(
spec.settings.video_constraint_publish_webcam,
OwnOrDefault::Own(VideoConstraint::P480)
Expand Down
2 changes: 1 addition & 1 deletion config/src/default-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ audio_enabled: true
video_enabled: true
auto_gain_control: default
noise_suppression: none
transport: webtransport
transport: default
video_constraint_publish_webcam: default
video_constraint_subscribe: default
video_max_concurrent_tracks: default
Expand Down
5 changes: 4 additions & 1 deletion config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub struct Config {
#[serde(default)]
pub noise_suppression: NoiseSuppression,
#[serde(default)]
pub transport: TransportMode,
pub transport: OwnOrDefault<TransportMode>,
#[serde(default)]
pub video_constraint_publish_webcam: OwnOrDefault<VideoConstraint>,
#[serde(default)]
Expand Down Expand Up @@ -542,6 +542,7 @@ video_max_concurrent_tracks: 2
.add_source(config::File::from_str(
r#"
auto_gain_control: true
transport: webrtc
video_constraint_publish_webcam: none
video_constraint_subscribe: default
video_max_concurrent_tracks: unlimited
Expand Down Expand Up @@ -570,6 +571,7 @@ blur: false

for config in [from_file, from_source, from_saved] {
assert_eq!(config.auto_gain_control, OwnOrDefault::Own(true));
assert_eq!(config.transport, OwnOrDefault::Own(TransportMode::WebRTC));
assert_eq!(
config.video_constraint_publish_webcam,
OwnOrDefault::Own(VideoConstraint::None)
Expand All @@ -590,6 +592,7 @@ blur: false
let config = Config::default();

assert_eq!(config.auto_gain_control, OwnOrDefault::Default);
assert_eq!(config.transport, OwnOrDefault::Default);
assert_eq!(config.video_constraint_publish_webcam, OwnOrDefault::Default);
assert_eq!(config.video_constraint_subscribe, OwnOrDefault::Default);
assert_eq!(config.video_max_concurrent_tracks, OwnOrDefault::Default);
Expand Down
2 changes: 1 addition & 1 deletion docs/browser-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ The current implementation has two frontend-specific automation layers:
For Hyper Core parity, the driver needs hooks for:

- join button, leave button, mute button, video button, screen share button, name input selectors, or equivalent control channels; see [`browser/src/participant/local/selectors.rs`](../browser/src/participant/local/selectors.rs) `:1-20`
- JS getters/setters for noise suppression, blur, camera resolution, and force-WebRTC transport; see [`browser/src/participant/local/commands.rs`](../browser/src/participant/local/commands.rs) `:80-124`
- JS getters/setters for noise suppression, blur, camera resolution, and transport; see [`browser/src/participant/local/commands.rs`](../browser/src/participant/local/commands.rs) `:80-124`
- state scraping for mute/video/screenshare button state and advanced media settings; see [`browser/src/participant/local/core.rs`](../browser/src/participant/local/core.rs) `:271-317`

For Hyper Lite parity, the driver needs the join/leave/mute/video/screenshare controls and state scraping, but the advanced settings can remain unsupported no-ops as they are today. See [`browser/src/participant/local/lite.rs`](../browser/src/participant/local/lite.rs) `:119-175`, `:193-223`.
Expand Down
Loading
Loading