mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
feat(voice): ship the screen share delivery rework to everyone (#2931)
This commit is contained in:
@@ -10524,7 +10524,6 @@
|
||||
},
|
||||
"gateway_rollout": {"$ref": "#/components/schemas/GatewayRolloutConfigResponse"},
|
||||
"voice_noise_suppression": {"$ref": "#/components/schemas/VoiceNoiseSuppressionConfigResponse"},
|
||||
"screen_share_delivery": {"$ref": "#/components/schemas/ScreenShareDeliveryConfigResponse"},
|
||||
"push_service_delivery": {"$ref": "#/components/schemas/PushServiceDeliveryConfigResponse"},
|
||||
"experiment_delivery": {"$ref": "#/components/schemas/ExperimentDeliveryConfigResponse"},
|
||||
"registration": {
|
||||
@@ -10953,7 +10952,6 @@
|
||||
"sso",
|
||||
"gateway_rollout",
|
||||
"voice_noise_suppression",
|
||||
"screen_share_delivery",
|
||||
"push_service_delivery",
|
||||
"experiment_delivery",
|
||||
"registration",
|
||||
@@ -11089,10 +11087,6 @@
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/VoiceNoiseSuppressionConfigUpdateRequest"}]
|
||||
},
|
||||
"screen_share_delivery": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/ScreenShareDeliveryConfigUpdateRequest"}]
|
||||
},
|
||||
"push_service_delivery": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/PushServiceDeliveryConfigUpdateRequest"}]
|
||||
@@ -15208,24 +15202,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ScreenShareDeliveryConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"VoiceNoiseSuppressionConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15327,36 +15303,6 @@
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ScreenShareDeliveryConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "screen-share-delivery-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VoiceNoiseSuppressionConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -23,8 +23,6 @@ pub struct InstanceConfigResponse {
|
||||
#[serde(default)]
|
||||
pub voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
#[serde(default)]
|
||||
pub screen_share_delivery: ScreenShareDeliveryConfigResponse,
|
||||
#[serde(default)]
|
||||
pub push_service_delivery: PushServiceDeliveryConfigResponse,
|
||||
#[serde(default)]
|
||||
pub experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
@@ -452,7 +450,6 @@ impl VoiceE2eeScope {
|
||||
|
||||
pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000;
|
||||
pub const PUSH_SERVICE_DELIVERY_DEFAULT_SALT: &str = "push-service-delivery-v1";
|
||||
pub const SCREEN_SHARE_DELIVERY_DEFAULT_SALT: &str = "screen-share-delivery-v1";
|
||||
pub const VOICE_NS_MAX_GUILD_OVERRIDES: usize = 200;
|
||||
|
||||
impl NoiseSuppressionBackend {
|
||||
@@ -543,44 +540,6 @@ pub struct VoiceNoiseSuppressionConfigUpdateRequest {
|
||||
pub suppression_strength: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ScreenShareDeliveryConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ScreenShareDeliveryConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: SCREEN_SHARE_DELIVERY_DEFAULT_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct ScreenShareDeliveryConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct PushServiceDeliveryConfigResponse {
|
||||
@@ -735,8 +694,6 @@ pub struct InstanceConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice_noise_suppression: Option<VoiceNoiseSuppressionConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub screen_share_delivery: Option<ScreenShareDeliveryConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub push_service_delivery: Option<PushServiceDeliveryConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
|
||||
@@ -1076,30 +1033,18 @@ mod tests {
|
||||
.expect("admin schema");
|
||||
let noise = serde_json::from_value::<VoiceNoiseSuppressionConfigResponse>(json!({}))
|
||||
.expect("default noise config");
|
||||
let screen_share = serde_json::from_value::<ScreenShareDeliveryConfigResponse>(json!({}))
|
||||
.expect("default screen share config");
|
||||
let delivery = serde_json::from_value::<ExperimentDeliveryConfigResponse>(json!({}))
|
||||
.expect("default delivery config");
|
||||
let noise = serde_json::to_value(noise).expect("serializable noise config");
|
||||
let screen_share =
|
||||
serde_json::to_value(screen_share).expect("serializable screen share config");
|
||||
let delivery = serde_json::to_value(delivery).expect("serializable delivery config");
|
||||
let generated_noise: generated_types::VoiceNoiseSuppressionConfigResponse =
|
||||
serde_json::from_value(noise.clone()).expect("generated noise config contract");
|
||||
let generated_screen_share: generated_types::ScreenShareDeliveryConfigResponse =
|
||||
serde_json::from_value(screen_share.clone())
|
||||
.expect("generated screen share config contract");
|
||||
let generated_delivery: generated_types::ExperimentDeliveryConfigResponse =
|
||||
serde_json::from_value(delivery.clone()).expect("generated delivery config contract");
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_noise).expect("serializable generated noise config"),
|
||||
noise
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_screen_share)
|
||||
.expect("serializable generated screen share config"),
|
||||
screen_share
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_delivery)
|
||||
.expect("serializable generated delivery config"),
|
||||
@@ -1107,7 +1052,6 @@ mod tests {
|
||||
);
|
||||
for (name, value) in [
|
||||
("VoiceNoiseSuppressionConfigResponse", noise),
|
||||
("ScreenShareDeliveryConfigResponse", screen_share),
|
||||
("ExperimentDeliveryConfigResponse", delivery),
|
||||
] {
|
||||
for (field, value) in value.as_object().expect("config object") {
|
||||
@@ -1119,29 +1063,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_share_delivery_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = ScreenShareDeliveryConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::ScreenShareDeliveryConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(ScreenShareDeliveryConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_suppression_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = VoiceNoiseSuppressionConfigUpdateRequest {
|
||||
|
||||
@@ -19,9 +19,8 @@ use crate::{
|
||||
InstanceServicesUpdateRequest, InstanceYoutubeIntegrationUpdateRequest,
|
||||
LimitConfigUpdateRequest, LimitRule, LimitRuleFilters, NoiseSuppressionBackend,
|
||||
PremiumMode, PushServiceDeliveryConfigUpdateRequest, RegistrationMode,
|
||||
ScreenShareDeliveryConfigUpdateRequest, SsoConfigUpdateRequest,
|
||||
VOICE_NS_MAX_GUILD_OVERRIDES, VoiceE2eeScope, VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
VoiceNoiseSuppressionGuildOverride,
|
||||
SsoConfigUpdateRequest, VOICE_NS_MAX_GUILD_OVERRIDES, VoiceE2eeScope,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest, VoiceNoiseSuppressionGuildOverride,
|
||||
},
|
||||
},
|
||||
config::AdminConfig,
|
||||
@@ -208,10 +207,6 @@ pub async fn instance_config_post(
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_screen_share_delivery" => match build_screen_share_delivery_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_push_service_delivery" => match build_push_service_delivery_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
@@ -653,38 +648,6 @@ fn build_voice_noise_suppression_update(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_screen_share_delivery_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
screen_share_delivery: Some(ScreenShareDeliveryConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("screen_share_delivery_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"screen_share_delivery_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(
|
||||
form,
|
||||
"screen_share_delivery_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("screen_share_delivery_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("screen_share_delivery_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_push_service_delivery_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
@@ -1638,83 +1601,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_screen_share_delivery_update_reads_the_rollout_fields() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"screen_share_delivery_enabled=true&screen_share_delivery_rollout_basis_points=%20250%20&screen_share_delivery_rollout_salt=%20screen-share-delivery-v2%20&screen_share_delivery_included_user_ids=1500000000000000001%0A1500000000000000002&screen_share_delivery_excluded_user_ids=1500000000000000003%2C%201500000000000000004",
|
||||
);
|
||||
let update = build_screen_share_delivery_update(&form)
|
||||
.expect("valid form")
|
||||
.screen_share_delivery
|
||||
.expect("screen share delivery update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.rollout_basis_points, Some(250));
|
||||
assert_eq!(
|
||||
update.rollout_salt,
|
||||
Some("screen-share-delivery-v2".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000001".to_owned(),
|
||||
"1500000000000000002".to_owned()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000003".to_owned(),
|
||||
"1500000000000000004".to_owned()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_screen_share_delivery_update_leaves_the_feature_inert_when_nothing_is_submitted() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
let request = build_screen_share_delivery_update(&form).expect("valid form");
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).expect("serializable update"),
|
||||
serde_json::json!({"screen_share_delivery": {
|
||||
"enabled": false,
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_screen_share_delivery_update_rejects_invalid_rollout_fields() {
|
||||
for (form, message) in [
|
||||
(
|
||||
"screen_share_delivery_rollout_basis_points=10001",
|
||||
"Rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_rollout_basis_points=abc",
|
||||
"Rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_rollout_salt=%20%20",
|
||||
"Rollout salt must be between 1 and 64 characters",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_included_user_ids=123%2Cinvalid",
|
||||
"Included user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_excluded_user_ids=123%2Cinvalid",
|
||||
"Excluded user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
] {
|
||||
let form = MultiValueForm::parse(form.as_bytes());
|
||||
assert_eq!(
|
||||
build_screen_share_delivery_update(&form).expect_err("invalid rollout field"),
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
|
||||
@@ -7,8 +7,7 @@ use crate::{
|
||||
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
|
||||
LimitConfigResponse, NoiseSuppressionBackend, PUSH_SERVICE_DELIVERY_DEFAULT_SALT,
|
||||
PendingRegistrationResponse, PushServiceDeliveryConfigResponse, RegistrationUrlResponse,
|
||||
SCREEN_SHARE_DELIVERY_DEFAULT_SALT, ScreenShareDeliveryConfigResponse, SsoConfigResponse,
|
||||
VOICE_NS_MAX_GUILD_OVERRIDES, VoiceNoiseSuppressionConfigResponse,
|
||||
SsoConfigResponse, VOICE_NS_MAX_GUILD_OVERRIDES, VoiceNoiseSuppressionConfigResponse,
|
||||
},
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
@@ -149,7 +148,6 @@ pub fn instance_config_page(
|
||||
html! {
|
||||
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
|
||||
(voice_noise_suppression_section(base, csrf_token, &instance_config.voice_noise_suppression))
|
||||
(screen_share_delivery_section(base, csrf_token, &instance_config.screen_share_delivery))
|
||||
(push_service_delivery_section(base, csrf_token, &instance_config.push_service_delivery))
|
||||
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
|
||||
@if let Some(limit_config) = limit_config {
|
||||
@@ -1179,117 +1177,6 @@ fn voice_noise_suppression_section(
|
||||
)
|
||||
}
|
||||
|
||||
fn screen_share_delivery_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
screen_share_delivery: &ScreenShareDeliveryConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if screen_share_delivery.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = screen_share_delivery.included_user_ids.join("\n");
|
||||
let excluded_user_ids = screen_share_delivery.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Screen Share Delivery",
|
||||
"Pick how many clients publish screen shares through the reworked delivery path. While \
|
||||
the master switch below is off nothing on this form reaches any client: every user \
|
||||
keeps the screen share pipeline they have today, whatever the rest of these fields say. \
|
||||
A client that is already sharing keeps the path it started on until the share ends.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_screen_share_delivery"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (screen_share_delivery.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"screen_share_delivery_enabled",
|
||||
"true",
|
||||
"Serve screen share delivery assignments to clients",
|
||||
screen_share_delivery.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
feature is inert and keeps its current behavior, so the rollout and \
|
||||
targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"screen_share_delivery_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&screen_share_delivery.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"screen_share_delivery_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&screen_share_delivery.rollout_salt,
|
||||
SCREEN_SHARE_DELIVERY_DEFAULT_SALT,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"screen_share_delivery_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
screen_share_delivery.included_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"screen_share_delivery_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
screen_share_delivery.excluded_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage. This is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Screen Share Delivery Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn push_service_delivery_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
@@ -2042,25 +1929,6 @@ mod tests {
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_share_delivery_section_shows_list_counts_and_the_master_switch() {
|
||||
let screen_share_delivery = ScreenShareDeliveryConfigResponse {
|
||||
included_user_ids: vec!["1500000000000000001".to_owned()],
|
||||
excluded_user_ids: vec![
|
||||
"1500000000000000002".to_owned(),
|
||||
"1500000000000000003".to_owned(),
|
||||
],
|
||||
..ScreenShareDeliveryConfigResponse::default()
|
||||
};
|
||||
let markup =
|
||||
screen_share_delivery_section("/admin", "csrf", &screen_share_delivery).into_string();
|
||||
assert!(markup.contains("action=update_screen_share_delivery"));
|
||||
assert!(markup.contains("screen_share_delivery_enabled"));
|
||||
assert!(markup.contains("1 of 1000 stored"));
|
||||
assert!(markup.contains("2 of 1000 stored"));
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voice_noise_suppression_section_flags_a_list_at_its_cap() {
|
||||
let voice_noise_suppression = VoiceNoiseSuppressionConfigResponse {
|
||||
|
||||
@@ -409,15 +409,6 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
"future_object_knob": {"nested": true},
|
||||
"future_list_knob": ["a", "b"]
|
||||
},
|
||||
"screen_share_delivery": {
|
||||
"enabled": true,
|
||||
"config_version": 2,
|
||||
"rollout_basis_points": 2500,
|
||||
"rollout_salt": "screen-share-delivery-v1",
|
||||
"included_user_ids": ["1500000000000000001"],
|
||||
"future_delivery_knob": 9,
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"push_service_delivery": {
|
||||
"enabled": true,
|
||||
"config_version": 3,
|
||||
@@ -555,14 +546,6 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
assert_eq!(resp.voice_noise_suppression.rollout_basis_points, 10000);
|
||||
assert_eq!(*resp.voice_noise_suppression.rollout_salt, "voice-ns-v1");
|
||||
assert_eq!(resp.voice_noise_suppression.enabled_backends.len(), 3);
|
||||
assert!(resp.screen_share_delivery.enabled);
|
||||
assert_eq!(resp.screen_share_delivery.config_version, 2);
|
||||
assert_eq!(resp.screen_share_delivery.rollout_basis_points, 2500);
|
||||
assert_eq!(
|
||||
*resp.screen_share_delivery.rollout_salt,
|
||||
"screen-share-delivery-v1"
|
||||
);
|
||||
assert_eq!(resp.screen_share_delivery.included_user_ids.len(), 1);
|
||||
assert_eq!(resp.experiment_delivery.poll_interval_seconds, 300);
|
||||
assert!(resp.policy.single_community_guild_id.is_none());
|
||||
assert_eq!(resp.policy.services.gif_enabled, Some(true));
|
||||
@@ -573,7 +556,6 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
.replace("\"future_rollout_knob\": 3,", "")
|
||||
.replace("\"future_presentation_knob\": \"verbose\",", "")
|
||||
.replace("\"future_knob\": 7,", "")
|
||||
.replace("\"future_delivery_knob\": 9,", "")
|
||||
.replace("\"future_object_knob\": {\"nested\": true},", "")
|
||||
.replace("\"future_list_knob\": [\"a\", \"b\"],", "")
|
||||
.replace(
|
||||
|
||||
@@ -465,7 +465,6 @@ async fn mutating_admin_pages_render_usable_csrf_tokens() {
|
||||
"/instance-config?action=update_gateway_rollout",
|
||||
"/instance-config?action=update_sso",
|
||||
"/instance-config?action=update_voice_noise_suppression",
|
||||
"/instance-config?action=update_screen_share_delivery",
|
||||
"/instance-config?action=update_experiment_delivery",
|
||||
][..],
|
||||
),
|
||||
@@ -1200,14 +1199,6 @@ fn instance_config() -> Value {
|
||||
"guild_overrides": [],
|
||||
"suppression_strength": 80
|
||||
},
|
||||
"screen_share_delivery": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "screen-share-delivery-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"experiment_delivery": {
|
||||
"poll_interval_seconds": 300,
|
||||
"poll_jitter_percent": 15
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {GatewayRolloutConfigSchema} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {PushServiceDeliveryConfigSchema} from '@fluxer/schema/src/domains/admin/PushServiceDeliverySchemas';
|
||||
import {ScreenShareDeliveryConfigSchema} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {VoiceNoiseSuppressionConfigSchema} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {ExperimentDeliveryConfigSchema} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
@@ -65,7 +64,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
ssoConfig,
|
||||
gatewayRollout,
|
||||
voiceNoiseSuppression,
|
||||
screenShareDelivery,
|
||||
pushServiceDelivery,
|
||||
experimentDelivery,
|
||||
registrationConfig,
|
||||
@@ -75,7 +73,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
instanceConfigRepository.getSsoConfig(),
|
||||
instanceConfigRepository.getGatewayRolloutConfig(),
|
||||
instanceConfigRepository.getVoiceNoiseSuppressionConfig(),
|
||||
instanceConfigRepository.getScreenShareDeliveryConfig(),
|
||||
instanceConfigRepository.getPushServiceDeliveryConfig(),
|
||||
instanceConfigRepository.getExperimentDeliveryConfig(),
|
||||
instanceConfigRepository.getRegistrationConfig(),
|
||||
@@ -108,7 +105,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
},
|
||||
gateway_rollout: gatewayRollout,
|
||||
voice_noise_suppression: voiceNoiseSuppression,
|
||||
screen_share_delivery: screenShareDelivery,
|
||||
push_service_delivery: pushServiceDelivery,
|
||||
experiment_delivery: experimentDelivery,
|
||||
registration: {
|
||||
@@ -273,18 +269,6 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.screen_share_delivery) {
|
||||
const patch = omitUndefinedFields(data.screen_share_delivery);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await instanceConfigRepository.updateScreenShareDeliveryConfig((current) =>
|
||||
ScreenShareDeliveryConfigSchema.parse({
|
||||
...current,
|
||||
...patch,
|
||||
config_version: current.config_version + 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.push_service_delivery) {
|
||||
const patch = omitUndefinedFields(data.push_service_delivery);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {RateLimitConfigs} from '@app/api/RateLimitConfig';
|
||||
import type {HonoApp} from '@app/api/types/HonoEnv';
|
||||
import {entityTagMatches} from '@app/api/utils/EntityTag';
|
||||
import {Headers as HttpHeaders} from '@fluxer/constants/src/Headers';
|
||||
import {resolveScreenShareDeliveryAssignment} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {resolveVoiceNoiseSuppressionAssignment} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {ExperimentAssignmentsResponse} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
|
||||
@@ -29,10 +28,9 @@ export function ExperimentController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const instanceConfigRepository = ctx.get('instanceConfigRepository');
|
||||
const [delivery, voiceConfig, screenShareConfig] = await Promise.all([
|
||||
const [delivery, voiceConfig] = await Promise.all([
|
||||
instanceConfigRepository.getExperimentDeliveryConfig(),
|
||||
instanceConfigRepository.getVoiceNoiseSuppressionConfig(),
|
||||
instanceConfigRepository.getScreenShareDeliveryConfig(),
|
||||
]);
|
||||
const userId = ctx.get('user').id.toString();
|
||||
const body: ExperimentAssignmentsResponse = {
|
||||
@@ -40,7 +38,6 @@ export function ExperimentController(app: HonoApp) {
|
||||
poll_jitter_percent: delivery.poll_jitter_percent,
|
||||
assignments: {
|
||||
voice_noise_suppression: resolveVoiceNoiseSuppressionAssignment(voiceConfig, userId),
|
||||
screen_share_delivery: resolveScreenShareDeliveryAssignment(screenShareConfig, userId),
|
||||
},
|
||||
};
|
||||
const etag = `"${createHash('sha256').update(JSON.stringify(body)).digest('hex')}"`;
|
||||
|
||||
@@ -6,10 +6,6 @@ import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHa
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {
|
||||
DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT,
|
||||
} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {
|
||||
DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
@@ -19,7 +15,6 @@ import {
|
||||
DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT,
|
||||
type ExperimentAssignmentsResponse,
|
||||
type ExperimentDeliveryConfigResponse,
|
||||
readScreenShareDeliveryAssignment,
|
||||
readVoiceNoiseSuppressionAssignment,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
@@ -56,7 +51,6 @@ describe('GET /experiments', () => {
|
||||
poll_jitter_percent: DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT,
|
||||
assignments: {
|
||||
voice_noise_suppression: INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
screen_share_delivery: INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -88,52 +82,6 @@ describe('GET /experiments', () => {
|
||||
expect(readVoiceNoiseSuppressionAssignment(body).enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('populates the screen share assignment key even when the rollout is disabled', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
const body = await createBuilder<ExperimentAssignmentsResponse>(harness, account.token).get(ENDPOINT).execute();
|
||||
|
||||
expect(Object.hasOwn(body.assignments, 'screen_share_delivery')).toBe(true);
|
||||
expect(readScreenShareDeliveryAssignment(body).enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves the screen share caller through the allowlist', async () => {
|
||||
const targeted = await createTestAccount(harness);
|
||||
const untargeted = await createTestAccount(harness);
|
||||
await getInstanceConfigRepository().setScreenShareDeliveryConfig({
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 4,
|
||||
rollout_basis_points: 0,
|
||||
included_user_ids: [targeted.userId],
|
||||
});
|
||||
|
||||
const targetedBody = await createBuilder<ExperimentAssignmentsResponse>(harness, targeted.token)
|
||||
.get(ENDPOINT)
|
||||
.execute();
|
||||
expect(targetedBody.assignments.screen_share_delivery).toEqual({enabled: true});
|
||||
|
||||
const untargetedBody = await createBuilder<ExperimentAssignmentsResponse>(harness, untargeted.token)
|
||||
.get(ENDPOINT)
|
||||
.execute();
|
||||
expect(untargetedBody.assignments.screen_share_delivery).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
it('keeps the screen share exclusion ahead of a full rollout', async () => {
|
||||
const excluded = await createTestAccount(harness);
|
||||
await getInstanceConfigRepository().setScreenShareDeliveryConfig({
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
enabled: true,
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [excluded.userId],
|
||||
excluded_user_ids: [excluded.userId],
|
||||
});
|
||||
|
||||
const body = await createBuilder<ExperimentAssignmentsResponse>(harness, excluded.token).get(ENDPOINT).execute();
|
||||
|
||||
expect(body.assignments.screen_share_delivery).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
it('serves the delivery cadence from the delivery config and not from the voice config', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await getInstanceConfigRepository().setExperimentDeliveryConfig({
|
||||
@@ -248,30 +196,6 @@ describe('GET /experiments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('serves a fresh body once the screen share config changes', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
const first = await createBuilder<ExperimentAssignmentsResponse>(harness, account.token)
|
||||
.get(ENDPOINT)
|
||||
.executeWithResponse();
|
||||
const staleEtag = first.response.headers.get('etag') as string;
|
||||
|
||||
await getInstanceConfigRepository().setScreenShareDeliveryConfig({
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 1,
|
||||
rollout_basis_points: 10000,
|
||||
});
|
||||
|
||||
const refreshed = await createBuilder<ExperimentAssignmentsResponse>(harness, account.token)
|
||||
.get(ENDPOINT)
|
||||
.header('If-None-Match', staleEtag)
|
||||
.executeWithResponse();
|
||||
expect(refreshed.response.status).toBe(HTTP_STATUS.OK);
|
||||
expect(refreshed.response.headers.get('etag')).not.toBe(staleEtag);
|
||||
expect(refreshed.json?.assignments.screen_share_delivery).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
it('serves a fresh body once the delivery config changes', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
@@ -328,44 +252,6 @@ describe('GET /experiments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('bumps the screen share config version on every admin update without the client sending one', async () => {
|
||||
const admin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_VIEW,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
]);
|
||||
|
||||
const afterFirst = await createBuilder<{screen_share_delivery: {config_version: number; enabled: boolean}}>(
|
||||
harness,
|
||||
admin.token,
|
||||
)
|
||||
.patch('/admin/instance/config')
|
||||
.body({screen_share_delivery: {enabled: true, rollout_basis_points: 10000}})
|
||||
.execute();
|
||||
expect(afterFirst.screen_share_delivery).toMatchObject({config_version: 1, enabled: true});
|
||||
|
||||
const afterSecond = await createBuilder<{screen_share_delivery: {config_version: number; enabled: boolean}}>(
|
||||
harness,
|
||||
admin.token,
|
||||
)
|
||||
.patch('/admin/instance/config')
|
||||
.body({screen_share_delivery: {rollout_salt: 'screen-share-delivery-v2'}})
|
||||
.execute();
|
||||
expect(afterSecond.screen_share_delivery).toMatchObject({config_version: 2, enabled: true});
|
||||
|
||||
const afterEmpty = await createBuilder<{screen_share_delivery: {config_version: number; enabled: boolean}}>(
|
||||
harness,
|
||||
admin.token,
|
||||
)
|
||||
.patch('/admin/instance/config')
|
||||
.body({screen_share_delivery: {}})
|
||||
.execute();
|
||||
expect(afterEmpty.screen_share_delivery).toMatchObject({config_version: 2, enabled: true});
|
||||
|
||||
const body = await createBuilder<ExperimentAssignmentsResponse>(harness, admin.token).get(ENDPOINT).execute();
|
||||
expect(body.assignments.screen_share_delivery).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
it('leaves the config version alone for an admin update that sets no field', async () => {
|
||||
const admin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
|
||||
@@ -18,10 +18,6 @@ import {InstanceConfigWriteRaceExecutor} from '@app/api/instance/tests/InstanceC
|
||||
import {startDockerContainer} from '@app/api/test/DockerTestContainer';
|
||||
import {InMemoryCassandraQueryExecutor} from '@app/api/test/InMemoryCassandraQueryExecutor';
|
||||
import {MockKVProvider} from '@app/api/test/mocks/MockKVProvider';
|
||||
import {
|
||||
DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
type ScreenShareDeliveryConfig,
|
||||
} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {
|
||||
DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
|
||||
type VoiceNoiseSuppressionConfig,
|
||||
@@ -39,7 +35,6 @@ import {
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const VOICE_NOISE_SUPPRESSION_CONFIG_KEY = 'voice_noise_suppression_config';
|
||||
const SCREEN_SHARE_DELIVERY_CONFIG_KEY = 'screen_share_delivery_config';
|
||||
const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config';
|
||||
const APP_PUBLIC_CONFIG_KEY = 'app_public_config';
|
||||
const INSTANCE_POLICY_CONFIG_KEY = 'instance_policy_config';
|
||||
@@ -356,70 +351,6 @@ describe('InstanceConfigRepository', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the default screen share delivery config when the key is absent', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await expect(repository.getScreenShareDeliveryConfig()).resolves.toEqual(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{name: 'unparseable text', stored: 'not-json'},
|
||||
{name: 'a json array', stored: '[]'},
|
||||
{name: 'out-of-range values', stored: '{"rollout_basis_points":99999}'},
|
||||
{name: 'a non-boolean enabled flag', stored: '{"enabled":"yes"}'},
|
||||
])('falls back to the default screen share delivery config for $name', async ({stored}) => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await repository.setConfig(SCREEN_SHARE_DELIVERY_CONFIG_KEY, stored);
|
||||
|
||||
await expect(repository.getScreenShareDeliveryConfig()).resolves.toEqual(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG);
|
||||
});
|
||||
|
||||
it('round-trips a stored screen share delivery config', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
const config: ScreenShareDeliveryConfig = {
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 5,
|
||||
rollout_basis_points: 2500,
|
||||
rollout_salt: 'screen-share-delivery-v2',
|
||||
included_user_ids: ['1400000000000000001'],
|
||||
excluded_user_ids: ['1400000000000000002'],
|
||||
};
|
||||
await repository.setScreenShareDeliveryConfig(config);
|
||||
|
||||
await expect(repository.getScreenShareDeliveryConfig()).resolves.toEqual(config);
|
||||
});
|
||||
|
||||
it('fills newly added screen share delivery fields from the schema defaults', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const repository = createRepository(kvProvider);
|
||||
|
||||
await repository.setConfig(
|
||||
SCREEN_SHARE_DELIVERY_CONFIG_KEY,
|
||||
JSON.stringify({enabled: true, config_version: 2, rollout_basis_points: 1000}),
|
||||
);
|
||||
|
||||
await expect(repository.getScreenShareDeliveryConfig()).resolves.toEqual({
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 2,
|
||||
rollout_basis_points: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the default experiment delivery config when the key is absent', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
@@ -492,26 +423,6 @@ describe('InstanceConfigRepository', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes a refresh so another repository observes the screen share delivery config', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
const kvProvider = new MockKVProvider();
|
||||
const reader = createRepository(kvProvider);
|
||||
const writer = createRepository(kvProvider);
|
||||
|
||||
await expect(reader.getScreenShareDeliveryConfig()).resolves.toEqual(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG);
|
||||
|
||||
await writer.setScreenShareDeliveryConfig({
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
enabled: true,
|
||||
config_version: 1,
|
||||
});
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
expect(await reader.getScreenShareDeliveryConfig()).toMatchObject({enabled: true, config_version: 1});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the registration URL id as the admin-visible registration code', async () => {
|
||||
const executor = new CountingInMemoryCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
|
||||
@@ -36,10 +36,6 @@ import {
|
||||
type PushServiceDeliveryConfig,
|
||||
PushServiceDeliveryConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/PushServiceDeliverySchemas';
|
||||
import {
|
||||
type ScreenShareDeliveryConfig,
|
||||
ScreenShareDeliveryConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {
|
||||
type VoiceNoiseSuppressionConfig,
|
||||
VoiceNoiseSuppressionConfigSchema,
|
||||
@@ -66,7 +62,6 @@ import {z} from 'zod';
|
||||
|
||||
const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config';
|
||||
const VOICE_NOISE_SUPPRESSION_CONFIG_KEY = 'voice_noise_suppression_config';
|
||||
const SCREEN_SHARE_DELIVERY_CONFIG_KEY = 'screen_share_delivery_config';
|
||||
const PUSH_SERVICE_DELIVERY_CONFIG_KEY = 'push_service_delivery_config';
|
||||
const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config';
|
||||
const REGISTRATION_CONFIG_KEY = 'registration_config';
|
||||
@@ -374,7 +369,6 @@ type StoredConfigSection =
|
||||
| 'app public'
|
||||
| 'gateway rollout'
|
||||
| 'voice noise suppression'
|
||||
| 'screen share delivery'
|
||||
| 'push service delivery'
|
||||
| 'experiment delivery'
|
||||
| 'instance policy'
|
||||
@@ -514,10 +508,6 @@ function parseStoredVoiceNoiseSuppressionConfig(raw: string | null): VoiceNoiseS
|
||||
return parseStoredConfigOrDefault(VoiceNoiseSuppressionConfigSchema, raw, 'voice noise suppression');
|
||||
}
|
||||
|
||||
function parseStoredScreenShareDeliveryConfig(raw: string | null): ScreenShareDeliveryConfig {
|
||||
return parseStoredConfigOrDefault(ScreenShareDeliveryConfigSchema, raw, 'screen share delivery');
|
||||
}
|
||||
|
||||
function parseStoredPushServiceDeliveryConfig(raw: string | null): PushServiceDeliveryConfig {
|
||||
return parseStoredConfigOrDefault(PushServiceDeliveryConfigSchema, raw, 'push service delivery');
|
||||
}
|
||||
@@ -1169,7 +1159,6 @@ export class InstanceConfigRepository {
|
||||
parseStoredGatewayRolloutConfig(snapshot.get(GATEWAY_ROLLOUT_CONFIG_KEY) ?? null),
|
||||
);
|
||||
parseStoredVoiceNoiseSuppressionConfig(snapshot.get(VOICE_NOISE_SUPPRESSION_CONFIG_KEY) ?? null);
|
||||
parseStoredScreenShareDeliveryConfig(snapshot.get(SCREEN_SHARE_DELIVERY_CONFIG_KEY) ?? null);
|
||||
parseStoredPushServiceDeliveryConfig(snapshot.get(PUSH_SERVICE_DELIVERY_CONFIG_KEY) ?? null);
|
||||
parseStoredExperimentDeliveryConfig(snapshot.get(EXPERIMENT_DELIVERY_CONFIG_KEY) ?? null);
|
||||
const policy = parseStoredInstancePolicyConfig(snapshot.get(INSTANCE_POLICY_CONFIG_KEY) ?? null);
|
||||
@@ -1267,27 +1256,6 @@ export class InstanceConfigRepository {
|
||||
);
|
||||
}
|
||||
|
||||
async getScreenShareDeliveryConfig(): Promise<ScreenShareDeliveryConfig> {
|
||||
const raw = await this.getConfig(SCREEN_SHARE_DELIVERY_CONFIG_KEY);
|
||||
return parseStoredScreenShareDeliveryConfig(raw);
|
||||
}
|
||||
|
||||
async setScreenShareDeliveryConfig(config: ScreenShareDeliveryConfig): Promise<void> {
|
||||
await this.updateScreenShareDeliveryConfig(() => config);
|
||||
}
|
||||
|
||||
updateScreenShareDeliveryConfig(
|
||||
update: (current: ScreenShareDeliveryConfig) => ScreenShareDeliveryConfig,
|
||||
): Promise<ScreenShareDeliveryConfig> {
|
||||
return this.updateStoredConfig(SCREEN_SHARE_DELIVERY_CONFIG_KEY, (raw) =>
|
||||
validateStoredConfig(
|
||||
ScreenShareDeliveryConfigSchema,
|
||||
update(parseStoredScreenShareDeliveryConfig(raw)),
|
||||
'screen share delivery',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async getPushServiceDeliveryConfig(): Promise<PushServiceDeliveryConfig> {
|
||||
const raw = await this.getConfig(PUSH_SERVICE_DELIVERY_CONFIG_KEY);
|
||||
return parseStoredPushServiceDeliveryConfig(raw);
|
||||
|
||||
@@ -27190,8 +27190,7 @@
|
||||
"assignments": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"voice_noise_suppression": {"$ref": "#/components/schemas/VoiceNoiseSuppressionAssignmentResponse"},
|
||||
"screen_share_delivery": {"$ref": "#/components/schemas/ScreenShareDeliveryAssignmentResponse"}
|
||||
"voice_noise_suppression": {"$ref": "#/components/schemas/VoiceNoiseSuppressionAssignmentResponse"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -30695,12 +30694,6 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DonationCurrency": {"type": "string", "enum": ["usd", "eur", "brl", "inr", "pln", "try", "sek", "dkk", "nok"]},
|
||||
"ScreenShareDeliveryAssignmentResponse": {
|
||||
"type": "object",
|
||||
"properties": {"enabled": {"type": "boolean"}},
|
||||
"required": ["enabled"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VoiceNoiseSuppressionAssignmentResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -52,7 +52,6 @@ export interface InternalRoomOptions {
|
||||
|
||||
singlePeerConnection: boolean;
|
||||
subscriberVideoCodecExclusions?: Array<VideoCodec>;
|
||||
screenShareDelivery?: boolean;
|
||||
h264HardwareProfiles?: ReadonlySet<string>;
|
||||
dataStream?: RoomDataStreamOptions;
|
||||
}
|
||||
|
||||
@@ -73,66 +73,34 @@ describe('applyVideoStartBitrate', () => {
|
||||
}
|
||||
|
||||
it('adds a start bitrate to a non-SVC codec section', () => {
|
||||
for (const screenShareDelivery of [false, true]) {
|
||||
const media = videoMedia('camera-track', [
|
||||
{payload: 96, config: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'},
|
||||
]);
|
||||
expect(applyVideoStartBitrate(media, 'camera-track', 'H264', 1000, false, screenShareDelivery)).toBe(96);
|
||||
expect(applyVideoStartBitrate(media, 'camera-track', 'H264', 1000, false)).toBe(96);
|
||||
expect(media.fmtp[0]?.config).toBe(
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f;x-google-start-bitrate=900',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('caps camera start bitrates but not screen share start bitrates while screen share delivery is off', () => {
|
||||
const camera = videoMedia('camera-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
applyVideoStartBitrate(camera, 'camera-track', 'H264', 3000);
|
||||
expect(camera.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=1000');
|
||||
|
||||
const screen = videoMedia('screen-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
applyVideoStartBitrate(screen, 'screen-track', 'H264', 6000, true);
|
||||
expect(screen.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=5400');
|
||||
});
|
||||
|
||||
it('caps camera and screen share start bitrates at their own ceilings', () => {
|
||||
const camera = videoMedia('camera-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
applyVideoStartBitrate(camera, 'camera-track', 'H264', 3000, false, true);
|
||||
applyVideoStartBitrate(camera, 'camera-track', 'H264', 3000, false);
|
||||
expect(camera.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=1000');
|
||||
|
||||
const screen = videoMedia('screen-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
applyVideoStartBitrate(screen, 'screen-track', 'H264', 6000, true, true);
|
||||
applyVideoStartBitrate(screen, 'screen-track', 'H264', 6000, true);
|
||||
expect(screen.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=1500');
|
||||
});
|
||||
|
||||
it('leaves a small screen share start bitrate on the floor while screen share delivery is off', () => {
|
||||
const screen = videoMedia('screen-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
applyVideoStartBitrate(screen, 'screen-track', 'H264', 300, true);
|
||||
expect(screen.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=270');
|
||||
});
|
||||
|
||||
it('keeps a screen share start bitrate above the frame dropper cliff', () => {
|
||||
const screen = videoMedia('screen-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
applyVideoStartBitrate(screen, 'screen-track', 'H264', 300, true, true);
|
||||
applyVideoStartBitrate(screen, 'screen-track', 'H264', 300, true);
|
||||
expect(screen.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=600');
|
||||
});
|
||||
|
||||
it('stamps only the lead payload type while screen share delivery is off', () => {
|
||||
const media = multiPayloadScreenMedia();
|
||||
expect(applyVideoStartBitrate(media, 'screen-track', 'H264', 6000, true)).toBe(116);
|
||||
expect(media.fmtp.find((fmtp) => fmtp.payload === 116)?.config).toBe(
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f;x-google-start-bitrate=5400',
|
||||
);
|
||||
expect(media.fmtp.find((fmtp) => fmtp.payload === 102)?.config).toBe(
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f',
|
||||
);
|
||||
expect(media.fmtp.find((fmtp) => fmtp.payload === 108)?.config).toBe(
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
||||
);
|
||||
});
|
||||
|
||||
it('stamps every payload type the codec is offered under, not only the lead one', () => {
|
||||
const media = multiPayloadScreenMedia();
|
||||
expect(applyVideoStartBitrate(media, 'screen-track', 'H264', 6000, true, true)).toBe(116);
|
||||
expect(applyVideoStartBitrate(media, 'screen-track', 'H264', 6000, true)).toBe(116);
|
||||
for (const fmtp of media.fmtp) {
|
||||
expect(fmtp.config).toContain('x-google-start-bitrate=1500');
|
||||
}
|
||||
@@ -140,7 +108,6 @@ describe('applyVideoStartBitrate', () => {
|
||||
});
|
||||
|
||||
it('only touches the fmtp line for the matching payload', () => {
|
||||
for (const screenShareDelivery of [false, true]) {
|
||||
const media = videoMedia(
|
||||
'screen-track',
|
||||
[
|
||||
@@ -152,35 +119,24 @@ describe('applyVideoStartBitrate', () => {
|
||||
{payload: 98, codec: 'VP9'},
|
||||
],
|
||||
);
|
||||
applyVideoStartBitrate(media, 'screen-track', 'VP9', 1500, true, screenShareDelivery);
|
||||
applyVideoStartBitrate(media, 'screen-track', 'VP9', 1500, true);
|
||||
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f');
|
||||
expect(media.fmtp[1]?.config).toBe('profile-id=0;x-google-start-bitrate=1350');
|
||||
}
|
||||
});
|
||||
|
||||
it('never appends a second start bitrate while screen share delivery is off', () => {
|
||||
const media = videoMedia('camera-track', [
|
||||
{payload: 96, config: 'profile-level-id=42e01f;x-google-start-bitrate=900'},
|
||||
]);
|
||||
applyVideoStartBitrate(media, 'camera-track', 'H264', 2000);
|
||||
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=900');
|
||||
});
|
||||
|
||||
it('replaces a start bitrate an earlier offer wrote instead of keeping it', () => {
|
||||
const media = videoMedia('camera-track', [
|
||||
{payload: 96, config: 'profile-level-id=42e01f;x-google-start-bitrate=900'},
|
||||
]);
|
||||
applyVideoStartBitrate(media, 'camera-track', 'H264', 2000, false, true);
|
||||
applyVideoStartBitrate(media, 'camera-track', 'H264', 2000, false);
|
||||
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=1000');
|
||||
});
|
||||
|
||||
it('leaves other tracks and missing codecs alone', () => {
|
||||
for (const screenShareDelivery of [false, true]) {
|
||||
const media = videoMedia('camera-track', [{payload: 96, config: 'profile-level-id=42e01f'}]);
|
||||
expect(applyVideoStartBitrate(media, 'other-track', 'H264', 2000, false, screenShareDelivery)).toBeUndefined();
|
||||
expect(applyVideoStartBitrate(media, 'camera-track', 'AV1', 2000, false, screenShareDelivery)).toBe(0);
|
||||
expect(applyVideoStartBitrate(media, 'other-track', 'H264', 2000, false)).toBeUndefined();
|
||||
expect(applyVideoStartBitrate(media, 'camera-track', 'AV1', 2000, false)).toBe(0);
|
||||
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ export function applyVideoStartBitrate(
|
||||
codec: string,
|
||||
maxbr: number,
|
||||
isScreenShare = false,
|
||||
screenShareDelivery = false,
|
||||
): number | undefined {
|
||||
if (!media.msid?.includes(cid)) {
|
||||
return undefined;
|
||||
@@ -65,25 +64,10 @@ export function applyVideoStartBitrate(
|
||||
const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
|
||||
let startBitrate = Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
||||
if (isScreenShare) {
|
||||
startBitrate = screenShareDelivery
|
||||
? Math.max(minScreenShareStartBitrateKbps, Math.min(calculatedStartBitrate, maxScreenShareStartBitrateKbps))
|
||||
: calculatedStartBitrate;
|
||||
}
|
||||
|
||||
if (!screenShareDelivery) {
|
||||
const codecPayload = codecPayloads[0];
|
||||
const fmtp = media.fmtp.find((entry) => entry.payload === codecPayload);
|
||||
if (fmtp) {
|
||||
if (!fmtp.config.includes(startBitrateParameter)) {
|
||||
fmtp.config += `;${startBitrateParameter}=${startBitrate}`;
|
||||
}
|
||||
} else {
|
||||
media.fmtp.push({
|
||||
payload: codecPayload,
|
||||
config: `${startBitrateParameter}=${startBitrate}`,
|
||||
});
|
||||
}
|
||||
return codecPayload;
|
||||
startBitrate = Math.max(
|
||||
minScreenShareStartBitrateKbps,
|
||||
Math.min(calculatedStartBitrate, maxScreenShareStartBitrateKbps),
|
||||
);
|
||||
}
|
||||
|
||||
for (const payload of codecPayloads) {
|
||||
@@ -143,8 +127,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
||||
|
||||
excludedVideoDecoderMimeTypes: Set<string> = new Set();
|
||||
|
||||
private screenShareDelivery: boolean;
|
||||
|
||||
onOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
|
||||
|
||||
onIceCandidate?: (candidate: RTCIceCandidate) => void;
|
||||
@@ -161,10 +143,9 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
||||
|
||||
onTrack?: (ev: RTCTrackEvent) => void;
|
||||
|
||||
constructor(config?: RTCConfiguration, loggerOptions: LoggerOptions = {}, screenShareDelivery: boolean = false) {
|
||||
constructor(config?: RTCConfiguration, loggerOptions: LoggerOptions = {}) {
|
||||
super();
|
||||
this.loggerOptions = loggerOptions;
|
||||
this.screenShareDelivery = screenShareDelivery;
|
||||
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCTransport, () => this.logContext);
|
||||
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
||||
this.config = config;
|
||||
@@ -412,7 +393,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
||||
trackbr.codec,
|
||||
trackbr.maxbr,
|
||||
trackbr.isScreenShare,
|
||||
this.screenShareDelivery,
|
||||
);
|
||||
if (codecPayload === undefined) {
|
||||
return false;
|
||||
@@ -471,10 +451,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
||||
for (const transceiver of this.getTransceivers()) {
|
||||
if (transceiver.receiver.track?.kind !== 'video') continue;
|
||||
if ((transceiver as {stopped?: boolean}).stopped) continue;
|
||||
const receives = this.screenShareDelivery
|
||||
? transceiver.direction === 'recvonly'
|
||||
: transceiver.direction === 'recvonly' || transceiver.direction === 'sendrecv';
|
||||
if (!receives) continue;
|
||||
if (transceiver.direction !== 'recvonly') continue;
|
||||
if (typeof transceiver.setCodecPreferences !== 'function') continue;
|
||||
try {
|
||||
transceiver.setCodecPreferences(allowed);
|
||||
|
||||
@@ -98,7 +98,6 @@ export class PCTransportManager {
|
||||
loggerOptions: LoggerOptions,
|
||||
rtcConfig?: RTCConfiguration,
|
||||
subscriberVideoCodecExclusions?: Array<VideoCodec>,
|
||||
screenShareDelivery: boolean = false,
|
||||
) {
|
||||
this.loggerOptions = loggerOptions;
|
||||
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager, () => this.logContext);
|
||||
@@ -106,10 +105,10 @@ export class PCTransportManager {
|
||||
|
||||
this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
|
||||
this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
|
||||
this.publisher = new PCTransport(rtcConfig, loggerOptions, screenShareDelivery);
|
||||
this.publisher = new PCTransport(rtcConfig, loggerOptions);
|
||||
this._mode = mode;
|
||||
if (mode !== 'publisher-only') {
|
||||
this.subscriber = new PCTransport(rtcConfig, loggerOptions, screenShareDelivery);
|
||||
this.subscriber = new PCTransport(rtcConfig, loggerOptions);
|
||||
this.subscriber.onConnectionStateChange = this.updateState;
|
||||
this.subscriber.onIceConnectionStateChange = this.updateState;
|
||||
this.subscriber.onSignalingStatechange = this.updateState;
|
||||
@@ -127,7 +126,7 @@ export class PCTransportManager {
|
||||
};
|
||||
}
|
||||
|
||||
const receivingTransport = screenShareDelivery ? (this.subscriber ?? this.publisher) : this.subscriber;
|
||||
const receivingTransport = this.subscriber ?? this.publisher;
|
||||
if (receivingTransport) {
|
||||
for (const codec of subscriberVideoCodecExclusions ?? []) {
|
||||
receivingTransport.excludedVideoDecoderMimeTypes.add(`video/${codec}`);
|
||||
|
||||
@@ -25,41 +25,20 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
const mainLine = 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f';
|
||||
const highLine = 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f';
|
||||
|
||||
it('keeps Main and Baseline ahead of Constrained Baseline while screen share delivery is off', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const baseline = codec('video/H264', baselineLine);
|
||||
const highProfile = codec('video/H264', highLine);
|
||||
const rtx = codec('video/rtx');
|
||||
const preferences = selectPublisherCodecPreferences('h264', [constrainedBaseline, rtx, baseline, highProfile]);
|
||||
expect(preferences).toEqual([highProfile, baseline, constrainedBaseline, rtx]);
|
||||
});
|
||||
|
||||
it('offers High first, then the one profile this server always registers, then the ones it registers nowhere', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const baseline = codec('video/H264', baselineLine);
|
||||
const highProfile = codec('video/H264', highLine);
|
||||
const rtx = codec('video/rtx');
|
||||
const preferences = selectPublisherCodecPreferences(
|
||||
'h264',
|
||||
[constrainedBaseline, rtx, baseline, highProfile],
|
||||
true,
|
||||
);
|
||||
const preferences = selectPublisherCodecPreferences('h264', [constrainedBaseline, rtx, baseline, highProfile]);
|
||||
expect(preferences).toEqual([highProfile, constrainedBaseline, baseline, rtx]);
|
||||
});
|
||||
|
||||
it('leads with Main and then Baseline while screen share delivery is off', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const mainProfile = codec('video/H264', mainLine);
|
||||
const baseline = codec('video/H264', baselineLine);
|
||||
const preferences = selectPublisherCodecPreferences('h264', [mainProfile, baseline, constrainedBaseline]);
|
||||
expect(preferences).toEqual([mainProfile, baseline, constrainedBaseline]);
|
||||
});
|
||||
|
||||
it('never leads with Main or Baseline, which this server registers nowhere and deletes from the answer', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const mainProfile = codec('video/H264', mainLine);
|
||||
const baseline = codec('video/H264', baselineLine);
|
||||
const preferences = selectPublisherCodecPreferences('h264', [mainProfile, baseline, constrainedBaseline], true);
|
||||
const preferences = selectPublisherCodecPreferences('h264', [mainProfile, baseline, constrainedBaseline]);
|
||||
expect(preferences).toEqual([constrainedBaseline, mainProfile, baseline]);
|
||||
});
|
||||
|
||||
@@ -75,21 +54,8 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
];
|
||||
}
|
||||
|
||||
it('sorts the capabilities Chromium reports by the old table while screen share delivery is off', () => {
|
||||
const preferences = selectPublisherCodecPreferences('h264', chromiumCapabilities());
|
||||
expect(preferences.map((entry) => entry.sdpFmtpLine)).toEqual([
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640034',
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f',
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f',
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
||||
'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f',
|
||||
'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f',
|
||||
'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f',
|
||||
]);
|
||||
});
|
||||
|
||||
it('offers High first out of the capabilities Chromium reports, so the only hardware profile this server registers wins', () => {
|
||||
const preferences = selectPublisherCodecPreferences('h264', chromiumCapabilities(), true);
|
||||
const preferences = selectPublisherCodecPreferences('h264', chromiumCapabilities());
|
||||
expect(preferences.map((entry) => entry.sdpFmtpLine)).toEqual([
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640034',
|
||||
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
||||
@@ -116,7 +82,7 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
return {constrainedBaseline, mainProfile, highProfileLevel31, highProfileLevel51, constrainedHigh};
|
||||
}
|
||||
|
||||
it('ranks High, Constrained High, Main and Baseline above Constrained Baseline while screen share delivery is off', () => {
|
||||
it('ranks High and Constrained High above Constrained Baseline, whatever level each one reports', () => {
|
||||
const {constrainedBaseline, mainProfile, highProfileLevel31, highProfileLevel51, constrainedHigh} = levelSpread();
|
||||
const preferences = selectPublisherCodecPreferences('h264', [
|
||||
mainProfile,
|
||||
@@ -125,22 +91,6 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
constrainedHigh,
|
||||
constrainedBaseline,
|
||||
]);
|
||||
expect(preferences).toEqual([
|
||||
highProfileLevel31,
|
||||
highProfileLevel51,
|
||||
constrainedHigh,
|
||||
mainProfile,
|
||||
constrainedBaseline,
|
||||
]);
|
||||
});
|
||||
|
||||
it('ranks High and Constrained High above Constrained Baseline, whatever level each one reports', () => {
|
||||
const {constrainedBaseline, mainProfile, highProfileLevel31, highProfileLevel51, constrainedHigh} = levelSpread();
|
||||
const preferences = selectPublisherCodecPreferences(
|
||||
'h264',
|
||||
[mainProfile, highProfileLevel31, highProfileLevel51, constrainedHigh, constrainedBaseline],
|
||||
true,
|
||||
);
|
||||
expect(preferences).toEqual([
|
||||
highProfileLevel31,
|
||||
highProfileLevel51,
|
||||
@@ -150,7 +100,7 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('ranks packetization-mode=1 above packetization-mode=0 in both arms, which no hardware encoder takes', () => {
|
||||
it('ranks packetization-mode=1 above packetization-mode=0, which no hardware encoder takes', () => {
|
||||
const constrainedBaselineMode0 = codec(
|
||||
'video/H264',
|
||||
'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f',
|
||||
@@ -167,7 +117,6 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
const capabilities = [highProfileMode0, constrainedBaselineMode0, constrainedBaselineMode1, highProfileMode1];
|
||||
const expected = [highProfileMode1, constrainedBaselineMode1, highProfileMode0, constrainedBaselineMode0];
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities)).toEqual(expected);
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, true)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('puts the chosen codec first and keeps every other codec in browser capability order', () => {
|
||||
@@ -175,7 +124,6 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
const vp8 = codec('video/VP8');
|
||||
const rtx = codec('video/rtx');
|
||||
expect(selectPublisherCodecPreferences('vp9', [vp8, rtx, vp9])).toEqual([vp9, vp8, rtx]);
|
||||
expect(selectPublisherCodecPreferences('vp9', [vp8, rtx, vp9], true)).toEqual([vp9, vp8, rtx]);
|
||||
});
|
||||
|
||||
it('keeps the other codecs so a later publication on the same connection can negotiate them', () => {
|
||||
@@ -192,7 +140,7 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const capabilities = [vp8, highProfile, constrainedBaseline];
|
||||
expect(selectPublisherCodecPreferences('vp8', capabilities)).toEqual([vp8, highProfile, constrainedBaseline]);
|
||||
expect(selectPublisherCodecPreferences('vp8', capabilities, true, new Set(['42e0']))).toEqual([
|
||||
expect(selectPublisherCodecPreferences('vp8', capabilities, new Set(['42e0']))).toEqual([
|
||||
vp8,
|
||||
constrainedBaseline,
|
||||
highProfile,
|
||||
@@ -203,25 +151,15 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
expect(selectPublisherCodecPreferences('av1', [codec('video/VP8'), codec('video/rtx')])).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores the profiles this host measured while screen share delivery is off', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const highProfile = codec('video/H264', highLine);
|
||||
const capabilities = [highProfile, constrainedBaseline];
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, false, new Set(['42e0']))).toEqual([
|
||||
highProfile,
|
||||
constrainedBaseline,
|
||||
]);
|
||||
});
|
||||
|
||||
it('only lets a profile this host encodes in hardware outrank Constrained Baseline', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const highProfile = codec('video/H264', highLine);
|
||||
const capabilities = [highProfile, constrainedBaseline];
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, true, new Set(['42e0']))).toEqual([
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, new Set(['42e0']))).toEqual([
|
||||
constrainedBaseline,
|
||||
highProfile,
|
||||
]);
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, true, new Set(['6400', '42e0']))).toEqual([
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, new Set(['6400', '42e0']))).toEqual([
|
||||
highProfile,
|
||||
constrainedBaseline,
|
||||
]);
|
||||
@@ -231,8 +169,8 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
const constrainedBaseline = codec('video/H264', constrainedBaselineLine);
|
||||
const highProfile = codec('video/H264', highLine);
|
||||
const capabilities = [constrainedBaseline, highProfile];
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, true)).toEqual([highProfile, constrainedBaseline]);
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, true, new Set())).toEqual([
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities)).toEqual([highProfile, constrainedBaseline]);
|
||||
expect(selectPublisherCodecPreferences('h264', capabilities, new Set())).toEqual([
|
||||
highProfile,
|
||||
constrainedBaseline,
|
||||
]);
|
||||
@@ -245,7 +183,7 @@ describe('selectPublisherCodecPreferences', () => {
|
||||
'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=64001f',
|
||||
);
|
||||
expect(
|
||||
selectPublisherCodecPreferences('h264', [highProfileMode0, constrainedBaselineMode1], true, new Set(['6400'])),
|
||||
selectPublisherCodecPreferences('h264', [highProfileMode0, constrainedBaselineMode1], new Set(['6400'])),
|
||||
).toEqual([constrainedBaselineMode1, highProfileMode0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,13 +123,6 @@ const videoCodecMimeTypes: Record<VideoCodec, Array<string>> = {
|
||||
vp8: ['video/vp8'],
|
||||
};
|
||||
const h264ProfileRanks = new Map([
|
||||
['6400', 0],
|
||||
['640c', 1],
|
||||
['4d00', 2],
|
||||
['4200', 3],
|
||||
['42e0', 4],
|
||||
]);
|
||||
const h264DeliveryProfileRanks = new Map([
|
||||
['6400', 0],
|
||||
['640c', 1],
|
||||
['42e0', 2],
|
||||
@@ -139,8 +132,7 @@ const h264DeliveryProfileRanks = new Map([
|
||||
const h264UnrankedProfileScore = 5;
|
||||
const h264MissingProfileScore = 6;
|
||||
const h264NonHardwareProfilePenalty = 8;
|
||||
const h264PacketizationMode0Score = 10;
|
||||
const h264DeliveryPacketizationMode0Score = 20;
|
||||
const h264PacketizationMode0Score = 20;
|
||||
type RtpCodecCapability = RTCRtpCapabilities['codecs'][number] & {sdpFmtpLine?: string};
|
||||
|
||||
enum PCState {
|
||||
@@ -523,7 +515,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
||||
this.loggerOptions,
|
||||
rtcConfig,
|
||||
this.options.subscriberVideoCodecExclusions,
|
||||
this.options.screenShareDelivery ?? false,
|
||||
);
|
||||
} else {
|
||||
this.participantSid = joinResponse.participant?.sid;
|
||||
@@ -537,7 +528,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
||||
this.loggerOptions,
|
||||
rtcConfig,
|
||||
this.options.subscriberVideoCodecExclusions,
|
||||
this.options.screenShareDelivery ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1062,12 +1052,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
||||
if (typeof RTCRtpSender === 'undefined' || typeof RTCRtpSender.getCapabilities !== 'function') return;
|
||||
const capabilities = RTCRtpSender.getCapabilities('video');
|
||||
if (!capabilities) return;
|
||||
const preferences = selectPublisherCodecPreferences(
|
||||
codec,
|
||||
capabilities.codecs,
|
||||
this.options.screenShareDelivery ?? false,
|
||||
this.options.h264HardwareProfiles,
|
||||
);
|
||||
const preferences = selectPublisherCodecPreferences(codec, capabilities.codecs, this.options.h264HardwareProfiles);
|
||||
if (preferences.length === 0) {
|
||||
this.log.warn('sender cannot encode the requested codec, leaving the browser order in place', {
|
||||
...this.logContext,
|
||||
@@ -1843,33 +1828,27 @@ function getFmtpParameter(sdpFmtpLine: string | undefined, key: string): string
|
||||
|
||||
function getH264PublisherCodecScore(
|
||||
codec: RtpCodecCapability,
|
||||
screenShareDelivery: boolean,
|
||||
hardwareProfiles: ReadonlySet<string> | undefined,
|
||||
): number {
|
||||
const profileLevelId = getFmtpParameter(codec.sdpFmtpLine, 'profile-level-id');
|
||||
const packetizationMode = getFmtpParameter(codec.sdpFmtpLine, 'packetization-mode');
|
||||
const mode0Score = screenShareDelivery ? h264DeliveryPacketizationMode0Score : h264PacketizationMode0Score;
|
||||
const packetizationScore = packetizationMode === '1' ? 0 : mode0Score;
|
||||
const packetizationScore = packetizationMode === '1' ? 0 : h264PacketizationMode0Score;
|
||||
if (!profileLevelId) return packetizationScore + h264MissingProfileScore;
|
||||
const profile = profileLevelId.slice(0, 4);
|
||||
if (!screenShareDelivery) {
|
||||
return packetizationScore + (h264ProfileRanks.get(profile) ?? h264UnrankedProfileScore);
|
||||
}
|
||||
const isSoftwareOnly = hardwareProfiles !== undefined && hardwareProfiles.size > 0 && !hardwareProfiles.has(profile);
|
||||
const hardwareScore = isSoftwareOnly ? h264NonHardwareProfilePenalty : 0;
|
||||
return packetizationScore + hardwareScore + (h264DeliveryProfileRanks.get(profile) ?? h264UnrankedProfileScore);
|
||||
return packetizationScore + hardwareScore + (h264ProfileRanks.get(profile) ?? h264UnrankedProfileScore);
|
||||
}
|
||||
|
||||
function preferHardwareH264Codecs(
|
||||
codecs: ReadonlyArray<RtpCodecCapability>,
|
||||
screenShareDelivery: boolean,
|
||||
hardwareProfiles: ReadonlySet<string> | undefined,
|
||||
): Array<RtpCodecCapability> {
|
||||
return codecs
|
||||
.map((codec, index) => ({
|
||||
codec,
|
||||
index,
|
||||
score: getH264PublisherCodecScore(codec, screenShareDelivery, hardwareProfiles),
|
||||
score: getH264PublisherCodecScore(codec, hardwareProfiles),
|
||||
}))
|
||||
.sort((a, b) => a.score - b.score || a.index - b.index)
|
||||
.map((entry) => entry.codec);
|
||||
@@ -1878,17 +1857,15 @@ function preferHardwareH264Codecs(
|
||||
export function selectPublisherCodecPreferences(
|
||||
codec: VideoCodec,
|
||||
codecs: ReadonlyArray<RtpCodecCapability>,
|
||||
screenShareDelivery: boolean = false,
|
||||
h264HardwareProfiles?: ReadonlySet<string>,
|
||||
): Array<RtpCodecCapability> {
|
||||
const mimeTypes = new Set(videoCodecMimeTypes[codec]);
|
||||
const selected = codecs.filter((entry) => mimeTypes.has(entry.mimeType.toLowerCase()));
|
||||
if (selected.length === 0) return [];
|
||||
const preferred =
|
||||
codec === 'h264' ? preferHardwareH264Codecs(selected, screenShareDelivery, h264HardwareProfiles) : selected;
|
||||
const preferred = codec === 'h264' ? preferHardwareH264Codecs(selected, h264HardwareProfiles) : selected;
|
||||
const isH264 = (entry: RtpCodecCapability): boolean => entry.mimeType.toLowerCase() === 'video/h264';
|
||||
const remaining = codecs.filter((entry) => !mimeTypes.has(entry.mimeType.toLowerCase()));
|
||||
const rankedH264 = preferHardwareH264Codecs(remaining.filter(isH264), screenShareDelivery, h264HardwareProfiles);
|
||||
const rankedH264 = preferHardwareH264Codecs(remaining.filter(isH264), h264HardwareProfiles);
|
||||
let nextH264 = 0;
|
||||
const rest = remaining.map((entry) => (isH264(entry) ? rankedH264[nextH264++] : entry));
|
||||
return [...preferred, ...rest];
|
||||
|
||||
@@ -63,7 +63,6 @@ import {
|
||||
publishDefaults,
|
||||
roomConnectOptionDefaults,
|
||||
roomOptionDefaults,
|
||||
screenShareDeliveryPublishDefaults,
|
||||
videoDefaults,
|
||||
} from './defaults.ts';
|
||||
import {ConnectionError, ConnectionErrorReason, UnexpectedConnectionState, UnsupportedServer} from './errors.ts';
|
||||
@@ -228,7 +227,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
||||
...options?.videoCaptureDefaults,
|
||||
};
|
||||
this.options.publishDefaults = {
|
||||
...(this.options.screenShareDelivery ? screenShareDeliveryPublishDefaults : publishDefaults),
|
||||
...publishDefaults,
|
||||
...options?.publishDefaults,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {AudioPresets, BackupCodecPolicy, ScreenSharePresets, VideoPresets} from
|
||||
|
||||
export const defaultVideoCodec = 'h264';
|
||||
|
||||
export const screenShareDeliveryPublishDefaults: TrackPublishDefaults = {
|
||||
export const publishDefaults: TrackPublishDefaults = {
|
||||
audioPreset: AudioPresets.music,
|
||||
dtx: false,
|
||||
red: true,
|
||||
@@ -21,11 +21,6 @@ export const screenShareDeliveryPublishDefaults: TrackPublishDefaults = {
|
||||
preConnectBuffer: false,
|
||||
} as const;
|
||||
|
||||
export const publishDefaults: TrackPublishDefaults = {
|
||||
...screenShareDeliveryPublishDefaults,
|
||||
degradationPreference: 'maintain-resolution',
|
||||
};
|
||||
|
||||
export const audioDefaults: AudioCaptureOptions = {
|
||||
deviceId: {ideal: 'default'},
|
||||
autoGainControl: true,
|
||||
|
||||
@@ -814,7 +814,6 @@ export default class LocalParticipant extends Participant {
|
||||
...this.roomOptions.publishDefaults,
|
||||
...options,
|
||||
};
|
||||
track.screenShareDelivery = this.roomOptions.screenShareDelivery ?? false;
|
||||
const isStereoInput =
|
||||
('channelCount' in track.mediaStreamTrack.getSettings() &&
|
||||
track.mediaStreamTrack.getSettings().channelCount === 2) ||
|
||||
@@ -1788,7 +1787,7 @@ export default class LocalParticipant extends Participant {
|
||||
return;
|
||||
}
|
||||
let subscribedCodecs = update.subscribedCodecs;
|
||||
if (this.roomOptions.screenShareDelivery && hasSingleRidlessEncoding(pub.videoTrack)) {
|
||||
if (hasSingleRidlessEncoding(pub.videoTrack)) {
|
||||
subscribedCodecs = subscribedCodecs.filter((codec) => codec.qualities.some((quality) => quality.enabled));
|
||||
if (subscribedCodecs.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -41,8 +41,6 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
|
||||
|
||||
codec?: VideoCodec;
|
||||
|
||||
screenShareDelivery: boolean = false;
|
||||
|
||||
get constraints() {
|
||||
return this._constraints;
|
||||
}
|
||||
@@ -563,7 +561,7 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
|
||||
);
|
||||
|
||||
private debouncedTrackMuteHandler = debounce(async () => {
|
||||
if (this.screenShareDelivery && this.source === Track.Source.ScreenShare) {
|
||||
if (this.source === Track.Source.ScreenShare) {
|
||||
this.log.debug('screen share capture went idle, keeping upstream published', this.logContext);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {VoiceNoiseSuppressionAssignmentResponse} from '@fluxer/schema/src/d
|
||||
import {
|
||||
type ExperimentAssignmentsResponse,
|
||||
INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE,
|
||||
readScreenShareDeliveryAssignment,
|
||||
readVoiceNoiseSuppressionAssignment,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
@@ -123,8 +122,6 @@ describe('ExperimentAssignments cold start', () => {
|
||||
await settle();
|
||||
expect(ExperimentAssignments.response).toBe(INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE);
|
||||
expect(ExperimentAssignments.response.assignments.voice_noise_suppression).toBeUndefined();
|
||||
expect(ExperimentAssignments.response.assignments.screen_share_delivery).toBeUndefined();
|
||||
expect(readScreenShareDeliveryAssignment(ExperimentAssignments.response).enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the inert envelope while unauthenticated and retries later', async () => {
|
||||
@@ -170,32 +167,6 @@ describe('ExperimentAssignments response handling', () => {
|
||||
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
|
||||
});
|
||||
|
||||
it('adopts a screen share delivery assignment beside the voice one', async () => {
|
||||
await adopt({
|
||||
...CANARY_ENVELOPE,
|
||||
assignments: {...CANARY_ENVELOPE.assignments, screen_share_delivery: {enabled: true}},
|
||||
});
|
||||
expect(readScreenShareDeliveryAssignment(ExperimentAssignments.response).enabled).toBe(true);
|
||||
expect(readVoiceNoiseSuppressionAssignment(ExperimentAssignments.response)).toEqual(CANARY_ASSIGNMENT);
|
||||
});
|
||||
|
||||
it('reads screen share delivery as disabled when the envelope omits it', async () => {
|
||||
await adopt(CANARY_ENVELOPE);
|
||||
expect(readScreenShareDeliveryAssignment(ExperimentAssignments.response).enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('discards an envelope with a malformed screen share delivery assignment', async () => {
|
||||
await adopt(CANARY_ENVELOPE);
|
||||
vi.mocked(http.get).mockResolvedValue(
|
||||
reply(200, {
|
||||
...CANARY_ENVELOPE,
|
||||
assignments: {...CANARY_ENVELOPE.assignments, screen_share_delivery: {enabled: 'yes'}},
|
||||
}),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(400_000);
|
||||
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
|
||||
});
|
||||
|
||||
it('accepts an envelope that carries no voice noise suppression assignment', async () => {
|
||||
await adopt({poll_interval_seconds: 600, poll_jitter_percent: 0, assignments: {}});
|
||||
expect(ExperimentAssignments.response.assignments.voice_noise_suppression).toBeUndefined();
|
||||
|
||||
+10
-32
@@ -2,25 +2,18 @@
|
||||
|
||||
import {videoElementHasRenderedFrame} from '@app/features/voice/components/VideoElementFrameState';
|
||||
import {Store} from '@app/features/voice/engine/Store';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
|
||||
export const LAST_FRAME_SNAPSHOTS_MAX = 8;
|
||||
export const LAST_FRAME_SNAPSHOT_WIDTH_MAX = 640;
|
||||
export const LAST_FRAME_SNAPSHOT_JPEG_QUALITY = 0.7;
|
||||
|
||||
const LAST_FRAME_SNAPSHOT_LEGACY_WIDTH_MAX = 1280;
|
||||
|
||||
function computeSnapshotDimensions(
|
||||
sourceWidth: number,
|
||||
sourceHeight: number,
|
||||
widthMax: number,
|
||||
): {width: number; height: number} {
|
||||
if (sourceWidth <= widthMax) {
|
||||
function computeSnapshotDimensions(sourceWidth: number, sourceHeight: number): {width: number; height: number} {
|
||||
if (sourceWidth <= LAST_FRAME_SNAPSHOT_WIDTH_MAX) {
|
||||
return {width: sourceWidth, height: sourceHeight};
|
||||
}
|
||||
const scale = widthMax / sourceWidth;
|
||||
const scale = LAST_FRAME_SNAPSHOT_WIDTH_MAX / sourceWidth;
|
||||
return {
|
||||
width: widthMax,
|
||||
width: LAST_FRAME_SNAPSHOT_WIDTH_MAX,
|
||||
height: Math.max(1, Math.round(sourceHeight * scale)),
|
||||
};
|
||||
}
|
||||
@@ -29,12 +22,11 @@ function drawSourceToOffscreenCanvas(
|
||||
source: CanvasImageSource,
|
||||
sourceWidth: number,
|
||||
sourceHeight: number,
|
||||
widthMax: number,
|
||||
): OffscreenCanvas | null {
|
||||
if (typeof OffscreenCanvas === 'undefined') return null;
|
||||
if (sourceWidth <= 0 || sourceHeight <= 0) return null;
|
||||
try {
|
||||
const {width, height} = computeSnapshotDimensions(sourceWidth, sourceHeight, widthMax);
|
||||
const {width, height} = computeSnapshotDimensions(sourceWidth, sourceHeight);
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return null;
|
||||
@@ -45,16 +37,11 @@ function drawSourceToOffscreenCanvas(
|
||||
}
|
||||
}
|
||||
|
||||
function drawSourceToDataUrl(
|
||||
source: CanvasImageSource,
|
||||
sourceWidth: number,
|
||||
sourceHeight: number,
|
||||
widthMax: number,
|
||||
): string | null {
|
||||
function drawSourceToDataUrl(source: CanvasImageSource, sourceWidth: number, sourceHeight: number): string | null {
|
||||
if (typeof document === 'undefined') return null;
|
||||
if (sourceWidth <= 0 || sourceHeight <= 0) return null;
|
||||
try {
|
||||
const {width, height} = computeSnapshotDimensions(sourceWidth, sourceHeight, widthMax);
|
||||
const {width, height} = computeSnapshotDimensions(sourceWidth, sourceHeight);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
@@ -114,21 +101,13 @@ class LastFrameSnapshotCache extends Store {
|
||||
|
||||
captureFromVideoElement(key: string, video: HTMLVideoElement | null): void {
|
||||
if (!key) return;
|
||||
const deliveryEnabled = ScreenShareDeliveryRollout.enabled;
|
||||
if (deliveryEnabled && typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
||||
if (!videoElementHasRenderedFrame(video)) return;
|
||||
const renderedVideo = video as HTMLVideoElement;
|
||||
const widthMax = deliveryEnabled ? LAST_FRAME_SNAPSHOT_WIDTH_MAX : LAST_FRAME_SNAPSHOT_LEGACY_WIDTH_MAX;
|
||||
if (deliveryEnabled) {
|
||||
const captureId = this.nextCaptureId;
|
||||
this.nextCaptureId += 1;
|
||||
this.captureIds.set(key, captureId);
|
||||
const canvas = drawSourceToOffscreenCanvas(
|
||||
renderedVideo,
|
||||
renderedVideo.videoWidth,
|
||||
renderedVideo.videoHeight,
|
||||
widthMax,
|
||||
);
|
||||
const canvas = drawSourceToOffscreenCanvas(renderedVideo, renderedVideo.videoWidth, renderedVideo.videoHeight);
|
||||
if (canvas) {
|
||||
void canvas
|
||||
.convertToBlob({type: 'image/jpeg', quality: LAST_FRAME_SNAPSHOT_JPEG_QUALITY})
|
||||
@@ -144,8 +123,7 @@ class LastFrameSnapshotCache extends Store {
|
||||
return;
|
||||
}
|
||||
this.captureIds.delete(key);
|
||||
}
|
||||
const dataUrl = drawSourceToDataUrl(renderedVideo, renderedVideo.videoWidth, renderedVideo.videoHeight, widthMax);
|
||||
const dataUrl = drawSourceToDataUrl(renderedVideo, renderedVideo.videoWidth, renderedVideo.videoHeight);
|
||||
if (!dataUrl) return;
|
||||
this.retainSnapshot(key, dataUrl);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
getLocalScreenShareVideoPublications,
|
||||
isLiveLocalTrackPublication,
|
||||
} from '@app/features/voice/engine/VoiceTrackPublicationUtils';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {
|
||||
buildScreenShareCodecProfile,
|
||||
@@ -357,7 +356,6 @@ class ScreenShareCodecNegotiation {
|
||||
encoderMode: ScreenShareEncoderMode;
|
||||
order: ReadonlyArray<VideoCodec>;
|
||||
} | null = null;
|
||||
private frozenDelivery: {trackSid: string; enabled: boolean} | null = null;
|
||||
private lastCodecChangeAt = 0;
|
||||
private suppressionTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private publishedTrackSid: string | null = null;
|
||||
@@ -452,22 +450,9 @@ class ScreenShareCodecNegotiation {
|
||||
this.localCodecs = buildLocalCodecAdvertisements(this.resolveCodecPreferenceOrder());
|
||||
}
|
||||
|
||||
private resolveScreenShareDelivery(room: Room | null = this.room): boolean {
|
||||
const trackSid = this.getLocalScreenSharePublication(room)?.trackSid ?? null;
|
||||
if (trackSid === null) {
|
||||
this.frozenDelivery = null;
|
||||
return ScreenShareDeliveryRollout.enabled;
|
||||
}
|
||||
if (this.frozenDelivery?.trackSid === trackSid) return this.frozenDelivery.enabled;
|
||||
const enabled = ScreenShareDeliveryRollout.enabled;
|
||||
this.frozenDelivery = {trackSid, enabled};
|
||||
return enabled;
|
||||
}
|
||||
|
||||
private resolveCodecPreferenceOrder(
|
||||
preference: CodecPreference = VoiceSettings.getPreferredScreenShareCodec(),
|
||||
): ReadonlyArray<VideoCodec> {
|
||||
if (!this.resolveScreenShareDelivery()) return getScreenShareCodecPreferenceOrder(preference);
|
||||
const encoderMode = VoiceSettings.getScreenShareEncoderMode();
|
||||
const frozen = this.frozenCodecPreference;
|
||||
if (
|
||||
@@ -523,7 +508,7 @@ class ScreenShareCodecNegotiation {
|
||||
unknownParticipants: number;
|
||||
} {
|
||||
const now = Date.now();
|
||||
const viewerIdentities = this.resolveScreenShareDelivery(room) ? this.getScreenShareViewerIdentities(room) : null;
|
||||
const viewerIdentities = this.getScreenShareViewerIdentities(room);
|
||||
const knownRemoteCodecs: Array<Array<FluxerCodecAdvertisement>> = [];
|
||||
const participants: Array<{identity: string; firstSeenAt: number}> = [];
|
||||
for (const participant of room?.remoteParticipants.values() ?? []) {
|
||||
@@ -640,7 +625,6 @@ class ScreenShareCodecNegotiation {
|
||||
this.selectedCodec = null;
|
||||
this.localCodecs = [];
|
||||
this.frozenCodecPreference = null;
|
||||
this.frozenDelivery = null;
|
||||
this.lastCodecChangeAt = 0;
|
||||
this.publishedTrackSid = null;
|
||||
this.clearSuppressionTimer();
|
||||
@@ -749,7 +733,6 @@ class ScreenShareCodecNegotiation {
|
||||
const {knownRemoteCodecs, unknownParticipants} = this.getRemoteCodecInputs(room);
|
||||
this.scheduleGraceReevaluations(room, bindingRevision);
|
||||
const previousCodec = this.selectedCodec;
|
||||
const delivery = this.resolveScreenShareDelivery(room);
|
||||
this.negotiationSnapshot = transitionScreenShareCodecNegotiationSnapshot(this.negotiationSnapshot, {
|
||||
type: 'negotiation.evaluate',
|
||||
localCodecs: this.localCodecs,
|
||||
@@ -757,7 +740,7 @@ class ScreenShareCodecNegotiation {
|
||||
unknownParticipants,
|
||||
reason,
|
||||
codecPreference: this.resolveCodecPreferenceOrder(),
|
||||
publishedCodec: delivery ? this.observePublishedScreenShareCodec(room) : null,
|
||||
publishedCodec: this.observePublishedScreenShareCodec(room),
|
||||
});
|
||||
const selection = this.negotiationSnapshot.context.selection;
|
||||
if (!selection) return null;
|
||||
@@ -765,11 +748,7 @@ class ScreenShareCodecNegotiation {
|
||||
this.selectedCodec = selection.codec;
|
||||
return selection;
|
||||
}
|
||||
if (
|
||||
delivery &&
|
||||
previousCodec !== null &&
|
||||
Date.now() - this.lastCodecChangeAt < SCREEN_SHARE_CODEC_CHANGE_SUPPRESSION_MS
|
||||
) {
|
||||
if (previousCodec !== null && Date.now() - this.lastCodecChangeAt < SCREEN_SHARE_CODEC_CHANGE_SUPPRESSION_MS) {
|
||||
logger.debug('Suppressed a screen share codec change inside the change window', {
|
||||
codec: selection.codec,
|
||||
previousCodec,
|
||||
@@ -779,7 +758,7 @@ class ScreenShareCodecNegotiation {
|
||||
return selection;
|
||||
}
|
||||
this.selectedCodec = selection.codec;
|
||||
if (delivery) this.armCodecChangeSuppression();
|
||||
this.armCodecChangeSuppression();
|
||||
this.mediaSessionId = createId('media');
|
||||
logger.info('Selected screen share codec from XState capability intersection', selection);
|
||||
await this.publishSessionUpdate(room, selection);
|
||||
|
||||
@@ -9,12 +9,7 @@ import {
|
||||
type VoiceGatewayStateEvent,
|
||||
type VoiceGatewayStateSnapshot,
|
||||
} from '@app/features/voice/engine/VoiceGatewayStateMachine';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/state/ScreenShareDeliveryRollout', () => ({
|
||||
ScreenShareDeliveryRollout: {enabled: false},
|
||||
default: {enabled: false},
|
||||
}));
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function voiceState(overrides: Partial<VoiceState> = {}): VoiceState {
|
||||
return {
|
||||
|
||||
@@ -19,12 +19,7 @@ import {
|
||||
type VoiceMediaGraphStreamTileState,
|
||||
} from '@app/features/voice/engine/VoiceMediaGraphTileState';
|
||||
import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/state/ScreenShareDeliveryRollout', () => ({
|
||||
ScreenShareDeliveryRollout: {enabled: false},
|
||||
default: {enabled: false},
|
||||
}));
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const SIMULATION_SEEDS: ReadonlyArray<number> = Array.from({length: 20}, (_, index) => 1 + index * 7919);
|
||||
const SIMULATION_STEP_COUNT = 1000;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
voiceMediaGraphStatsTrackKey,
|
||||
} from '@app/features/voice/engine/VoiceMediaGraphStatsObservations';
|
||||
import type {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import type {VoiceEngineV2PerTrackStats} from '@fluxer/voice_engine_v2';
|
||||
|
||||
const VOICE_MEDIA_GRAPH_STATS_TRACK_LIMIT = 1024;
|
||||
@@ -77,9 +76,7 @@ function perTrackStatToInfo(track: VoiceEngineV2PerTrackStats): VoiceMediaGraphP
|
||||
if (track.kind !== 'video') return null;
|
||||
const width = track.frameWidth ?? track.sourceFrameWidth;
|
||||
const height = track.frameHeight ?? track.sourceFrameHeight;
|
||||
const fps = ScreenShareDeliveryRollout.enabled
|
||||
? (track.effectiveFramesPerSecond ?? track.framesPerSecond)
|
||||
: (track.effectiveFramesPerSecond ?? track.framesPerSecond ?? track.sourceFramesPerSecond);
|
||||
const fps = track.effectiveFramesPerSecond ?? track.framesPerSecond;
|
||||
const info: VoiceMediaGraphPartialTrackInfo = {};
|
||||
if (isPositiveDimension(width) && isPositiveDimension(height)) {
|
||||
info.width = width;
|
||||
@@ -193,9 +190,7 @@ function observationToPartialTrackInfo(
|
||||
): VoiceMediaGraphPartialTrackInfo | null {
|
||||
const width = observation.width ?? observation.sourceWidth ?? undefined;
|
||||
const height = observation.height ?? observation.sourceHeight ?? undefined;
|
||||
const fps = ScreenShareDeliveryRollout.enabled
|
||||
? (observation.fps ?? undefined)
|
||||
: (observation.fps ?? observation.sourceFps ?? undefined);
|
||||
const fps = observation.fps ?? undefined;
|
||||
const info: VoiceMediaGraphPartialTrackInfo = {};
|
||||
if (isPositiveDimension(width) && isPositiveDimension(height)) {
|
||||
info.width = width;
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from '@app/features/voice/engine/VoiceStreamWatchState';
|
||||
import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import ParticipantVolume from '@app/features/voice/state/ParticipantVolume';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import {ScreenShareWatchErrorCode, ScreenShareWatchFailures} from '@app/features/voice/state/ScreenShareWatchFailures';
|
||||
import {monitorScreenShareDecodeHealth} from '@app/features/voice/utils/ScreenShareCodecDiagnostics';
|
||||
import {markScreenShareDecodeFailure} from '@app/features/voice/utils/VideoDecoderCapabilities';
|
||||
@@ -108,30 +107,6 @@ interface ParticipantSpeakingDisposer {
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
type LatencyTunedReceiver = RTCRtpReceiver & {
|
||||
jitterBufferTarget?: number;
|
||||
playoutDelayHint?: number;
|
||||
};
|
||||
|
||||
function getRemoteTrackReceiver(track: RemoteTrack): LatencyTunedReceiver | undefined {
|
||||
return (track as RemoteTrack & {receiver?: LatencyTunedReceiver}).receiver;
|
||||
}
|
||||
|
||||
function applyInteractiveReceiverBuffer(track: RemoteTrack, pub: RemoteTrackPublication): void {
|
||||
const receiver = getRemoteTrackReceiver(track);
|
||||
if (!receiver) return;
|
||||
try {
|
||||
if (pub.kind === Track.Kind.Video && pub.source === Track.Source.ScreenShare) {
|
||||
receiver.jitterBufferTarget = 80;
|
||||
receiver.playoutDelayHint = 0.04;
|
||||
} else if (pub.kind === Track.Kind.Audio) {
|
||||
receiver.jitterBufferTarget = 60;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Failed to apply interactive receiver buffer target', {error, source: pub.source, kind: pub.kind});
|
||||
}
|
||||
}
|
||||
|
||||
export function bindRoomEvents(
|
||||
room: Room,
|
||||
attemptId: number,
|
||||
@@ -141,7 +116,6 @@ export function bindRoomEvents(
|
||||
dependencies: RoomEventDependencies,
|
||||
): void {
|
||||
const guard = dependencies.connection.createGuardedHandler;
|
||||
const screenShareDeliveryEnabled = ScreenShareDeliveryRollout.enabled;
|
||||
const participantSpeakingDisposers = new Map<string, ParticipantSpeakingDisposer>();
|
||||
const screenShareDecodeMonitorCancels = new Map<string, () => void>();
|
||||
const remoteTrackLifecycleDisposers = new Map<string, () => void>();
|
||||
@@ -443,9 +417,6 @@ export function bindRoomEvents(
|
||||
trackSid: pub.trackSid,
|
||||
});
|
||||
}
|
||||
if (!screenShareDeliveryEnabled) {
|
||||
applyInteractiveReceiverBuffer(track, pub);
|
||||
}
|
||||
monitorDecodeHealth(track, pub);
|
||||
dependencies.remoteSpeaking.attachIfApplicable(participant, pub, track);
|
||||
bindRemoteTrackLifecycleIfApplicable(participant, pub, track);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {isElectronPlatform} from '@app/features/platform/types/Platform';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {Store} from '@app/features/voice/engine/Store';
|
||||
import {sendVoiceStateDisconnect} from '@app/features/voice/engine/VoiceChannelConnector';
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
isReadyToRepublishTrack,
|
||||
} from '@app/features/voice/engine/v2/VoiceEngineV2AppAdapterAssertions';
|
||||
import {VoiceEngineV2AppReconnectPolicy} from '@app/features/voice/engine/v2/VoiceEngineV2AppReconnectPolicy';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import VoiceRegionTeleport from '@app/features/voice/state/VoiceRegionTeleport';
|
||||
import {
|
||||
findVideoPublishCodecPolicyViolation,
|
||||
@@ -45,10 +43,7 @@ import {
|
||||
} from '@app/features/voice/utils/CodecCapabilityDetector';
|
||||
import {getH264HardwareProfilesSync} from '@app/features/voice/utils/GpuEncoderCapabilities';
|
||||
import {setNoiseSuppressionScopeGuildId} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection';
|
||||
import {
|
||||
SCREEN_SHARE_DELIVERY_MAX_VIDEO_BITRATE_BPS,
|
||||
SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS,
|
||||
} from '@app/features/voice/utils/ScreenShareOptions';
|
||||
import {SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS} from '@app/features/voice/utils/ScreenShareOptions';
|
||||
import {
|
||||
clearScreenShareDecodeFailures,
|
||||
getVideoDecoderExclusionsSync,
|
||||
@@ -151,9 +146,7 @@ function createWebAudioMixOption(): RoomOptions['webAudioMix'] {
|
||||
function createRoomPublishDefaults(): RoomOptions['publishDefaults'] {
|
||||
return {
|
||||
screenShareEncoding: {
|
||||
maxBitrate: ScreenShareDeliveryRollout.enabled
|
||||
? SCREEN_SHARE_DELIVERY_MAX_VIDEO_BITRATE_BPS
|
||||
: SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS,
|
||||
maxBitrate: SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS,
|
||||
maxFramerate: 30,
|
||||
priority: 'high',
|
||||
},
|
||||
@@ -164,7 +157,6 @@ function createRoomPublishDefaults(): RoomOptions['publishDefaults'] {
|
||||
function createRoomOptions(
|
||||
e2eeKey: string | null,
|
||||
subscriberVideoCodecExclusions: RoomOptions['subscriberVideoCodecExclusions'],
|
||||
screenShareDelivery: boolean,
|
||||
): {
|
||||
roomOptions: RoomOptions;
|
||||
e2eeKeyProvider: ExternalE2EEKeyProvider | null;
|
||||
@@ -176,8 +168,7 @@ function createRoomOptions(
|
||||
webAudioMix: createWebAudioMixOption(),
|
||||
publishDefaults: createRoomPublishDefaults(),
|
||||
subscriberVideoCodecExclusions,
|
||||
screenShareDelivery,
|
||||
h264HardwareProfiles: screenShareDelivery ? getH264HardwareProfilesSync()?.profiles : undefined,
|
||||
h264HardwareProfiles: getH264HardwareProfilesSync()?.profiles,
|
||||
};
|
||||
let e2eeKeyProvider: ExternalE2EEKeyProvider | null = null;
|
||||
let e2eeWorker: Worker | null = null;
|
||||
@@ -196,19 +187,11 @@ function createRoomOptions(
|
||||
return {roomOptions, e2eeKeyProvider, e2eeWorker};
|
||||
}
|
||||
|
||||
function createRoomConnectOptions(screenShareDelivery: boolean): RoomConnectOptions {
|
||||
function createRoomConnectOptions(): RoomConnectOptions {
|
||||
const connectOptions: RoomConnectOptions = {
|
||||
autoSubscribe: false,
|
||||
};
|
||||
assert.equal(connectOptions.autoSubscribe, false, 'LiveKit connect options must not auto-subscribe');
|
||||
if (!screenShareDelivery && isElectronPlatform()) {
|
||||
connectOptions.rtcConfig = {iceTransportPolicy: 'relay'};
|
||||
assert.equal(
|
||||
connectOptions.rtcConfig.iceTransportPolicy,
|
||||
'relay',
|
||||
'Electron LiveKit connects must force relay ICE',
|
||||
);
|
||||
}
|
||||
return connectOptions;
|
||||
}
|
||||
|
||||
@@ -564,12 +547,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store {
|
||||
logger.warn('Aborting LiveKit room creation after codec probing because attempt is stale', {attemptId});
|
||||
return;
|
||||
}
|
||||
const screenShareDelivery = ScreenShareDeliveryRollout.enabled;
|
||||
const {roomOptions, e2eeKeyProvider, e2eeWorker} = createRoomOptions(
|
||||
e2eeKey,
|
||||
subscriberVideoCodecExclusions,
|
||||
screenShareDelivery,
|
||||
);
|
||||
const {roomOptions, e2eeKeyProvider, e2eeWorker} = createRoomOptions(e2eeKey, subscriberVideoCodecExclusions);
|
||||
const room = new LiveKitRoom(roomOptions);
|
||||
ownE2EEWorker(room, e2eeWorker);
|
||||
let roomClosed = false;
|
||||
@@ -602,7 +580,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store {
|
||||
return;
|
||||
}
|
||||
logger.info('Attempting to connect to LiveKit', {endpoint, guildId, channelId: resolvedChannelId});
|
||||
const connectOptions = createRoomConnectOptions(screenShareDelivery);
|
||||
const connectOptions = createRoomConnectOptions();
|
||||
room
|
||||
.connect(endpoint, token, connectOptions)
|
||||
.then(() => {
|
||||
@@ -704,14 +682,12 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store {
|
||||
const connectionId = raw.connection_id ?? null;
|
||||
this.abortHotSwap();
|
||||
const cachedExclusions = getVideoDecoderExclusionsSync();
|
||||
const screenShareDelivery = ScreenShareDeliveryRollout.enabled;
|
||||
const roomOptions: RoomOptions = {
|
||||
adaptiveStream: false,
|
||||
dynacast: true,
|
||||
webAudioMix: createWebAudioMixOption(),
|
||||
publishDefaults: createRoomPublishDefaults(),
|
||||
subscriberVideoCodecExclusions: cachedExclusions && cachedExclusions.length > 0 ? cachedExclusions : undefined,
|
||||
screenShareDelivery,
|
||||
};
|
||||
if (!this.isLatestConnectionAttempt(attemptId) || this.connectionState.room !== existingRoom) {
|
||||
logger.warn('Region hot-swap: aborted before room creation because attempt is stale', {attemptId});
|
||||
@@ -728,7 +704,7 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store {
|
||||
this.abortHotSwap();
|
||||
}
|
||||
});
|
||||
const connectOptions = createRoomConnectOptions(screenShareDelivery);
|
||||
const connectOptions = createRoomConnectOptions();
|
||||
logger.info('Region hot-swap: connecting to new endpoint', {endpoint, guildId, channelId});
|
||||
newRoom
|
||||
.connect(endpoint, token, connectOptions)
|
||||
|
||||
+1
-10
@@ -63,7 +63,6 @@ import ActiveScreenShareSource, {
|
||||
type PublishedScreenShareSource,
|
||||
} from '@app/features/voice/state/ActiveScreenShareSource';
|
||||
import LocalVoiceState from '@app/features/voice/state/LocalVoiceState';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {
|
||||
prepareHighFidelityScreenShareAudioTrack,
|
||||
@@ -183,7 +182,6 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
encoderVerificationTimer: (() => void) | null = null;
|
||||
private screenShareTrackingHolds = 0;
|
||||
private screenShareCapturePaused = false;
|
||||
private screenShareDeliveryArmed = false;
|
||||
private screenShareTrackingGeneration = 0;
|
||||
private readonly reconciledCodecPairs = new Set<string>();
|
||||
private readonly verifiedCodecCorrectionsByTrack = new WeakMap<MediaStreamTrack, number>();
|
||||
@@ -679,7 +677,6 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
preferredTrack?: LocalVideoTrack,
|
||||
): void {
|
||||
this.cleanupActiveScreenShareEndListenerInternal();
|
||||
this.screenShareDeliveryArmed = ScreenShareDeliveryRollout.enabled;
|
||||
const publication = preferredTrack ? undefined : participant.getTrackPublication(Track.Source.ScreenShare);
|
||||
const videoTrack = preferredTrack ?? publication?.videoTrack;
|
||||
const mediaStreamTrack = videoTrack?.mediaStreamTrack;
|
||||
@@ -730,15 +727,11 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
};
|
||||
const onMuted = (): void => reportCapturePaused(true);
|
||||
const onUnmuted = (): void => reportCapturePaused(false);
|
||||
const tracksCapturePauses = this.screenShareDeliveryArmed;
|
||||
mediaStreamTrack.addEventListener('ended', onEnded);
|
||||
if (tracksCapturePauses) {
|
||||
mediaStreamTrack.addEventListener('mute', onMuted);
|
||||
mediaStreamTrack.addEventListener('unmute', onUnmuted);
|
||||
}
|
||||
this.activeScreenShareEndListener = () => {
|
||||
mediaStreamTrack.removeEventListener('ended', onEnded);
|
||||
if (!tracksCapturePauses) return;
|
||||
mediaStreamTrack.removeEventListener('mute', onMuted);
|
||||
mediaStreamTrack.removeEventListener('unmute', onUnmuted);
|
||||
};
|
||||
@@ -1152,7 +1145,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
nextConstraints.frameRate = {ideal: resolution.frameRate, max: resolution.frameRate};
|
||||
}
|
||||
if (JSON.stringify(currentConstraints) === JSON.stringify(nextConstraints)) return 'unchanged';
|
||||
if (this.screenShareDeliveryArmed && screenShareSourceHasClonedTracks(screenShareTrack)) {
|
||||
if (screenShareSourceHasClonedTracks(screenShareTrack)) {
|
||||
logger.warn('Screen share capture geometry needs a restart while a backup codec holds a clone of the source', {
|
||||
resolution,
|
||||
});
|
||||
@@ -1202,7 +1195,6 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
this.ensureScreenShareKeepAliveSinkInternal(participant);
|
||||
updateLocalParticipantFromRoom(room);
|
||||
this.syncLocalScreenShareAudioStateInternal(participant, participant.isScreenShareEnabled);
|
||||
if (!this.screenShareDeliveryArmed) return true;
|
||||
return geometryUpdate !== 'needs-capture-restart' && geometryUpdate !== 'failed';
|
||||
}
|
||||
|
||||
@@ -1225,7 +1217,6 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
}
|
||||
|
||||
resetStreamTracking(): void {
|
||||
this.screenShareDeliveryArmed = false;
|
||||
this.setScreenShareCapturePausedInternal(false);
|
||||
this.clearScreenShareKeepAliveSinkInternal();
|
||||
this.setStreamingPriorityInternal(false);
|
||||
|
||||
@@ -32,11 +32,6 @@ const voiceStates: Record<string, Record<string, Record<string, unknown>>> = {};
|
||||
const settingsUpdate = vi.fn();
|
||||
const openPremiumModal = vi.fn();
|
||||
|
||||
vi.mock('@app/features/voice/state/ScreenShareDeliveryRollout', () => ({
|
||||
ScreenShareDeliveryRollout: {enabled: true},
|
||||
default: {enabled: true},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/utils/GpuEncoderCapabilities', () => ({
|
||||
getGpuEncoderReportSync: () => gpuReport,
|
||||
getH264HardwareProfilesSync: () => h264HardwareProfiles,
|
||||
|
||||
-48
@@ -6,13 +6,11 @@ import {
|
||||
stopMediaTrack,
|
||||
stopUnselectedStreamTracks,
|
||||
} from '@app/features/voice/engine/voice_screen_share_manager/shared';
|
||||
import ActiveScreenShareSource from '@app/features/voice/state/ActiveScreenShareSource';
|
||||
import {rememberCapturedDisplayAudioTrack} from '@app/features/voice/utils/NativeAudioCaptureBridge';
|
||||
import {ScreenShareAudioCaptureError} from '@app/features/voice/utils/ScreenShareAudioCaptureError';
|
||||
import type {ScreenShareCaptureOptions} from 'livekit-client';
|
||||
|
||||
type DisplayMediaVideoConstraints = MediaTrackConstraints & {
|
||||
colorSpace?: string;
|
||||
cursor?: 'always' | 'motion' | 'never';
|
||||
displaySurface?: 'browser' | 'monitor' | 'window';
|
||||
};
|
||||
@@ -20,37 +18,12 @@ type DisplayMediaAudioConstraints = MediaTrackConstraints & {
|
||||
restrictOwnAudio?: boolean;
|
||||
suppressLocalAudioPlayback?: boolean;
|
||||
};
|
||||
type DisplayMediaTrackSettings = MediaTrackSettings & {
|
||||
cursor?: 'always' | 'motion' | 'never';
|
||||
displaySurface?: 'browser' | 'monitor' | 'window';
|
||||
};
|
||||
function resolveDisplayMediaCursorCapture(
|
||||
displaySurface: DisplayMediaVideoConstraints['displaySurface'],
|
||||
): 'always' | 'motion' | 'never' {
|
||||
return displaySurface === 'window' ? 'never' : 'always';
|
||||
}
|
||||
|
||||
function getRequestedDisplayMediaVideoConstraints(
|
||||
options: ScreenShareCaptureOptions | undefined,
|
||||
): DisplayMediaVideoConstraints | null {
|
||||
if (typeof options?.video !== 'object' || !options.video) return null;
|
||||
return options.video as DisplayMediaVideoConstraints;
|
||||
}
|
||||
|
||||
function resolveCapturedDisplayMediaCursorCapture(
|
||||
track: Pick<MediaStreamTrack, 'getSettings'>,
|
||||
options?: ScreenShareCaptureOptions,
|
||||
): 'always' | 'motion' | 'never' {
|
||||
const requestedVideo = getRequestedDisplayMediaVideoConstraints(options);
|
||||
const requestedCursor = requestedVideo?.cursor;
|
||||
const requestedDisplaySurface = requestedVideo?.displaySurface;
|
||||
if (requestedCursor && requestedCursor !== resolveDisplayMediaCursorCapture(requestedDisplaySurface)) {
|
||||
return requestedCursor;
|
||||
}
|
||||
const settings = track.getSettings() as DisplayMediaTrackSettings;
|
||||
return resolveDisplayMediaCursorCapture(settings.displaySurface ?? requestedDisplaySurface);
|
||||
}
|
||||
|
||||
export function getDisplayMediaOptions(options?: ScreenShareCaptureOptions): DisplayMediaStreamOptions {
|
||||
let videoConstraints: MediaTrackConstraints | boolean = options?.video ?? true;
|
||||
const resolution = options?.resolution;
|
||||
@@ -95,21 +68,6 @@ export function getDisplayMediaOptions(options?: ScreenShareCaptureOptions): Dis
|
||||
} as DisplayMediaStreamOptions;
|
||||
}
|
||||
|
||||
function buildCapturedDisplayMediaConstraints(
|
||||
displayMediaOptions: DisplayMediaStreamOptions,
|
||||
cursor: 'always' | 'motion' | 'never',
|
||||
): MediaTrackConstraints {
|
||||
const requestedVideo =
|
||||
typeof displayMediaOptions.video === 'object' && displayMediaOptions.video
|
||||
? (displayMediaOptions.video as DisplayMediaVideoConstraints)
|
||||
: undefined;
|
||||
const constraints: DisplayMediaVideoConstraints = {colorSpace: 'rec709', cursor};
|
||||
if (requestedVideo?.width !== undefined) constraints.width = requestedVideo.width;
|
||||
if (requestedVideo?.height !== undefined) constraints.height = requestedVideo.height;
|
||||
if (requestedVideo?.frameRate !== undefined) constraints.frameRate = requestedVideo.frameRate;
|
||||
return constraints;
|
||||
}
|
||||
|
||||
export async function createDisplayScreenShareTracks(
|
||||
options?: ScreenShareCaptureOptions,
|
||||
captureContext?: DisplayScreenShareCaptureContext,
|
||||
@@ -127,12 +85,6 @@ export async function createDisplayScreenShareTracks(
|
||||
if (options?.contentHint) {
|
||||
videoTrack.contentHint = options.contentHint;
|
||||
}
|
||||
if (ActiveScreenShareSource.getTarget()?.delivery !== true) {
|
||||
const cursor = resolveCapturedDisplayMediaCursorCapture(videoTrack, options);
|
||||
await videoTrack
|
||||
.applyConstraints(buildCapturedDisplayMediaConstraints(displayMediaOptions, cursor))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
const capturedAudioTrack = stream.getAudioTracks()[0];
|
||||
const audioTrack = capturedAudioTrack?.readyState === 'live' ? capturedAudioTrack : undefined;
|
||||
rememberCapturedDisplayAudioTrack(capturedAudioTrack, Boolean(capturedAudioTrack) && !audioTrack);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {getDesktopTroubleshootingSettings} from '@app/features/devtools/utils/De
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import ScreenShareCodecNegotiation from '@app/features/voice/engine/ScreenShareCodecNegotiation';
|
||||
import ActiveScreenShareSource from '@app/features/voice/state/ActiveScreenShareSource';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import SoftwareEncoderWarning from '@app/features/voice/state/SoftwareEncoderWarning';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
resolveScreenShareLayering,
|
||||
resolveScreenShareSenderCodec,
|
||||
resolveScreenShareTarget,
|
||||
SCREEN_SHARE_DELIVERY_MAX_VIDEO_BITRATE_BPS,
|
||||
SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS,
|
||||
type ScreenShareContext,
|
||||
type ScreenShareLayering,
|
||||
@@ -195,11 +193,13 @@ export async function releaseScreenShareCaptureCleanup(snapshot: ScreenShareCapt
|
||||
}
|
||||
}
|
||||
|
||||
function clampScreenShareEncoding(encoding: VideoEncoding, delivery: boolean): VideoEncoding {
|
||||
const ceiling = delivery ? SCREEN_SHARE_DELIVERY_MAX_VIDEO_BITRATE_BPS : SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS;
|
||||
function clampScreenShareEncoding(encoding: VideoEncoding): VideoEncoding {
|
||||
return {
|
||||
...encoding,
|
||||
maxBitrate: typeof encoding.maxBitrate === 'number' ? Math.min(encoding.maxBitrate, ceiling) : encoding.maxBitrate,
|
||||
maxBitrate:
|
||||
typeof encoding.maxBitrate === 'number'
|
||||
? Math.min(encoding.maxBitrate, SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS)
|
||||
: encoding.maxBitrate,
|
||||
priority: encoding.priority ?? 'high',
|
||||
};
|
||||
}
|
||||
@@ -208,15 +208,10 @@ export function resolveActiveScreenShareContext(): ScreenShareContext {
|
||||
return ActiveScreenShareSource.getShareContext() ?? 'display';
|
||||
}
|
||||
|
||||
function resolveScreenShareDeliveryArm(): boolean {
|
||||
return ActiveScreenShareSource.getTarget()?.delivery ?? ScreenShareDeliveryRollout.enabled;
|
||||
}
|
||||
|
||||
export function resolveConfiguredScreenShareTarget(
|
||||
context: ScreenShareContext,
|
||||
sourceDimensions: {width: number; height: number} | null,
|
||||
): ScreenShareTarget {
|
||||
const delivery = resolveScreenShareDeliveryArm();
|
||||
return resolveScreenShareTarget({
|
||||
mode: VoiceSettings.getStreamingMode(),
|
||||
storedResolution: VoiceSettings.getScreenshareResolution(),
|
||||
@@ -225,13 +220,8 @@ export function resolveConfiguredScreenShareTarget(
|
||||
context,
|
||||
sourceDimensions,
|
||||
hintSetting: VoiceSettings.getScreenShareContentHint(),
|
||||
delivery,
|
||||
...(delivery
|
||||
? {
|
||||
codec: getPreferredScreenShareCodec(),
|
||||
softwareEncoderClamp: ActiveScreenShareSource.isSoftwareEncoderClamped(),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,7 +232,6 @@ export function resolveActiveScreenShareTarget(
|
||||
}
|
||||
|
||||
function recommitClampedScreenShareTarget(committed: ScreenShareTarget): ScreenShareTarget {
|
||||
if (committed.delivery !== true) return committed;
|
||||
if (committed.softwareEncoderClamped) return committed;
|
||||
const resolved = resolveActiveScreenShareTarget(committed.context);
|
||||
if (!resolved.softwareEncoderClamped) return committed;
|
||||
@@ -270,7 +259,7 @@ export function getStatsKind(
|
||||
|
||||
function resolveScreenShareEncoding(target: ScreenShareTarget, publishOptions?: TrackPublishOptions): VideoEncoding {
|
||||
if (publishOptions?.screenShareEncoding) {
|
||||
return clampScreenShareEncoding(publishOptions.screenShareEncoding, target.delivery === true);
|
||||
return clampScreenShareEncoding(publishOptions.screenShareEncoding);
|
||||
}
|
||||
return {maxBitrate: target.maxBitrate, maxFramerate: target.frameRate, priority: 'high'};
|
||||
}
|
||||
@@ -863,18 +852,14 @@ export function startScreenShareEncoderMonitor(options: ScreenShareEncoderMonito
|
||||
let framesEncoded: number | null = null;
|
||||
let sendSnapshot: ScreenShareSendSnapshot | null = null;
|
||||
let cpuLimitedTicks = 0;
|
||||
const delivery = ActiveScreenShareSource.getTarget()?.delivery === true;
|
||||
const tick = async (): Promise<void> => {
|
||||
const sender = options.track.sender;
|
||||
if (!sender) return;
|
||||
const stats = await sender.getStats();
|
||||
if (delivery) {
|
||||
syncScreenShareCaptureSize(sender);
|
||||
}
|
||||
const encoded = countEncodedVideoFrames(stats);
|
||||
ActiveScreenShareSource.setEncoding(encoded !== null && framesEncoded !== null && encoded > framesEncoded);
|
||||
framesEncoded = encoded;
|
||||
if (delivery) {
|
||||
const snapshot = collectScreenShareSendSnapshot(stats);
|
||||
cpuLimitedTicks = snapshot.cpuLimited ? cpuLimitedTicks + 1 : 0;
|
||||
const limit = classifyScreenShareSendLimit(sendSnapshot, snapshot, cpuLimitedTicks);
|
||||
@@ -883,7 +868,6 @@ export function startScreenShareEncoderMonitor(options: ScreenShareEncoderMonito
|
||||
logger.info('Screen share send limit changed', {limit, codec: options.codec});
|
||||
}
|
||||
ActiveScreenShareSource.setLimit(limit);
|
||||
}
|
||||
if (!verified) {
|
||||
const verification = verifyScreenShareEncoderStart(stats, options.codec, options.onEncodeFailure);
|
||||
if (verification === 'failed') return;
|
||||
@@ -907,8 +891,6 @@ export function startScreenShareEncoderMonitor(options: ScreenShareEncoderMonito
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
ActiveScreenShareSource.setEncoding(false);
|
||||
if (delivery) {
|
||||
ActiveScreenShareSource.setLimit(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,11 +70,6 @@ class ActiveScreenShareSource {
|
||||
this.encoding = false;
|
||||
return;
|
||||
}
|
||||
if (target.delivery !== true) {
|
||||
this.target = target;
|
||||
this.encoding = false;
|
||||
return;
|
||||
}
|
||||
const degradationPreference = this.frozenDegradationPreference ?? target.degradationPreference;
|
||||
this.frozenDegradationPreference = degradationPreference;
|
||||
this.softwareEncoderClamped = this.softwareEncoderClamped || target.softwareEncoderClamped;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import ExperimentAssignments from '@app/features/experiment/state/ExperimentAssignments';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {readScreenShareDeliveryAssignment} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
|
||||
const logger = new Logger('ScreenShareDeliveryRollout');
|
||||
|
||||
class ScreenShareDeliveryRolloutSelector {
|
||||
get enabled(): boolean {
|
||||
try {
|
||||
return readScreenShareDeliveryAssignment(ExperimentAssignments.response).enabled;
|
||||
} catch (err) {
|
||||
logger.warn('Failed to resolve screen share delivery assignment:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ScreenShareDeliveryRollout = new ScreenShareDeliveryRolloutSelector();
|
||||
|
||||
export default ScreenShareDeliveryRollout;
|
||||
@@ -3,7 +3,6 @@
|
||||
import AppStorage from '@app/features/platform/state/PersistentStorage';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {makePersistent} from '@app/features/platform/utils/MobXPersistence';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import type {
|
||||
CodecPreference,
|
||||
ScreenShareContentHint,
|
||||
@@ -263,9 +262,6 @@ function applyStreamingModeDefaultMigrationV1(parsed: Record<string, unknown>):
|
||||
}
|
||||
|
||||
function applyScreenShareFrameRateFloorMigrationV1(parsed: Record<string, unknown>): boolean {
|
||||
if (!ScreenShareDeliveryRollout.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.screenShareFrameRateFloorMigratedV1 === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import type {TrackPublishOptions} from 'livekit-client';
|
||||
|
||||
export const OPUS_MAX_AUDIO_BITRATE_BPS = 510000;
|
||||
@@ -40,9 +39,7 @@ export function buildMicrophonePublishOptions(
|
||||
|
||||
export const SCREEN_SHARE_AUDIO_PUBLISH_OPTIONS: TrackPublishOptions = {
|
||||
audioPreset: {
|
||||
get maxBitrate(): number {
|
||||
return ScreenShareDeliveryRollout.enabled ? SCREEN_SHARE_AUDIO_BITRATE_BPS : OPUS_MAX_AUDIO_BITRATE_BPS;
|
||||
},
|
||||
maxBitrate: SCREEN_SHARE_AUDIO_BITRATE_BPS,
|
||||
priority: 'high',
|
||||
},
|
||||
dtx: false,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
isFirefoxBrowser,
|
||||
type NativePlatform,
|
||||
} from '@app/features/ui/utils/NativeUtils';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {getGpuEncoderReportSync, type HardwareEncodeAnswer} from '@app/features/voice/utils/GpuEncoderCapabilities';
|
||||
import {
|
||||
@@ -26,10 +25,7 @@ import {
|
||||
type ScreenShareCodecRanking,
|
||||
} from '@app/features/voice/utils/ScreenShareCodecSelection';
|
||||
import {normaliseStreamingModeForContext} from '@app/features/voice/utils/ScreenShareOptions';
|
||||
import {
|
||||
getProbedVideoDecoderExclusionsSync,
|
||||
getVideoDecoderExclusionsSync,
|
||||
} from '@app/features/voice/utils/VideoDecoderCapabilities';
|
||||
import {getProbedVideoDecoderExclusionsSync} from '@app/features/voice/utils/VideoDecoderCapabilities';
|
||||
import type {TrackPublishDefaults, TrackPublishOptions} from 'livekit-client';
|
||||
import {BackupCodecPolicy, supportsVideoCodec, type VideoCodec, type VideoEncoding} from 'livekit-client';
|
||||
|
||||
@@ -98,7 +94,6 @@ let cachedReportNativeHardwareEncoderKey: object | null | undefined;
|
||||
let cachedReportHardwareAccelerationDisabled: boolean | undefined;
|
||||
let cachedReportAv1OptIn: boolean | undefined;
|
||||
let cachedReportHevcOptIn: boolean | undefined;
|
||||
let cachedReportDelivery: boolean | undefined;
|
||||
const runtimeEncodeFailureCodecs = new Set<VideoCodec>();
|
||||
const observedSoftwareEncodeCodecs = new Set<VideoCodec>();
|
||||
|
||||
@@ -214,21 +209,15 @@ function getScreenShareCodecPolicyUnsupported(
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasPublishPathNativeHardwareEncoder(
|
||||
codec: VideoCodec,
|
||||
context: CodecPolicyContext,
|
||||
delivery: boolean,
|
||||
): boolean {
|
||||
function hasPublishPathNativeHardwareEncoder(codec: VideoCodec, context: CodecPolicyContext): boolean {
|
||||
if (!hasNativeHardwareEncoder(codec)) return false;
|
||||
const backend = getNativeHardwareEncoderCapabilitiesSync()?.backend;
|
||||
if (!delivery) return backend !== 'videotoolbox' && backend !== 'nvenc';
|
||||
if (backend === 'videotoolbox') return true;
|
||||
if (backend === 'nvenc') return context.platform !== 'linux';
|
||||
return false;
|
||||
}
|
||||
|
||||
function buildReport(): CodecCapabilityReport {
|
||||
const delivery = ScreenShareDeliveryRollout.enabled;
|
||||
const {caps, probedSuccessfully} = probeRawCapabilities();
|
||||
const context = buildScreenShareCodecPolicyContext();
|
||||
const gpuReport = getGpuEncoderReportSync();
|
||||
@@ -243,17 +232,11 @@ function buildReport(): CodecCapabilityReport {
|
||||
if (observedSoftwareEncodeCodecs.has(codec)) {
|
||||
return 'software';
|
||||
}
|
||||
if (!delivery) {
|
||||
if (hasPublishPathNativeHardwareEncoder(codec, context, delivery)) {
|
||||
return 'hardware';
|
||||
}
|
||||
return gpuReport ? gpuReport[codec] : 'unknown';
|
||||
}
|
||||
const measured = gpuReport ? gpuReport[codec] : 'unknown';
|
||||
if (measured !== 'unknown') {
|
||||
return measured;
|
||||
}
|
||||
return hasPublishPathNativeHardwareEncoder(codec, context, delivery) ? 'hardware' : 'unknown';
|
||||
return hasPublishPathNativeHardwareEncoder(codec, context) ? 'hardware' : 'unknown';
|
||||
}
|
||||
type DescribedUnsupported = Omit<CodecSupportInfo, 'hardwareAccelerated'>;
|
||||
function unsupported(codec: keyof CodecCapabilities, info: DescribedUnsupported): CodecSupportInfo {
|
||||
@@ -271,8 +254,8 @@ function buildReport(): CodecCapabilityReport {
|
||||
detail: 'This codec failed while publishing during the current session.',
|
||||
});
|
||||
}
|
||||
const supportedByNativeHardware = hasPublishPathNativeHardwareEncoder(codec, context, delivery);
|
||||
if (caps[codec] || (!delivery && supportedByNativeHardware)) {
|
||||
const supportedByNativeHardware = hasPublishPathNativeHardwareEncoder(codec, context);
|
||||
if (caps[codec]) {
|
||||
return {
|
||||
supported: true,
|
||||
reason: 'supported',
|
||||
@@ -348,10 +331,8 @@ export function getCodecCapabilityReport(): CodecCapabilityReport {
|
||||
const hardwareAccelerationDisabled = isDesktopHardwareAccelerationDisabled();
|
||||
const av1OptIn = VoiceSettings.getScreenShareAv1OptIn();
|
||||
const hevcOptIn = VoiceSettings.getScreenShareHevcOptIn();
|
||||
const delivery = ScreenShareDeliveryRollout.enabled;
|
||||
if (
|
||||
cachedReport &&
|
||||
cachedReportDelivery === delivery &&
|
||||
cachedReportGpuKey === currentGpu &&
|
||||
cachedReportNativeHardwareEncoderKey === currentNativeHardwareEncoder &&
|
||||
cachedReportHardwareAccelerationDisabled === hardwareAccelerationDisabled &&
|
||||
@@ -365,7 +346,6 @@ export function getCodecCapabilityReport(): CodecCapabilityReport {
|
||||
cachedReportHardwareAccelerationDisabled = hardwareAccelerationDisabled;
|
||||
cachedReportAv1OptIn = av1OptIn;
|
||||
cachedReportHevcOptIn = hevcOptIn;
|
||||
cachedReportDelivery = delivery;
|
||||
return cachedReport;
|
||||
}
|
||||
|
||||
@@ -538,9 +518,7 @@ export function getVideoPublishCodecDenial(codec: VideoCodec): VideoPublishCodec
|
||||
if (!supportsVideoCodec(codec)) return 'sender-cannot-encode';
|
||||
if (getScreenShareCodecPolicyUnsupported(codec, buildScreenShareCodecPolicyContext())) return 'policy';
|
||||
if (runtimeEncodeFailureCodecs.has(codec)) return 'runtime-failed';
|
||||
const decoderExclusions = ScreenShareDeliveryRollout.enabled
|
||||
? getProbedVideoDecoderExclusionsSync()
|
||||
: getVideoDecoderExclusionsSync();
|
||||
const decoderExclusions = getProbedVideoDecoderExclusionsSync();
|
||||
if (decoderExclusions?.includes(codec) === true) return 'decoder-excluded';
|
||||
return null;
|
||||
}
|
||||
@@ -701,7 +679,6 @@ export function resetCachedCodecCapabilities(): void {
|
||||
cachedReportHardwareAccelerationDisabled = undefined;
|
||||
cachedReportAv1OptIn = undefined;
|
||||
cachedReportHevcOptIn = undefined;
|
||||
cachedReportDelivery = undefined;
|
||||
runtimeEncodeFailureCodecs.clear();
|
||||
observedSoftwareEncodeCodecs.clear();
|
||||
resetNativeHardwareEncoderCapabilities();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {getElectronAPI, isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import type {GpuDeviceInfo, GpuInfo} from '@app/types/electron.d';
|
||||
import type {VideoCodec} from 'livekit-client';
|
||||
|
||||
@@ -238,12 +237,6 @@ export const H264_ENCODE_PROBE_CONTENT_TYPES: ReadonlyArray<string> = H264_PROBE
|
||||
(profileLevelId) => `video/H264;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=${profileLevelId}`,
|
||||
);
|
||||
|
||||
const CONTROL_H264_PROBE_PROFILE_LEVEL_IDS: ReadonlyArray<string> = ['640028', '4d0028', '420028', '42e028'];
|
||||
|
||||
const CONTROL_H264_ENCODE_PROBE_CONTENT_TYPES: ReadonlyArray<string> = CONTROL_H264_PROBE_PROFILE_LEVEL_IDS.map(
|
||||
(profileLevelId) => `video/H264;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=${profileLevelId}`,
|
||||
);
|
||||
|
||||
export const WEBRTC_ENCODE_PROBE_CONTENT_TYPES: Record<VideoCodec, ReadonlyArray<string>> = {
|
||||
av1: ['video/AV1'],
|
||||
h265: ['video/H265'],
|
||||
@@ -415,24 +408,17 @@ export async function probeWebRtcEncodeEfficiency(
|
||||
): Promise<Record<VideoCodec, HardwareEncodeAnswer> | null> {
|
||||
const mediaCapabilities = getMediaCapabilities();
|
||||
if (!mediaCapabilities) return null;
|
||||
const delivery = ScreenShareDeliveryRollout.enabled;
|
||||
const video = resolveEncodeProbeVideoConfig(config);
|
||||
const codecs: ReadonlyArray<VideoCodec> = ['av1', 'h265', 'h264', 'vp9', 'vp8'];
|
||||
const answers = await Promise.all(
|
||||
codecs.map((codec) =>
|
||||
probeContentTypesEncodeEfficiency(
|
||||
mediaCapabilities,
|
||||
codec === 'h264' && !delivery
|
||||
? CONTROL_H264_ENCODE_PROBE_CONTENT_TYPES
|
||||
: WEBRTC_ENCODE_PROBE_CONTENT_TYPES[codec],
|
||||
video,
|
||||
),
|
||||
probeContentTypesEncodeEfficiency(mediaCapabilities, WEBRTC_ENCODE_PROBE_CONTENT_TYPES[codec], video),
|
||||
),
|
||||
);
|
||||
const result = {} as Record<VideoCodec, HardwareEncodeAnswer>;
|
||||
codecs.forEach((codec, index) => {
|
||||
const codecAnswers = answers[index] ?? [];
|
||||
if (delivery && codec === 'h264') recordH264HardwareProfileProbe(codecAnswers, video);
|
||||
if (codec === 'h264') recordH264HardwareProfileProbe(codecAnswers, video);
|
||||
result[codec] = collapseEncodeProbeAnswers(codecAnswers);
|
||||
});
|
||||
return result;
|
||||
@@ -460,7 +446,6 @@ let pendingPromise: Promise<HardwareEncodeReport | null> | null = null;
|
||||
|
||||
function fetchReport(): Promise<HardwareEncodeReport | null> {
|
||||
if (!isDesktop()) {
|
||||
if (!ScreenShareDeliveryRollout.enabled) return Promise.resolve(null);
|
||||
return probeH264HardwareProfiles().then(() => null);
|
||||
}
|
||||
const electron = getElectronAPI();
|
||||
|
||||
@@ -9,12 +9,7 @@ import {
|
||||
markScreenShareDecodeFailure,
|
||||
resetVideoDecoderExclusions,
|
||||
} from '@app/features/voice/utils/VideoDecoderCapabilities';
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/state/ScreenShareDeliveryRollout', () => ({
|
||||
ScreenShareDeliveryRollout: {enabled: true},
|
||||
default: {enabled: true},
|
||||
}));
|
||||
import {afterEach, describe, expect, it} from 'vitest';
|
||||
|
||||
function buildStats(inbound: Record<string, unknown>): RTCStatsReport {
|
||||
return new Map<string, unknown>([
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import SoftwareEncoderWarning from '@app/features/voice/state/SoftwareEncoderWarning';
|
||||
import {classifyVideoDecoderAcceleration} from '@app/features/voice/utils/VideoAccelerationClassification';
|
||||
import type {VideoCodec} from 'livekit-client';
|
||||
|
||||
const logger = new Logger('ScreenShareCodecDiagnostics');
|
||||
const DECODE_SAMPLE_INTERVAL_MS = 5000;
|
||||
const DECODER_VERIFICATION_DELAY_MS = 5000;
|
||||
const UNKNOWN_DECODER_IMPLEMENTATION = 'software decoder';
|
||||
|
||||
interface CodecStatsEntry {
|
||||
@@ -144,97 +142,6 @@ function collectInboundVideoStats(stats: RTCStatsReport): {
|
||||
return {codecs, reports};
|
||||
}
|
||||
|
||||
function findSoftwareVideoDecoderInStats(stats: RTCStatsReport): SoftwareVideoDecoderInfo | null {
|
||||
const {codecs, reports} = collectInboundVideoStats(stats);
|
||||
for (const report of reports) {
|
||||
if (getStatsKind(report, codecs) !== 'video') continue;
|
||||
const implementation =
|
||||
typeof report.decoderImplementation === 'string' && report.decoderImplementation.length > 0
|
||||
? report.decoderImplementation
|
||||
: null;
|
||||
const powerEfficientDecoder =
|
||||
typeof report.powerEfficientDecoder === 'boolean' ? report.powerEfficientDecoder : null;
|
||||
if (!isSoftwareVideoStats(implementation, powerEfficientDecoder)) continue;
|
||||
return {
|
||||
codec: getCodecLabel(report.codecId ? codecs.get(report.codecId)?.mimeType : undefined),
|
||||
implementation: implementation ?? UNKNOWN_DECODER_IMPLEMENTATION,
|
||||
powerEfficientDecoder,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findStalledVideoDecoderInStats(stats: RTCStatsReport): StalledVideoDecoderInfo | null {
|
||||
const {codecs, reports} = collectInboundVideoStats(stats);
|
||||
for (const report of reports) {
|
||||
if (getStatsKind(report, codecs) !== 'video') continue;
|
||||
const framesDecoded = finiteNumber(report.framesDecoded);
|
||||
if (framesDecoded === null || framesDecoded > 0) continue;
|
||||
const framesReceived = finiteNumber(report.framesReceived);
|
||||
if (framesReceived === null || framesReceived < 1) continue;
|
||||
const mimeType = report.codecId ? codecs.get(report.codecId)?.mimeType : undefined;
|
||||
const codec = getVideoCodecFromMimeType(mimeType);
|
||||
if (!codec) continue;
|
||||
return {
|
||||
codec,
|
||||
mimeType,
|
||||
packetsReceived: finiteNumber(report.packetsReceived) ?? 0,
|
||||
bytesReceived: finiteNumber(report.bytesReceived) ?? 0,
|
||||
framesDecoded,
|
||||
framesReceived,
|
||||
framesDropped: finiteNumber(report.framesDropped),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function scheduleScreenShareDecoderVerification(
|
||||
getStats: () => Promise<RTCStatsReport | undefined>,
|
||||
onDecodeFailure?: (failure: StalledVideoDecoderInfo) => void,
|
||||
): () => void {
|
||||
let cancelled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
const scheduleConfirmation = (first: StalledVideoDecoderInfo): void => {
|
||||
timer = setTimeout(async () => {
|
||||
timer = null;
|
||||
try {
|
||||
const stats = await getStats();
|
||||
const confirmed = confirmDecodeStall(first, stats ? findStalledVideoDecoderInStats(stats) : null);
|
||||
if (cancelled || !confirmed) return;
|
||||
logger.warn('Screen share video decode is stalled', confirmed);
|
||||
onDecodeFailure?.(confirmed);
|
||||
} catch (error) {
|
||||
logger.debug('Failed to confirm the screen share decode stall', {error});
|
||||
}
|
||||
}, DECODER_VERIFICATION_DELAY_MS);
|
||||
};
|
||||
timer = setTimeout(async () => {
|
||||
timer = null;
|
||||
let firstStall: StalledVideoDecoderInfo | null = null;
|
||||
try {
|
||||
const stats = await getStats();
|
||||
if (!stats) return;
|
||||
firstStall = findStalledVideoDecoderInStats(stats);
|
||||
const decoder = findSoftwareVideoDecoderInStats(stats);
|
||||
if (!decoder) return;
|
||||
logger.warn('Screen share is using a software decoder', decoder);
|
||||
SoftwareEncoderWarning.triggerDecoderWarning(decoder.codec, decoder.implementation);
|
||||
} catch (error) {
|
||||
logger.debug('Failed to verify screen share decoder', {error});
|
||||
} finally {
|
||||
if (!cancelled && firstStall) {
|
||||
scheduleConfirmation(firstStall);
|
||||
}
|
||||
}
|
||||
}, DECODER_VERIFICATION_DELAY_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (!timer) return;
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
}
|
||||
|
||||
export function findInboundVideoDecodeSample(stats: RTCStatsReport): InboundVideoDecodeSample | null {
|
||||
const {codecs, reports} = collectInboundVideoStats(stats);
|
||||
for (const report of reports) {
|
||||
@@ -338,7 +245,6 @@ export function monitorScreenShareDecodeHealth(
|
||||
getStats: () => Promise<RTCStatsReport | undefined>,
|
||||
onDecodeStall?: (failure: StalledVideoDecoderInfo) => void,
|
||||
): () => void {
|
||||
if (!ScreenShareDeliveryRollout.enabled) return scheduleScreenShareDecoderVerification(getStats, onDecodeStall);
|
||||
let cancelled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
let previousSample: InboundVideoDecodeSample | null = null;
|
||||
|
||||
@@ -8,14 +8,7 @@ import {
|
||||
resolveScreenShareLayering,
|
||||
resolveScreenShareTarget,
|
||||
} from '@app/features/voice/utils/ScreenShareOptions';
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const rollout = vi.hoisted(() => ({enabled: false}));
|
||||
|
||||
vi.mock('@app/features/voice/state/ScreenShareDeliveryRollout', () => ({
|
||||
ScreenShareDeliveryRollout: rollout,
|
||||
default: rollout,
|
||||
}));
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/utils/NativeAudioCaptureBridge', () => ({
|
||||
rememberCapturedDisplayAudioTrack: () => undefined,
|
||||
@@ -29,10 +22,6 @@ const {getDisplayMediaOptions} = await import(
|
||||
'@app/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture'
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
rollout.enabled = false;
|
||||
});
|
||||
|
||||
function collectKeys(value: unknown, keys: Set<string>): Set<string> {
|
||||
if (typeof value !== 'object' || value === null) return keys;
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
@@ -87,29 +76,19 @@ describe('display capture constraints', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('the screen share delivery experiment', () => {
|
||||
it('keeps the source preset and the 90 and 120 FPS rungs off the experiment', () => {
|
||||
expect(targetOf()).toMatchObject({resolution: 'source', frameRate: 15});
|
||||
expect(resolveScreenShareFrameRate(120)).toBe(120);
|
||||
expect(resolveScreenShareFrameRate(90)).toBe(90);
|
||||
expect(resolveScreenShareFrameRate(60)).toBe(60);
|
||||
});
|
||||
|
||||
it('moves the preset to 1080p30 and lands the faster rungs on 60 FPS on the experiment', () => {
|
||||
rollout.enabled = true;
|
||||
describe('screen share quality', () => {
|
||||
it('uses the 1080p30 preset and lands the faster rungs on 60 FPS', () => {
|
||||
expect(targetOf()).toMatchObject({resolution: 'high', frameRate: 30});
|
||||
expect(resolveScreenShareFrameRate(120)).toBe(60);
|
||||
expect(resolveScreenShareFrameRate(90)).toBe(60);
|
||||
expect(resolveScreenShareFrameRate(60)).toBe(60);
|
||||
});
|
||||
|
||||
it('reads the rung table off the experiment and the pixel budget on it', () => {
|
||||
expect(getScreenShareBitrateBps('source', 60)).toBe(6_000_000);
|
||||
rollout.enabled = true;
|
||||
it('reads the bitrate off the pixel budget', () => {
|
||||
expect(getScreenShareBitrateBps('source', 60)).toBe(9_000_000);
|
||||
});
|
||||
|
||||
it('publishes the stored frame rate and the rung bitrate off the experiment', () => {
|
||||
it('publishes the resolved frame rate and the pixel budget bitrate', () => {
|
||||
const {publishOptions} = buildScreenShareOptions({
|
||||
resolution: 'source',
|
||||
frameRate: 90,
|
||||
@@ -118,26 +97,18 @@ describe('the screen share delivery experiment', () => {
|
||||
sourceDimensions: {width: 3840, height: 2160},
|
||||
});
|
||||
expect(publishOptions.screenShareEncoding).toEqual({
|
||||
maxBitrate: 6_000_000,
|
||||
maxFramerate: 90,
|
||||
maxBitrate: 9_000_000,
|
||||
maxFramerate: 60,
|
||||
priority: 'high',
|
||||
});
|
||||
expect(publishOptions.degradationPreference).toBe('maintain-resolution');
|
||||
});
|
||||
|
||||
it('holds the motion hint for every surface off the experiment and only for a camera on it', () => {
|
||||
expect(targetOf({mode: 'gaming'}).contentHint).toBe('motion');
|
||||
rollout.enabled = true;
|
||||
it('holds the motion hint only for a camera', () => {
|
||||
expect(targetOf({mode: 'gaming'}).contentHint).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores the software H.264 clamp off the experiment', () => {
|
||||
expect(targetOf({mode: 'gaming', softwareEncoderClamp: true})).toMatchObject({
|
||||
resolution: 'ultra',
|
||||
frameRate: 60,
|
||||
softwareEncoderClamped: false,
|
||||
});
|
||||
rollout.enabled = true;
|
||||
it('applies the software H.264 clamp', () => {
|
||||
expect(targetOf({mode: 'gaming', softwareEncoderClamp: true})).toMatchObject({
|
||||
resolution: 'medium',
|
||||
frameRate: 30,
|
||||
@@ -147,15 +118,7 @@ describe('the screen share delivery experiment', () => {
|
||||
});
|
||||
|
||||
describe('screen share degradation preference', () => {
|
||||
it('holds the resolution for every share off the experiment', () => {
|
||||
expect(
|
||||
resolveScreenShareDegradationPreference({
|
||||
context: 'display',
|
||||
rung: 'medium',
|
||||
contentHint: undefined,
|
||||
maxBitrate: 3_000_000,
|
||||
}),
|
||||
).toBe('maintain-resolution');
|
||||
it('keeps device shares balanced', () => {
|
||||
expect(
|
||||
resolveScreenShareDegradationPreference({
|
||||
context: 'device',
|
||||
@@ -167,7 +130,6 @@ describe('screen share degradation preference', () => {
|
||||
});
|
||||
|
||||
it('refuses maintain-framerate below the initial frame dropper cliff', () => {
|
||||
rollout.enabled = true;
|
||||
expect(
|
||||
resolveScreenShareDegradationPreference({
|
||||
context: 'display',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import type {ScreenshareResolution, StreamingMode} from '@app/features/voice/state/VoiceSettings';
|
||||
import type {
|
||||
ScreenShareContentHint,
|
||||
@@ -23,8 +22,7 @@ const DIMENSIONS: Record<
|
||||
ultra: {width: 2560, height: 1440},
|
||||
source: {width: 3840, height: 2160},
|
||||
};
|
||||
export const SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS = 6_000_000;
|
||||
export const SCREEN_SHARE_DELIVERY_MAX_VIDEO_BITRATE_BPS = 9_000_000;
|
||||
export const SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS = 9_000_000;
|
||||
export const SUPPORTED_SCREEN_SHARE_FRAME_RATES = [15, 30, 60, 90, 120] as const;
|
||||
|
||||
export type SupportedScreenShareFrameRate = (typeof SUPPORTED_SCREEN_SHARE_FRAME_RATES)[number];
|
||||
@@ -52,15 +50,7 @@ const BITRATE_RUNGS = [
|
||||
'source',
|
||||
] as const satisfies ReadonlyArray<ScreenshareResolution>;
|
||||
|
||||
function isScreenShareDeliveryEnabled(delivery: boolean | undefined): boolean {
|
||||
return delivery ?? ScreenShareDeliveryRollout.enabled;
|
||||
}
|
||||
|
||||
export function resolveScreenShareFrameRate(frameRate: number, delivery?: boolean): SupportedScreenShareFrameRate {
|
||||
if (!isScreenShareDeliveryEnabled(delivery)) {
|
||||
if (frameRate >= 120) return 120;
|
||||
if (frameRate >= 90) return 90;
|
||||
}
|
||||
export function resolveScreenShareFrameRate(frameRate: number): SupportedScreenShareFrameRate {
|
||||
if (frameRate >= 60) return 60;
|
||||
if (frameRate >= 30) return 30;
|
||||
return 15;
|
||||
@@ -83,12 +73,10 @@ function computeScreenShareBitrateBps(
|
||||
pixels: number,
|
||||
rung: ScreenshareResolution,
|
||||
frameRate: SupportedScreenShareFrameRate,
|
||||
delivery: boolean,
|
||||
): number {
|
||||
const rungFloor = BITRATE_KBPS[rung][frameRate] * 1000;
|
||||
if (!delivery) return rungFloor;
|
||||
const computed = Math.round(SCREEN_SHARE_BITS_PER_PIXEL_PER_FRAME * pixels * frameRate);
|
||||
return Math.min(SCREEN_SHARE_DELIVERY_MAX_VIDEO_BITRATE_BPS, Math.max(rungFloor, computed));
|
||||
return Math.min(SCREEN_SHARE_MAX_VIDEO_BITRATE_BPS, Math.max(rungFloor, computed));
|
||||
}
|
||||
|
||||
export function getScreenShareBitrateBps(
|
||||
@@ -98,15 +86,9 @@ export function getScreenShareBitrateBps(
|
||||
width: number;
|
||||
height: number;
|
||||
} | null,
|
||||
delivery?: boolean,
|
||||
): number {
|
||||
const fit = resolveEffectiveScreenShareDimensions(resolution, sourceDimensions);
|
||||
return computeScreenShareBitrateBps(
|
||||
fit.width * fit.height,
|
||||
fit.rung,
|
||||
frameRate,
|
||||
isScreenShareDeliveryEnabled(delivery),
|
||||
);
|
||||
return computeScreenShareBitrateBps(fit.width * fit.height, fit.rung, frameRate);
|
||||
}
|
||||
|
||||
export const STREAMING_MODE_PRESETS: Record<
|
||||
@@ -115,16 +97,6 @@ export const STREAMING_MODE_PRESETS: Record<
|
||||
resolution: ScreenshareResolution;
|
||||
frameRate: SupportedScreenShareFrameRate;
|
||||
}
|
||||
> = {
|
||||
gaming: {resolution: 'ultra', frameRate: 60},
|
||||
screenshare: {resolution: 'source', frameRate: 15},
|
||||
};
|
||||
const SCREEN_SHARE_DELIVERY_STREAMING_MODE_PRESETS: Record<
|
||||
Exclude<StreamingMode, 'custom'>,
|
||||
{
|
||||
resolution: ScreenshareResolution;
|
||||
frameRate: SupportedScreenShareFrameRate;
|
||||
}
|
||||
> = {
|
||||
gaming: {resolution: 'ultra', frameRate: 60},
|
||||
screenshare: {resolution: 'high', frameRate: 30},
|
||||
@@ -152,7 +124,6 @@ export interface ScreenShareBuildConfig {
|
||||
includeAudio: boolean;
|
||||
contentHint?: ScreenShareCaptureOptions['contentHint'];
|
||||
degradationPreference?: NonNullable<TrackPublishOptions['degradationPreference']>;
|
||||
delivery?: boolean;
|
||||
sourceDimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -208,10 +179,9 @@ export function resolveEffectiveScreenShareDimensions(
|
||||
}
|
||||
|
||||
export function buildScreenShareOptions(config: ScreenShareBuildConfig): BuiltScreenShareOptions {
|
||||
const delivery = isScreenShareDeliveryEnabled(config.delivery);
|
||||
const {width, height, rung} = resolveEffectiveScreenShareDimensions(config.resolution, config.sourceDimensions);
|
||||
const resolvedFrameRate = resolveScreenShareFrameRate(config.frameRate, delivery);
|
||||
const maxBitrate = computeScreenShareBitrateBps(width * height, rung, resolvedFrameRate, delivery);
|
||||
const resolvedFrameRate = resolveScreenShareFrameRate(config.frameRate);
|
||||
const maxBitrate = computeScreenShareBitrateBps(width * height, rung, resolvedFrameRate);
|
||||
const video: ScreenShareVideoOptions = {
|
||||
cursor: resolveScreenShareCursorCapture(config.preferredDisplaySurface),
|
||||
...(config.preferredDisplaySurface ? {displaySurface: config.preferredDisplaySurface} : {}),
|
||||
@@ -235,7 +205,6 @@ export function buildScreenShareOptions(config: ScreenShareBuildConfig): BuiltSc
|
||||
contentHint: config.contentHint,
|
||||
maxBitrate,
|
||||
degradationPreference: config.degradationPreference,
|
||||
delivery,
|
||||
}),
|
||||
screenShareEncoding: {maxBitrate, maxFramerate: resolvedFrameRate, priority: 'high'},
|
||||
},
|
||||
@@ -270,17 +239,14 @@ export function resolveStreamingModeSettings(
|
||||
customResolution: ScreenshareResolution,
|
||||
customFrameRate: number,
|
||||
hasHigherQuality: boolean,
|
||||
delivery?: boolean,
|
||||
): {
|
||||
resolution: ScreenshareResolution;
|
||||
frameRate: SupportedScreenShareFrameRate;
|
||||
} {
|
||||
const enabled = isScreenShareDeliveryEnabled(delivery);
|
||||
const presets = enabled ? SCREEN_SHARE_DELIVERY_STREAMING_MODE_PRESETS : STREAMING_MODE_PRESETS;
|
||||
const resolved =
|
||||
mode === 'custom'
|
||||
? {resolution: customResolution, frameRate: resolveScreenShareFrameRate(customFrameRate, enabled)}
|
||||
: (hasHigherQuality ? presets : FREE_STREAMING_MODE_PRESETS)[mode];
|
||||
? {resolution: customResolution, frameRate: resolveScreenShareFrameRate(customFrameRate)}
|
||||
: (hasHigherQuality ? STREAMING_MODE_PRESETS : FREE_STREAMING_MODE_PRESETS)[mode];
|
||||
if (hasHigherQuality) {
|
||||
return resolved;
|
||||
}
|
||||
@@ -319,7 +285,6 @@ export interface ScreenShareQualityInput {
|
||||
storedFrameRate: number;
|
||||
entitled: boolean;
|
||||
context: ScreenShareContext;
|
||||
delivery?: boolean;
|
||||
}
|
||||
|
||||
export interface ScreenShareTargetInput extends ScreenShareQualityInput {
|
||||
@@ -341,7 +306,6 @@ export interface ScreenShareTarget {
|
||||
contentHint: ScreenShareCaptureOptions['contentHint'];
|
||||
degradationPreference: NonNullable<TrackPublishOptions['degradationPreference']>;
|
||||
softwareEncoderClamped: boolean;
|
||||
delivery?: boolean;
|
||||
presetOwned: boolean;
|
||||
tierLimited: boolean;
|
||||
deviceMapped: boolean;
|
||||
@@ -353,15 +317,11 @@ export interface ScreenShareDegradationInput {
|
||||
contentHint: ScreenShareCaptureOptions['contentHint'];
|
||||
maxBitrate: number;
|
||||
degradationPreference?: NonNullable<TrackPublishOptions['degradationPreference']>;
|
||||
delivery?: boolean;
|
||||
}
|
||||
|
||||
export function resolveScreenShareDegradationPreference(
|
||||
target: ScreenShareDegradationInput,
|
||||
): NonNullable<TrackPublishOptions['degradationPreference']> {
|
||||
if (!isScreenShareDeliveryEnabled(target.delivery)) {
|
||||
return target.context === 'device' ? 'balanced' : 'maintain-resolution';
|
||||
}
|
||||
if (target.degradationPreference) return target.degradationPreference;
|
||||
if (target.context === 'device') return 'balanced';
|
||||
if (target.rung === 'source') return 'maintain-resolution';
|
||||
@@ -393,17 +353,14 @@ function clampToSoftwareH264Budget(quality: {
|
||||
return {mode: quality.mode, resolution: cappedResolution, frameRate: cappedFrameRate};
|
||||
}
|
||||
|
||||
function resolveEffectiveScreenShareQuality(
|
||||
input: ScreenShareQualityInput,
|
||||
delivery: boolean,
|
||||
): {
|
||||
function resolveEffectiveScreenShareQuality(input: ScreenShareQualityInput): {
|
||||
mode: StreamingMode;
|
||||
resolution: ScreenshareResolution;
|
||||
frameRate: SupportedScreenShareFrameRate;
|
||||
} {
|
||||
const mode = normaliseStreamingModeForContext(input.mode, input.context);
|
||||
const resolution = normaliseResolutionForContext(input.storedResolution, input.context, input.entitled);
|
||||
const settings = resolveStreamingModeSettings(mode, resolution, input.storedFrameRate, input.entitled, delivery);
|
||||
const settings = resolveStreamingModeSettings(mode, resolution, input.storedFrameRate, input.entitled);
|
||||
return {mode, resolution: settings.resolution, frameRate: settings.frameRate};
|
||||
}
|
||||
|
||||
@@ -420,23 +377,20 @@ function resolveScreenShareContentHint(
|
||||
mode: StreamingMode,
|
||||
hintSetting: ScreenShareContentHint,
|
||||
context: ScreenShareContext,
|
||||
delivery: boolean,
|
||||
): ScreenShareCaptureOptions['contentHint'] {
|
||||
const hint = resolveScreenShareContentHintForMode(mode, hintSetting);
|
||||
if (!delivery) return hint;
|
||||
return hint === 'motion' && context !== 'device' ? undefined : hint;
|
||||
}
|
||||
|
||||
export function resolveScreenShareTarget(input: ScreenShareTargetInput): ScreenShareTarget {
|
||||
const delivery = isScreenShareDeliveryEnabled(input.delivery);
|
||||
const clamped = delivery && (input.softwareEncoderClamp === true || shouldClampToSoftwareH264(input.codec));
|
||||
const configured = resolveEffectiveScreenShareQuality(input, delivery);
|
||||
const clamped = input.softwareEncoderClamp === true || shouldClampToSoftwareH264(input.codec);
|
||||
const configured = resolveEffectiveScreenShareQuality(input);
|
||||
const effective = clamped ? clampToSoftwareH264Budget(configured) : configured;
|
||||
const configuredOnDisplay = resolveEffectiveScreenShareQuality({...input, context: 'display'}, delivery);
|
||||
const configuredOnDisplay = resolveEffectiveScreenShareQuality({...input, context: 'display'});
|
||||
const onDisplay = clamped ? clampToSoftwareH264Budget(configuredOnDisplay) : configuredOnDisplay;
|
||||
const fit = resolveEffectiveScreenShareDimensions(effective.resolution, input.sourceDimensions);
|
||||
const maxBitrate = computeScreenShareBitrateBps(fit.width * fit.height, fit.rung, effective.frameRate, delivery);
|
||||
const contentHint = resolveScreenShareContentHint(effective.mode, input.hintSetting, input.context, delivery);
|
||||
const maxBitrate = computeScreenShareBitrateBps(fit.width * fit.height, fit.rung, effective.frameRate);
|
||||
const contentHint = resolveScreenShareContentHint(effective.mode, input.hintSetting, input.context);
|
||||
return {
|
||||
mode: effective.mode,
|
||||
resolution: effective.resolution,
|
||||
@@ -452,16 +406,14 @@ export function resolveScreenShareTarget(input: ScreenShareTargetInput): ScreenS
|
||||
rung: fit.rung,
|
||||
contentHint,
|
||||
maxBitrate,
|
||||
delivery,
|
||||
}),
|
||||
softwareEncoderClamped: clamped,
|
||||
delivery,
|
||||
presetOwned: effective.mode !== 'custom',
|
||||
tierLimited:
|
||||
!input.entitled &&
|
||||
input.mode === 'custom' &&
|
||||
(PREMIUM_SCREEN_SHARE_RESOLUTIONS.includes(input.storedResolution) ||
|
||||
resolveScreenShareFrameRate(input.storedFrameRate, delivery) > FREE_TIER_MAX_FRAME_RATE),
|
||||
resolveScreenShareFrameRate(input.storedFrameRate) > FREE_TIER_MAX_FRAME_RATE),
|
||||
deviceMapped:
|
||||
effective.mode !== onDisplay.mode ||
|
||||
effective.resolution !== onDisplay.resolution ||
|
||||
@@ -570,13 +522,12 @@ export function resolveScreenShareSenderCodec(
|
||||
}
|
||||
|
||||
export function buildScreenShareSenderParameters(input: ScreenShareSenderParametersInput): ScreenShareSenderParameters {
|
||||
const delivery = input.target.delivery === true;
|
||||
const degradationPreference = resolveScreenShareDegradationPreference(input.target);
|
||||
const targetPixels = input.target.width * input.target.height;
|
||||
const capturePixels = input.capture ? input.capture.width * input.capture.height : null;
|
||||
const baseScale = Math.min(...input.encodings.map((encoding) => encoding.scaleResolutionDownBy ?? 1));
|
||||
const targetScale =
|
||||
capturePixels === null || (delivery && degradationPreference === 'maintain-framerate')
|
||||
capturePixels === null || degradationPreference === 'maintain-framerate'
|
||||
? 1
|
||||
: Math.max(1, Math.sqrt(capturePixels / targetPixels));
|
||||
const sentPixels = capturePixels === null ? null : capturePixels / (targetScale * targetScale);
|
||||
@@ -584,12 +535,7 @@ export function buildScreenShareSenderParameters(input: ScreenShareSenderParamet
|
||||
sentPixels !== null && sentPixels < targetPixels * SENT_PIXEL_FILL_RATIO
|
||||
? Math.min(
|
||||
input.target.maxBitrate,
|
||||
computeScreenShareBitrateBps(
|
||||
sentPixels,
|
||||
resolveBitrateRungForPixels(sentPixels),
|
||||
input.target.frameRate,
|
||||
delivery,
|
||||
),
|
||||
computeScreenShareBitrateBps(sentPixels, resolveBitrateRungForPixels(sentPixels), input.target.frameRate),
|
||||
)
|
||||
: input.target.maxBitrate;
|
||||
const bitrates = distributeScreenShareBitrate(input.encodings, maxBitrate);
|
||||
@@ -628,9 +574,8 @@ export function resolveScreenShareQualityPick(
|
||||
input: ScreenShareQualityInput,
|
||||
pick: ScreenShareQualityPick,
|
||||
): ScreenShareQualityPatch | null {
|
||||
const delivery = isScreenShareDeliveryEnabled(input.delivery);
|
||||
const effective = resolveEffectiveScreenShareQuality(input, delivery);
|
||||
const stored = resolveEffectiveScreenShareQuality({...input, mode: 'custom'}, delivery);
|
||||
const effective = resolveEffectiveScreenShareQuality(input);
|
||||
const stored = resolveEffectiveScreenShareQuality({...input, mode: 'custom'});
|
||||
if (pick.axis === 'resolution') {
|
||||
if (normaliseResolutionForContext(pick.resolution, input.context, input.entitled) !== pick.resolution) return null;
|
||||
if (pick.resolution === effective.resolution) return null;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {resolveConfiguredScreenShareTarget} from '@app/features/voice/engine/voi
|
||||
import ActiveScreenShareSource from '@app/features/voice/state/ActiveScreenShareSource';
|
||||
import {clearDesktopSourceIntent, setDesktopSourceIntent} from '@app/features/voice/state/DesktopSourceIntent';
|
||||
import LocalVoiceState from '@app/features/voice/state/LocalVoiceState';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {
|
||||
filterRoutableLinuxAudioSources,
|
||||
@@ -806,7 +805,6 @@ export async function switchConfiguredDisplayScreenShare(
|
||||
}
|
||||
|
||||
export async function restartActiveScreenShareCapture(): Promise<boolean> {
|
||||
if (!ScreenShareDeliveryRollout.enabled) return false;
|
||||
if (!didScreenShareStart()) return false;
|
||||
const publishedSource = ActiveScreenShareSource.getPublishedSource();
|
||||
const sourceId = ActiveScreenShareSource.getSourceId();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import ScreenShareDeliveryRollout from '@app/features/voice/state/ScreenShareDeliveryRollout';
|
||||
import type {VideoCodec} from 'livekit-client';
|
||||
|
||||
const logger = new Logger('VideoDecoderCapabilities');
|
||||
@@ -150,7 +149,7 @@ export function getVideoDecoderExclusionsSync(): Array<VideoCodec> | null {
|
||||
}
|
||||
|
||||
export function markScreenShareDecodeFailure(codec: VideoCodec, reason: string): boolean {
|
||||
if (ScreenShareDeliveryRollout.enabled && isBaselineWebRtcDecodeCodec(codec)) {
|
||||
if (isBaselineWebRtcDecodeCodec(codec)) {
|
||||
logger.warn('Keeping a baseline codec advertised despite a local screen share decode stall', {codec, reason});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ Missing settings use the defaults documented below. Invalid stored configuration
|
||||
| sso | [SSO configuration](#sso-configuration-object) object | Single sign-on settings |
|
||||
| gateway_rollout | [Gateway rollout configuration](#gateway-rollout-configuration-object) object | Gateway admission and dispatch tuning |
|
||||
| voice_noise_suppression | [voice noise suppression configuration](#voice-noise-suppression-configuration-object) object | Client-side noise suppression rollout |
|
||||
| screen_share_delivery | [screen share delivery configuration](#screen-share-delivery-configuration-object) object | Reworked screen share delivery rollout |
|
||||
| push_service_delivery | [push service delivery configuration](#push-service-delivery-configuration-object) object | Push service delivery rollout |
|
||||
| experiment_delivery | [experiment delivery configuration](#experiment-delivery-configuration-object) object | Cadence every client polls the experiments route on |
|
||||
| registration | [registration configuration](#registration-configuration-object) object | Registration policy, issued URLs, and pending registrations |
|
||||
@@ -122,27 +121,6 @@ Every field is present on read. An absent document or missing field uses the def
|
||||
|
||||
How often a client revalidates this rollout is not set here. It is set once for every experiment in the [experiment delivery configuration](#experiment-delivery-configuration-object) below.
|
||||
|
||||
## Screen share delivery configuration object
|
||||
|
||||
The instance rollout of the reworked screen share delivery path. [Experiments](/http-api/experiments/) defines what a client resolves from it.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the rollout runs at all (default false) |
|
||||
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
|
||||
| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
|
||||
| rollout_salt | string | Salt of the sampling hash (1-64 characters, default `screen-share-delivery-v1`) |
|
||||
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
|
||||
| excluded_user_ids | array[snowflake] | Accounts the rollout never selects, up to 1000 entries (default empty) |
|
||||
|
||||
Every field is present on read. An absent document or missing field uses the defaults above.
|
||||
|
||||
`excluded_user_ids` is applied before `included_user_ids`, so the rollout never selects an account in both.
|
||||
|
||||
How often a client revalidates this rollout is not set here. It is set once for every experiment in the [experiment delivery configuration](#experiment-delivery-configuration-object) below.
|
||||
|
||||
## Push service delivery configuration object
|
||||
|
||||
The instance rollout of push service delivery.
|
||||
@@ -568,7 +546,6 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
| sso?<sup>1</sup> | object | Every [SSO configuration](#sso-configuration-object) field except `client_secret_set` and `redirect_uri`, plus `client_secret` |
|
||||
| gateway_rollout? | object | Any subset of the [Gateway rollout configuration](#gateway-rollout-configuration-object) fields, each bound as documented there |
|
||||
| voice_noise_suppression? | object | Any subset of the [noise suppression](#voice-noise-suppression-configuration-object) fields |
|
||||
| screen_share_delivery? | object | Any subset of the [screen share delivery](#screen-share-delivery-configuration-object) fields |
|
||||
| push_service_delivery? | object | Any subset of the [push service delivery](#push-service-delivery-configuration-object) fields |
|
||||
| experiment_delivery? | object | Any subset of the [experiment delivery](#experiment-delivery-configuration-object) fields |
|
||||
| registration? | object | `mode` and `admin_registration_urls_enabled` |
|
||||
@@ -583,8 +560,6 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
|
||||
`voice_noise_suppression` takes every [voice noise suppression configuration](#voice-noise-suppression-configuration-object) field except `config_version`, each bound as documented there. Fluxer raises `config_version` by one on each request that supplies at least one of them. A section that is absent, or present with no field set, writes nothing and leaves `config_version` alone.
|
||||
|
||||
`screen_share_delivery` works the same way, over the [screen share delivery configuration](#screen-share-delivery-configuration-object) fields and its own `config_version`.
|
||||
|
||||
`push_service_delivery` works the same way, over the [push service delivery configuration](#push-service-delivery-configuration-object) fields and its own `config_version`.
|
||||
|
||||
`experiment_delivery` takes both [experiment delivery configuration](#experiment-delivery-configuration-object) fields, each bound as documented there. It is a section of its own, so a write to it changes no `config_version` and changes no assignment, only the cadence on which clients ask for one.
|
||||
@@ -623,7 +598,7 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on
|
||||
| 400 | [error response](/admin-api/#error-response) | A policy transition is refused, returned as `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` |
|
||||
|
||||
:::caution[Sections are applied one after another]
|
||||
The order is `gateway_rollout`, `voice_noise_suppression`, `screen_share_delivery`, `push_service_delivery`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
|
||||
The order is `gateway_rollout`, `voice_noise_suppression`, `push_service_delivery`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
|
||||
:::
|
||||
|
||||
### Side effects
|
||||
|
||||
@@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `voice_noise_suppression`, whose placement protocol [Voice](/voice/) defines, and `screen_share_delivery`.
|
||||
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `voice_noise_suppression`, whose placement protocol [Voice](/voice/) defines.
|
||||
|
||||
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
|
||||
|
||||
@@ -33,7 +33,6 @@ One entry per experiment. The envelope reports this object even when it is empty
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| voice_noise_suppression? | [noise suppression assignment](#noise-suppression-assignment-object) object | The caller's noise suppression assignment |
|
||||
| screen_share_delivery? | [screen share delivery assignment](#screen-share-delivery-assignment-object) object | The caller's screen share delivery assignment |
|
||||
|
||||
Ignore unknown experiments and treat a missing experiment as off.
|
||||
|
||||
@@ -102,18 +101,6 @@ One backend replacement scoped to one guild. While the caller is connected to a
|
||||
|
||||
An override naming a backend that is absent from `enabled_backends` is dropped before the response is written, so every entry is runnable.
|
||||
|
||||
## Screen share delivery assignment object
|
||||
|
||||
One resolution of the instance screen share delivery rollout against one account. The field is present whenever the key is written.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the caller runs the reworked screen share delivery path |
|
||||
|
||||
A caller is drawn either by the operator's allowlist or by the sampled share of the account population. `enabled` is false in every other case, the rollout being off included.
|
||||
|
||||
## Get experiment assignments
|
||||
|
||||
<RouteHeader method="GET" path="/v1/experiments" bot />
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"fluxer_desktop/src/main/NotificationState.ts": ["exports", "types"],
|
||||
"packages/schema/src/domains/admin/AdminUserSchemas.ts": ["exports"],
|
||||
"packages/schema/src/domains/admin/PushServiceDeliverySchemas.ts": ["exports"],
|
||||
"packages/schema/src/domains/admin/ScreenShareDeliverySchemas.ts": ["exports"],
|
||||
"packages/schema/src/domains/admin/VoiceNoiseSuppressionSchemas.ts": ["exports"],
|
||||
"packages/schema/src/domains/download/DownloadSchemas.ts": ["exports"],
|
||||
"packages/schema/src/domains/geolocation/GeolocationSchemas.ts": ["exports"],
|
||||
|
||||
@@ -19,10 +19,6 @@ import {
|
||||
PushServiceDeliveryConfigResponse,
|
||||
PushServiceDeliveryConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/PushServiceDeliverySchemas';
|
||||
import {
|
||||
ScreenShareDeliveryConfigResponse,
|
||||
ScreenShareDeliveryConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {
|
||||
VoiceNoiseSuppressionConfigResponse,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
@@ -652,7 +648,6 @@ export const InstanceConfigResponse = z.object({
|
||||
sso: SsoConfigResponse,
|
||||
gateway_rollout: GatewayRolloutConfigResponse,
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
screen_share_delivery: ScreenShareDeliveryConfigResponse,
|
||||
push_service_delivery: PushServiceDeliveryConfigResponse,
|
||||
experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
registration: InstanceRegistrationResponse,
|
||||
@@ -690,7 +685,6 @@ const InstancePolicyUpdateSchema = z.object({
|
||||
export const InstanceConfigUpdateRequest = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(),
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigUpdateRequest.nullish(),
|
||||
screen_share_delivery: ScreenShareDeliveryConfigUpdateRequest.nullish(),
|
||||
push_service_delivery: PushServiceDeliveryConfigUpdateRequest.nullish(),
|
||||
experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(),
|
||||
registration: z
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT,
|
||||
resolveScreenShareDeliveryAssignment,
|
||||
type ScreenShareDeliveryConfig,
|
||||
ScreenShareDeliveryConfigSchema,
|
||||
ScreenShareDeliveryConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
|
||||
function createConfig(overrides: Partial<ScreenShareDeliveryConfig> = {}): ScreenShareDeliveryConfig {
|
||||
return {
|
||||
...DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG,
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function syntheticUserIds(count: number): Array<string> {
|
||||
const ids: Array<string> = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
ids.push((1400000000000000000n + BigInt(index)).toString());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function targetedUserIds(config: ScreenShareDeliveryConfig, userIds: ReadonlyArray<string>): Set<string> {
|
||||
const targeted = new Set<string>();
|
||||
for (const userId of userIds) {
|
||||
if (resolveScreenShareDeliveryAssignment(config, userId).enabled) {
|
||||
targeted.add(userId);
|
||||
}
|
||||
}
|
||||
return targeted;
|
||||
}
|
||||
|
||||
describe('screen share delivery configuration', () => {
|
||||
test('derives defaults from the schema with independently owned arrays', () => {
|
||||
const first = ScreenShareDeliveryConfigSchema.parse({});
|
||||
const second = ScreenShareDeliveryConfigSchema.parse({});
|
||||
expect(first).toEqual(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG);
|
||||
first.included_user_ids.push(TARGETED_USER_ID);
|
||||
first.excluded_user_ids.push(OTHER_USER_ID);
|
||||
expect(second).toEqual(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG);
|
||||
});
|
||||
|
||||
test('defaults to disabled with an empty rollout', () => {
|
||||
expect(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG.enabled).toBe(false);
|
||||
expect(DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG.rollout_basis_points).toBe(0);
|
||||
});
|
||||
|
||||
test.each([{}, {enabled: false}, {enabled: undefined}, {rollout_basis_points: 2500}])(
|
||||
'keeps partial updates free of configuration defaults: %j',
|
||||
(patch) => {
|
||||
expect(ScreenShareDeliveryConfigUpdateRequest.parse(patch)).toEqual(patch);
|
||||
},
|
||||
);
|
||||
|
||||
test('does not accept a client-provided configuration version', () => {
|
||||
expect(ScreenShareDeliveryConfigUpdateRequest.parse({config_version: 12})).toEqual({});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{rollout_basis_points: -1},
|
||||
{rollout_basis_points: 10001},
|
||||
{rollout_salt: ' '},
|
||||
{included_user_ids: ['not-an-id']},
|
||||
{excluded_user_ids: ['not-an-id']},
|
||||
])('applies the same validation to stored configuration and updates: %j', (value) => {
|
||||
expect(ScreenShareDeliveryConfigSchema.safeParse(value).success).toBe(false);
|
||||
expect(ScreenShareDeliveryConfigUpdateRequest.safeParse(value).success).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects more than a thousand targeted user ids', () => {
|
||||
const ids = syntheticUserIds(1001);
|
||||
expect(ScreenShareDeliveryConfigSchema.safeParse({included_user_ids: ids}).success).toBe(false);
|
||||
expect(ScreenShareDeliveryConfigSchema.safeParse({included_user_ids: ids.slice(0, 1000)}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveScreenShareDeliveryAssignment', () => {
|
||||
test('the inert assignment is disabled', () => {
|
||||
expect(INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('returns the inert assignment when the master switch is off', () => {
|
||||
const config = createConfig({
|
||||
enabled: false,
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveScreenShareDeliveryAssignment(config, TARGETED_USER_ID)).toEqual(
|
||||
INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns the inert assignment for the default config', () => {
|
||||
expect(resolveScreenShareDeliveryAssignment(createConfig(), TARGETED_USER_ID)).toEqual(
|
||||
INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
|
||||
test('never hands back the shared inert object', () => {
|
||||
const assignment = resolveScreenShareDeliveryAssignment(createConfig(), TARGETED_USER_ID);
|
||||
expect(assignment).not.toBe(INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT);
|
||||
});
|
||||
|
||||
test('denylist beats allowlist', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveScreenShareDeliveryAssignment(config, TARGETED_USER_ID).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('denylist beats the bucket', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveScreenShareDeliveryAssignment(config, TARGETED_USER_ID).enabled).toBe(false);
|
||||
expect(resolveScreenShareDeliveryAssignment(config, OTHER_USER_ID).enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('allowlist beats the bucket', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
rollout_basis_points: 0,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveScreenShareDeliveryAssignment(config, TARGETED_USER_ID).enabled).toBe(true);
|
||||
expect(resolveScreenShareDeliveryAssignment(config, OTHER_USER_ID).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{basisPoints: 0, enabled: false},
|
||||
{basisPoints: 10000, enabled: true},
|
||||
])('a rollout of $basisPoints basis points targets $enabled', ({basisPoints, enabled}) => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: basisPoints});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
expect(resolveScreenShareDeliveryAssignment(config, userId).enabled).toBe(enabled);
|
||||
}
|
||||
});
|
||||
|
||||
test('the bucket boundary is exclusive at the low end and inclusive one point above', () => {
|
||||
const salt = DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG.rollout_salt;
|
||||
const bucket = experimentBucket(TARGETED_USER_ID, salt);
|
||||
expect(
|
||||
resolveScreenShareDeliveryAssignment(
|
||||
createConfig({enabled: true, rollout_basis_points: bucket}),
|
||||
TARGETED_USER_ID,
|
||||
).enabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveScreenShareDeliveryAssignment(
|
||||
createConfig({enabled: true, rollout_basis_points: bucket + 1}),
|
||||
TARGETED_USER_ID,
|
||||
).enabled,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('raising the rollout basis points only ever adds users', () => {
|
||||
const userIds = syntheticUserIds(2000);
|
||||
const atOneThousand = targetedUserIds(createConfig({enabled: true, rollout_basis_points: 1000}), userIds);
|
||||
const atTwoThousand = targetedUserIds(createConfig({enabled: true, rollout_basis_points: 2000}), userIds);
|
||||
expect(atOneThousand.size).toBeGreaterThan(0);
|
||||
for (const userId of atOneThousand) {
|
||||
expect(atTwoThousand.has(userId)).toBe(true);
|
||||
}
|
||||
expect(atTwoThousand.size).toBeGreaterThan(atOneThousand.size);
|
||||
});
|
||||
|
||||
test('the targeted set follows the salt', () => {
|
||||
const userIds = syntheticUserIds(2000);
|
||||
const first = targetedUserIds(
|
||||
createConfig({enabled: true, rollout_basis_points: 5000, rollout_salt: 'screen-share-delivery-v1'}),
|
||||
userIds,
|
||||
);
|
||||
const second = targetedUserIds(
|
||||
createConfig({enabled: true, rollout_basis_points: 5000, rollout_salt: 'screen-share-delivery-v2'}),
|
||||
userIds,
|
||||
);
|
||||
expect(first).not.toEqual(second);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
const SCREEN_SHARE_DELIVERY_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
const SCREEN_SHARE_DELIVERY_MAX_TARGETED_USERS = 1000;
|
||||
const DEFAULT_SCREEN_SHARE_DELIVERY_SALT = 'screen-share-delivery-v1';
|
||||
|
||||
const ScreenShareDeliveryTargetIdSchema = z.string().regex(/^\d{1,20}$/u);
|
||||
const ScreenShareDeliveryTargetedUserIdsSchema = z
|
||||
.array(ScreenShareDeliveryTargetIdSchema)
|
||||
.max(SCREEN_SHARE_DELIVERY_MAX_TARGETED_USERS);
|
||||
|
||||
const screenShareDeliveryConfigFields = {
|
||||
enabled: z.boolean(),
|
||||
config_version: z.number().int().min(0),
|
||||
rollout_basis_points: z.number().int().min(0).max(SCREEN_SHARE_DELIVERY_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64),
|
||||
included_user_ids: ScreenShareDeliveryTargetedUserIdsSchema,
|
||||
excluded_user_ids: ScreenShareDeliveryTargetedUserIdsSchema,
|
||||
};
|
||||
|
||||
export const ScreenShareDeliveryConfigSchema = z.object({
|
||||
enabled: screenShareDeliveryConfigFields.enabled.default(false),
|
||||
config_version: screenShareDeliveryConfigFields.config_version.default(0),
|
||||
rollout_basis_points: screenShareDeliveryConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: screenShareDeliveryConfigFields.rollout_salt.default(DEFAULT_SCREEN_SHARE_DELIVERY_SALT),
|
||||
included_user_ids: screenShareDeliveryConfigFields.included_user_ids.default([]),
|
||||
excluded_user_ids: screenShareDeliveryConfigFields.excluded_user_ids.default([]),
|
||||
});
|
||||
|
||||
export type ScreenShareDeliveryConfig = z.infer<typeof ScreenShareDeliveryConfigSchema>;
|
||||
|
||||
export const DEFAULT_SCREEN_SHARE_DELIVERY_CONFIG: ScreenShareDeliveryConfig = ScreenShareDeliveryConfigSchema.parse(
|
||||
{},
|
||||
);
|
||||
|
||||
export const ScreenShareDeliveryConfigUpdateRequest = z
|
||||
.object(screenShareDeliveryConfigFields)
|
||||
.omit({config_version: true})
|
||||
.partial();
|
||||
|
||||
export type ScreenShareDeliveryConfigUpdateRequest = z.infer<typeof ScreenShareDeliveryConfigUpdateRequest>;
|
||||
|
||||
export const ScreenShareDeliveryConfigResponse = ScreenShareDeliveryConfigSchema;
|
||||
|
||||
export type ScreenShareDeliveryConfigResponse = z.infer<typeof ScreenShareDeliveryConfigResponse>;
|
||||
|
||||
export const ScreenShareDeliveryAssignmentResponse = z.object({
|
||||
enabled: screenShareDeliveryConfigFields.enabled,
|
||||
});
|
||||
|
||||
export type ScreenShareDeliveryAssignmentResponse = z.infer<typeof ScreenShareDeliveryAssignmentResponse>;
|
||||
|
||||
export const INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT: ScreenShareDeliveryAssignmentResponse = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export function resolveScreenShareDeliveryAssignment(
|
||||
config: ScreenShareDeliveryConfig,
|
||||
userId: string,
|
||||
): ScreenShareDeliveryAssignmentResponse {
|
||||
if (!config.enabled) return {...INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT};
|
||||
if (config.excluded_user_ids.includes(userId)) return {...INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT};
|
||||
if (config.included_user_ids.includes(userId)) return {enabled: true};
|
||||
return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points};
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT,
|
||||
ScreenShareDeliveryAssignmentResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
VoiceNoiseSuppressionAssignmentResponse,
|
||||
@@ -46,7 +42,6 @@ export type ExperimentDeliveryConfigResponse = z.infer<typeof ExperimentDelivery
|
||||
|
||||
const ExperimentAssignmentsSchema = z.object({
|
||||
voice_noise_suppression: VoiceNoiseSuppressionAssignmentResponse.optional(),
|
||||
screen_share_delivery: ScreenShareDeliveryAssignmentResponse.optional(),
|
||||
});
|
||||
|
||||
export const ExperimentAssignmentsResponse = z.object({
|
||||
@@ -68,9 +63,3 @@ export function readVoiceNoiseSuppressionAssignment(
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
return response.assignments.voice_noise_suppression ?? INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT;
|
||||
}
|
||||
|
||||
export function readScreenShareDeliveryAssignment(
|
||||
response: ExperimentAssignmentsResponse,
|
||||
): ScreenShareDeliveryAssignmentResponse {
|
||||
return response.assignments.screen_share_delivery ?? INERT_SCREEN_SHARE_DELIVERY_ASSIGNMENT;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user