Skip to content

Commit ad4681c

Browse files
committed
Adapt WebRTC streaming to latest frame
1 parent 3da10e7 commit ad4681c

9 files changed

Lines changed: 85 additions & 13 deletions

File tree

cli/native/XCWNativeBridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ void * _Nullable xcw_native_session_create(const char * _Nonnull udid, char * _N
7070
void xcw_native_session_destroy(void * _Nullable handle);
7171
bool xcw_native_session_start(void * _Nonnull handle, char * _Nullable * _Nullable error_message);
7272
void xcw_native_session_request_refresh(void * _Nonnull handle);
73+
void xcw_native_session_request_keyframe(void * _Nonnull handle);
7374
bool xcw_native_session_send_touch(void * _Nonnull handle, double x, double y, const char * _Nonnull phase, char * _Nullable * _Nullable error_message);
7475
bool xcw_native_session_send_multitouch(void * _Nonnull handle, double x1, double y1, double x2, double y2, const char * _Nonnull phase, char * _Nullable * _Nullable error_message);
7576
bool xcw_native_session_send_key(void * _Nonnull handle, uint16_t key_code, uint32_t modifiers, char * _Nullable * _Nullable error_message);

cli/native/XCWNativeBridge.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,12 @@ void xcw_native_session_request_refresh(void *handle) {
668668
}
669669
}
670670

671+
void xcw_native_session_request_keyframe(void *handle) {
672+
@autoreleasepool {
673+
[XCWNativeSessionFromHandle(handle) requestKeyFrame];
674+
}
675+
}
676+
671677
bool xcw_native_session_send_touch(void *handle, double x, double y, const char *phase, char **error_message) {
672678
@autoreleasepool {
673679
NSError *error = nil;

cli/native/XCWNativeSession.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
1313

1414
- (BOOL)start:(NSError * _Nullable * _Nullable)error;
1515
- (void)requestRefresh;
16+
- (void)requestKeyFrame;
1617
- (BOOL)sendTouchAtX:(double)x
1718
y:(double)y
1819
phase:(NSString *)phase

cli/native/XCWNativeSession.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ - (void)requestRefresh {
9494
[self.session requestFrameRefresh];
9595
}
9696

97+
- (void)requestKeyFrame {
98+
[self.session requestKeyFrameRefresh];
99+
}
100+
97101
- (BOOL)sendTouchAtX:(double)x
98102
y:(double)y
99103
phase:(NSString *)phase

client/src/features/stream/streamWorkerClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ function configureLowLatencyReceiver(receiver: RTCRtpReceiver) {
565565
jitterBufferTarget?: number;
566566
};
567567
if ("jitterBufferTarget" in lowLatencyReceiver) {
568-
lowLatencyReceiver.jitterBufferTarget = 0.03;
568+
lowLatencyReceiver.jitterBufferTarget = 0.001;
569569
}
570570
}
571571

server/src/native/bridge.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,12 @@ impl NativeSession {
624624
}
625625
}
626626

