Skip to content

Commit 8dc8a99

Browse files
authored
WebRTC improvements (#4603)
1 parent d5effd4 commit 8dc8a99

File tree

3 files changed

+41
-36
lines changed

3 files changed

+41
-36
lines changed

ui/src/features/inference/stream/stream.tsx

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,26 @@ const useSetTargetSizeBasedOnVideo = (
1212
) => {
1313
useEffect(() => {
1414
const video = videoRef.current;
15+
if (!video) return;
1516

16-
const onsize = video?.addEventListener('loadedmetadata', (event) => {
17-
const target = event.currentTarget as HTMLVideoElement;
18-
19-
if (target.videoWidth && target.videoHeight) {
20-
setSize({ width: target.videoWidth, height: target.videoHeight });
17+
const onLoaded = () => {
18+
if (video.videoWidth && video.videoHeight) {
19+
setSize({ width: video.videoWidth, height: video.videoHeight });
2120
}
22-
});
23-
24-
const onresize = video?.addEventListener('resize', (event) => {
25-
const target = event.currentTarget as HTMLVideoElement;
21+
};
2622

27-
if (target.videoWidth && target.videoHeight) {
28-
setSize({ width: target.videoWidth, height: target.videoHeight });
23+
const resizeObserver = new ResizeObserver(() => {
24+
if (video.videoWidth && video.videoHeight) {
25+
setSize({ width: video.videoWidth, height: video.videoHeight });
2926
}
3027
});
3128

32-
return () => {
33-
if (onsize) {
34-
video?.removeEventListener('loadedmetadata', onsize);
35-
}
29+
video.addEventListener('loadedmetadata', onLoaded);
30+
resizeObserver.observe(video);
3631

37-
if (onresize) {
38-
video?.removeEventListener('resize', onresize);
39-
}
32+
return () => {
33+
video.removeEventListener('loadedmetadata', onLoaded);
34+
resizeObserver.disconnect();
4035
};
4136
}, [setSize, videoRef]);
4237
};
@@ -56,7 +51,7 @@ const useStreamToVideo = () => {
5651
}
5752

5853
const receivers = peerConnection.getReceivers() ?? [];
59-
const stream = new MediaStream(receivers.map((receiver) => receiver.track));
54+
const stream = new MediaStream(receivers.map((receiver) => receiver.track).filter(Boolean));
6055

6156
if (videoOutput && videoOutput.srcObject !== stream) {
6257
videoOutput.srcObject = stream;

ui/src/features/inference/stream/web-rtc-connection.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (C) 2025 Intel Corporation
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { v4 as uuid } from 'uuid';
5+
46
import { fetchClient } from '../../../api/client';
57

68
export type WebRTCConnectionStatus = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'failed';
@@ -37,8 +39,7 @@ export class WebRTCConnection {
3739
private timeoutId?: ReturnType<typeof setTimeout>;
3840

3941
constructor() {
40-
// TODO: replace with uuid
41-
this.webrtcId = Math.random().toString(36).substring(7);
42+
this.webrtcId = uuid();
4243
}
4344

4445
public getStatus(): WebRTCConnectionStatus {
@@ -80,9 +81,11 @@ export class WebRTCConnection {
8081
} catch (err) {
8182
clearTimeout(this.timeoutId);
8283
console.error('Error setting up WebRTC:', err);
84+
8385
this.emit({ type: 'error', error: err as Error });
8486
this.updateStatus('failed');
85-
this.stop();
87+
88+
await this.stop();
8689
}
8790

8891
if (this.peerConnection) {
@@ -109,21 +112,26 @@ export class WebRTCConnection {
109112
}
110113

111114
private async waitForIceGathering(): Promise<void> {
112-
await new Promise<void>((resolve) => {
113-
if (!this.peerConnection || this.peerConnection.iceGatheringState === 'complete') {
114-
resolve();
115-
return;
116-
}
117-
118-
const checkState = () => {
119-
if (this.peerConnection && this.peerConnection.iceGatheringState === 'complete') {
120-
this.peerConnection.removeEventListener('icegatheringstatechange', checkState);
115+
await Promise.race([
116+
new Promise<void>((resolve) => {
117+
if (!this.peerConnection || this.peerConnection.iceGatheringState === 'complete') {
121118
resolve();
119+
return;
122120
}
123-
};
124121

125-
this.peerConnection?.addEventListener('icegatheringstatechange', checkState);
126-
});
122+
const checkState = () => {
123+
if (this.peerConnection && this.peerConnection.iceGatheringState === 'complete') {
124+
this.peerConnection.removeEventListener('icegatheringstatechange', checkState);
125+
resolve();
126+
}
127+
};
128+
129+
this.peerConnection?.addEventListener('icegatheringstatechange', checkState);
130+
}),
131+
new Promise<void>((_, reject) =>
132+
setTimeout(() => reject(new Error('ICE gathering timed out')), CONNECTION_TIMEOUT)
133+
),
134+
]);
127135
}
128136

129137
private async sendOffer(): Promise<SessionData | undefined> {

ui/tests/fixtures.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ const test = testBase.extend<Fixtures>({
2121
return response(200).json([]);
2222
}),
2323
http.post('/api/webrtc/offer', ({ response }) => {
24-
// Schema is empty, so we return an empty object
25-
return response(200).json({} as never);
24+
return response(200).json({
25+
type: 'answer',
26+
sdp: 'v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\n',
27+
} as never);
2628
}),
2729
http.post('/api/input_hook', ({ response }) => {
2830
// Schema is empty, so we return an empty object

0 commit comments

Comments
 (0)