Skip to content

Commit 252d2c0

Browse files
committed
fix(stream): pause when clients lose focus
1 parent 004af53 commit 252d2c0

5 files changed

Lines changed: 90 additions & 7 deletions

File tree

client/src/features/stream/useLiveStream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function currentClientBundle(): string {
100100
}
101101

102102
function isDocumentForeground(): boolean {
103-
return document.visibilityState === "visible";
103+
return document.visibilityState === "visible" && document.hasFocus();
104104
}
105105

106106
export function useLiveStream({

server/src/api/routes.rs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,10 +1372,47 @@ async fn record_client_stream_stats(
13721372
));
13731373
}
13741374

1375+
apply_stream_client_foreground_from_stats(&state, &payload);
13751376
state.metrics.record_client_stream_stats(payload);
13761377
Ok(json(json_value!({ "ok": true })))
13771378
}
13781379

1380+
pub(crate) fn apply_stream_client_foreground_from_stats(
1381+
state: &AppState,
1382+
stats: &ClientStreamStats,
1383+
) {
1384+
let Some(foreground) = client_stats_foreground(stats) else {
1385+
return;
1386+
};
1387+
let Some(udid) = stats
1388+
.udid
1389+
.as_deref()
1390+
.map(str::trim)
1391+
.filter(|value| !value.is_empty())
1392+
else {
1393+
return;
1394+
};
1395+
let client_id = stats.client_id.trim();
1396+
if client_id.is_empty() {
1397+
return;
1398+
}
1399+
let (any_foreground, changed) = state.stream_clients.record(udid, client_id, foreground);
1400+
if changed {
1401+
if let Some(session) = state.registry.get(udid) {
1402+
session.set_client_foreground(any_foreground);
1403+
}
1404+
}
1405+
}
1406+
1407+
fn client_stats_foreground(stats: &ClientStreamStats) -> Option<bool> {
1408+
if stats.kind != "page" {
1409+
return None;
1410+
}
1411+
let visible = stats.visibility_state.as_deref() == Some("visible");
1412+
let focused = stats.focused?;
1413+
Some(visible && focused)
1414+
}
1415+
13791416
async fn native_inspector_connect(
13801417
State(state): State<AppState>,
13811418
websocket: WebSocketUpgrade,
@@ -2558,6 +2595,7 @@ fn handle_h264_socket_message(
25582595
match message {
25592596
H264SocketMessage::ClientStats { stats } => {
25602597
if !stats.client_id.trim().is_empty() && !stats.kind.trim().is_empty() {
2598+
apply_stream_client_foreground_from_stats(state, &stats);
25612599
state.metrics.record_client_stream_stats(*stats);
25622600
}
25632601
}
@@ -6048,9 +6086,9 @@ async fn accessibility_snapshot(
60486086
mod tests {
60496087
use super::{
60506088
accessibility_point_snapshot, attach_tree_metadata, available_sources_for_snapshot,
6051-
best_inspector_session, compact_accessibility_snapshot, element_matches_selector,
6052-
first_matching_element, inspector_available_sources, inspector_metadata,
6053-
inspector_session_from_published, inspector_session_score,
6089+
best_inspector_session, client_stats_foreground, compact_accessibility_snapshot,
6090+
element_matches_selector, first_matching_element, inspector_available_sources,
6091+
inspector_metadata, inspector_session_from_published, inspector_session_score,
60546092
is_inspector_agent_transport_path, normalize_inspector_node,
60556093
normalize_screen_point_from_snapshot, normalized_gesture_coordinates,
60566094
parse_lsof_tcp_listener, parse_ui_application_service_line, resolved_stream_quality_limits,
@@ -6062,6 +6100,7 @@ mod tests {
60626100
SOURCE_NATIVE_SCRIPT, SOURCE_REACT_NATIVE, SOURCE_SWIFTUI, SOURCE_UIKIT,
60636101
};
60646102
use crate::inspector::PublishedInspector;
6103+
use crate::metrics::counters::ClientStreamStats;
60656104
use crate::transport::packet::FramePacket;
60666105
use bytes::Bytes;
60676106
use serde_json::{json, Value};
@@ -6101,6 +6140,45 @@ mod tests {
61016140
assert_eq!(registry.remove("udid", "hidden"), (false, false));
61026141
}
61036142

6143+
#[test]
6144+
fn client_stats_foreground_requires_visible_and_focused_page() {
6145+
let page_stats =
6146+
|visibility_state: Option<&str>, focused: Option<bool>| ClientStreamStats {
6147+
client_id: "client".to_owned(),
6148+
kind: "page".to_owned(),
6149+
visibility_state: visibility_state.map(ToOwned::to_owned),
6150+
focused,
6151+
..Default::default()
6152+
};
6153+
6154+
assert_eq!(
6155+
client_stats_foreground(&page_stats(Some("visible"), Some(true))),
6156+
Some(true)
6157+
);
6158+
assert_eq!(
6159+
client_stats_foreground(&page_stats(Some("visible"), Some(false))),
6160+
Some(false)
6161+
);
6162+
assert_eq!(
6163+
client_stats_foreground(&page_stats(Some("hidden"), Some(true))),
6164+
Some(false)
6165+
);
6166+
assert_eq!(
6167+
client_stats_foreground(&page_stats(Some("visible"), None)),
6168+
None
6169+
);
6170+
assert_eq!(
6171+
client_stats_foreground(&ClientStreamStats {
6172+
client_id: "client".to_owned(),
6173+
kind: "webrtc".to_owned(),
6174+
focused: Some(true),
6175+
visibility_state: Some("visible".to_owned()),
6176+
..Default::default()
6177+
}),
6178+
None
6179+
);
6180+
}
6181+
61046182
#[test]
61056183
fn selector_matching_uses_identifier_label_and_type_aliases() {
61066184
let snapshot = accessibility_snapshot();

server/src/metrics/counters.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub struct MetricsSnapshot {
3737
pub client_streams: Vec<ClientStreamStats>,
3838
}
3939

40-
#[derive(Clone, Debug, Deserialize, Serialize)]
40+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
4141
#[serde(rename_all = "camelCase")]
4242
pub struct ClientStreamStats {
4343
pub client_id: String,

server/src/simulators/registry.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ impl<T: Clone + Send + 'static> SessionRegistry<T> {
106106
})
107107
}
108108

109+
pub fn get(&self, udid: &str) -> Option<T> {
110+
self.store.get(udid)
111+
}
112+
109113
pub async fn get_or_create_async(&self, udid: &str) -> Result<T, AppError> {
110114
let registry = self.clone();
111115
let udid_owned = udid.to_owned();

server/src/transport/webrtc.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::android;
22
use crate::api::routes::{
3-
apply_stream_quality_payload, run_control_message, run_toggle_appearance_control, AppState,
4-
ControlMessage, StreamQualityPayload,
3+
apply_stream_client_foreground_from_stats, apply_stream_quality_payload, run_control_message,
4+
run_toggle_appearance_control, AppState, ControlMessage, StreamQualityPayload,
55
};
66
use crate::error::AppError;
77
use crate::metrics::counters::ClientStreamStats;
@@ -641,6 +641,7 @@ fn attach_control_data_channel(
641641
match message {
642642
WebRtcDataChannelMessage::ClientStats { stats } => {
643643
if !stats.client_id.trim().is_empty() && !stats.kind.trim().is_empty() {
644+
apply_stream_client_foreground_from_stats(&state, &stats);
644645
state.metrics.record_client_stream_stats(*stats);
645646
}
646647
}

0 commit comments

Comments
 (0)