627+
pub fn request_keyframe(&self) {
628+
unsafe {
629+
ffi::xcw_native_session_request_keyframe(self.handle);
630+
}
631+
}
632+
627633
pub unsafe fn set_frame_callback(
628634
&self,
629635
callback: Option<ffi::xcw_native_frame_callback>,

server/src/native/ffi.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ unsafe extern "C" {
174174
pub fn xcw_native_session_destroy(handle: *mut c_void);
175175
pub fn xcw_native_session_start(handle: *mut c_void, error_message: *mut *mut c_char) -> bool;
176176
pub fn xcw_native_session_request_refresh(handle: *mut c_void);
177+
pub fn xcw_native_session_request_keyframe(handle: *mut c_void);
177178
pub fn xcw_native_session_set_frame_callback(
178179
handle: *mut c_void,
179180
callback: Option<xcw_native_frame_callback>,

server/src/simulators/session.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ use tokio::task;
1414
use tokio::time::{timeout, Instant};
1515
use tracing::debug;
1616

17-
const FRAME_BROADCAST_CAPACITY: usize = 240;
18-
const MIN_REFRESH_INTERVAL_MS: u64 = 66;
17+
const FRAME_BROADCAST_CAPACITY: usize = 32;
18+
const MIN_REFRESH_INTERVAL_MS: u64 = 16;
19+
const MIN_KEYFRAME_INTERVAL_MS: u64 = 250;
1920

2021
pub struct SimulatorSession {
2122
inner: Arc<SimulatorSessionInner>,
@@ -34,6 +35,7 @@ struct SimulatorSessionInner {
3435
display_height: AtomicU64,
3536
frame_sequence: AtomicU64,
3637
last_refresh_ms: AtomicU64,
38+
last_keyframe_ms: AtomicU64,
3739
}
3840

3941
impl SimulatorSession {
@@ -56,6 +58,7 @@ impl SimulatorSession {
5658
display_height: AtomicU64::new(0),
5759
frame_sequence: AtomicU64::new(0),
5860
last_refresh_ms: AtomicU64::new(0),
61+
last_keyframe_ms: AtomicU64::new(0),
5962
});
6063

6164
let user_data = Weak::into_raw(Arc::downgrade(&inner)) as *mut c_void;
@@ -149,6 +152,22 @@ impl SimulatorSession {
149152
self.inner.native.request_refresh();
150153
}
151154

155+
pub fn request_keyframe(&self) {
156+
let now = now_ms();
157+
let previous = self.inner.last_keyframe_ms.load(Ordering::Relaxed);
158+
if now.saturating_sub(previous) < MIN_KEYFRAME_INTERVAL_MS {
159+
self.request_refresh();
160+
return;
161+
}
162+
self.inner.last_keyframe_ms.store(now, Ordering::Relaxed);
163+
self.inner.last_refresh_ms.store(now, Ordering::Relaxed);
164+
self.inner
165+
.metrics
166+
.keyframe_requests
167+
.fetch_add(1, Ordering::Relaxed);
168+
self.inner.native.request_keyframe();
169+
}
170+
152171
pub fn send_touch(&self, x: f64, y: f64, phase: &str) -> Result<(), AppError> {
153172
self.inner.native.send_touch(x, y, phase)
154173
}

server/src/transport/webrtc.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ const ANNEX_B_START_CODE: &[u8] = &[0, 0, 0, 1];
2626
const DEFAULT_STUN_URL: &str = "stun:stun.l.google.com:19302";
2727
const WEBRTC_CONTROL_CHANNEL_LABEL: &str = "simdeck-control";
2828
const WEBRTC_BOOTSTRAP_KEYFRAME_INTERVAL: Duration = Duration::from_millis(250);
29-
const WEBRTC_BOOTSTRAP_KEYFRAME_REPEATS: u8 = 12;
30-
const WEBRTC_REFRESH_INTERVAL: Duration = Duration::from_millis(66);
29+
const WEBRTC_BOOTSTRAP_KEYFRAME_REPEATS: u8 = 4;
30+
const WEBRTC_MIN_REFRESH_INTERVAL: Duration = Duration::from_millis(16);
31+
const WEBRTC_MAX_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
32+
const WEBRTC_WRITE_TIMEOUT: Duration = Duration::from_millis(120);
3133

3234
#[derive(Debug, Deserialize)]
3335
#[serde(rename_all = "camelCase")]
@@ -61,7 +63,7 @@ pub async fn create_answer(
6163
state.registry.remove(&udid);
6264
return Err(error);
6365
}
64-
session.request_refresh();
66+
session.request_keyframe();
6567
info!(
6668
"WebRTC offer for {udid}: remote_candidates={} remote_candidate_types={} ice_servers={}",
6769
count_sdp_candidates(&payload.sdp),
@@ -350,15 +352,19 @@ async fn stream_h264_frames(
350352
let mut last_sequence = 0u64;
351353
let mut send_timing = WebRtcSendTiming::new();
352354
let mut bootstrap_interval = time::interval(WEBRTC_BOOTSTRAP_KEYFRAME_INTERVAL);
353-
let mut refresh_interval = time::interval(WEBRTC_REFRESH_INTERVAL);
354-
refresh_interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
355+
let mut refresh_sleep = Box::pin(time::sleep(WEBRTC_MIN_REFRESH_INTERVAL));
355356
let mut bootstrap_frames_remaining = WEBRTC_BOOTSTRAP_KEYFRAME_REPEATS;
357+
let mut adaptive_refresh_interval = WEBRTC_MIN_REFRESH_INTERVAL;
358+
let mut waiting_for_keyframe = false;
356359
let _guard = WebRtcMetricsGuard::new(state.metrics.clone());
357360

358361
loop {
359362
tokio::select! {
360-
_ = refresh_interval.tick() => {
363+
_ = &mut refresh_sleep => {
361364
session.request_refresh();
365+
refresh_sleep
366+
.as_mut()
367+
.reset(time::Instant::now() + adaptive_refresh_interval);
362368
}
363369
_ = bootstrap_interval.tick(), if bootstrap_frames_remaining > 0 => {
364370
if let Err(error) = write_frame_sample(
@@ -380,7 +386,8 @@ async fn stream_h264_frames(
380386
.metrics
381387
.frames_dropped_server
382388
.fetch_add(skipped, Ordering::Relaxed);
383-
session.request_refresh();
389+
waiting_for_keyframe = true;
390+
session.request_keyframe();
384391
continue;
385392
}
386393
Err(broadcast::error::RecvError::Closed) => break,
@@ -392,7 +399,8 @@ async fn stream_h264_frames(
392399
.frames_dropped_server
393400
.fetch_add(skipped, Ordering::Relaxed);
394401
if !frame.is_keyframe {
395-
session.request_refresh();
402+
waiting_for_keyframe = true;
403+
session.request_keyframe();
396404
continue;
397405
}
398406
}
@@ -401,14 +409,32 @@ async fn stream_h264_frames(
401409
.metrics
402410
.frames_dropped_server
403411
.fetch_add(frame.frame_sequence - last_sequence - 1, Ordering::Relaxed);
404-
session.request_refresh();
412+
waiting_for_keyframe = true;
413+
session.request_keyframe();
414+
continue;
415+
}
416+
if waiting_for_keyframe && !frame.is_keyframe {
417+
state.metrics.frames_dropped_server.fetch_add(1, Ordering::Relaxed);
405418
continue;
406419
}
407420
if frame.is_keyframe {
408421
latest_keyframe = frame.clone();
422+
waiting_for_keyframe = false;
409423
}
410424
let duration = send_timing.duration_for(&frame);
411-
if let Err(error) = write_frame_sample(&video_track, &frame, duration).await {
425+
let started_at = time::Instant::now();
426+
let write_result = time::timeout(
427+
WEBRTC_WRITE_TIMEOUT,
428+
write_frame_sample(&video_track, &frame, duration),
429+
).await;
430+
adaptive_refresh_interval = adaptive_interval_for_write(started_at.elapsed());
431+
if let Err(error) = write_result
432+
.map_err(|_| anyhow::anyhow!(
433+
"timed out writing WebRTC frame after {}ms",
434+
WEBRTC_WRITE_TIMEOUT.as_millis()
435+
))
436+
.and_then(|result| result)
437+
{
412438
warn!("WebRTC frame write failed for {udid}: {error}");
413439
break;
414440
}
@@ -421,6 +447,14 @@ async fn stream_h264_frames(
421447
let _ = peer_connection.close().await;
422448
}
423449

450+
fn adaptive_interval_for_write(write_elapsed: Duration) -> Duration {
451+
let target_ms = (write_elapsed.as_millis() as u64).saturating_mul(2).clamp(
452+
WEBRTC_MIN_REFRESH_INTERVAL.as_millis() as u64,
453+
WEBRTC_MAX_REFRESH_INTERVAL.as_millis() as u64,
454+
);
455+
Duration::from_millis(target_ms)
456+
}
457+
424458
async fn write_frame_sample(
425459
video_track: &TrackLocalStaticSample,
426460
frame: &crate::transport::packet::SharedFrame,

0 commit comments

Comments
 (0)