Skip to content

Commit 5eda447

Browse files
authored
Merge pull request #70 from red5pro/TTS-269
TTS-269
2 parents 07f6b75 + ef53604 commit 5eda447

4 files changed

Lines changed: 78 additions & 9 deletions

File tree

src/hooks/useRecording.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import { ConferenceClient } from 'red5pro-conference-sdk';
1010
import { ConferenceEvents } from 'red5pro-conference-sdk';
1111
import { MediabunnyRecorder } from '../utils/MediabunnyRecorder';
1212
import { createCompositeStream } from '../utils/compositeStream';
13+
import { withTimeout, TimeoutError } from '../utils/withTimeout';
1314
import JSZip from 'jszip';
1415
import { S3Client } from '@aws-sdk/client-s3';
1516
import { Upload } from '@aws-sdk/lib-storage';
1617

18+
// Some browsers (notably Safari) can hang indefinitely while flushing WebCodecs
19+
// encoders on stop(); cap the wait so the UI never gets stuck showing "recording".
20+
const RECORDER_STOP_TIMEOUT_MS = 15000;
21+
1722
// Type definitions
1823
type MessageVariant = 'info' | 'success' | 'error' | 'warning';
1924

@@ -741,12 +746,20 @@ export const useRecording = (
741746
}
742747

743748
try {
744-
const blob = await mediabunnyRecorderRef.current.stop();
749+
const blob = await withTimeout(
750+
mediabunnyRecorderRef.current.stop(),
751+
RECORDER_STOP_TIMEOUT_MS,
752+
'Timed out stopping local recording',
753+
);
745754

746755
// Stop the local-only recorder alongside the composite one
747756
if (localOnlyRecorderRef.current) {
748757
try {
749-
const localOnlyBlob = await localOnlyRecorderRef.current.stop();
758+
const localOnlyBlob = await withTimeout(
759+
localOnlyRecorderRef.current.stop(),
760+
RECORDER_STOP_TIMEOUT_MS,
761+
'Timed out stopping local-only recording',
762+
);
750763
if (localOnlyBlob) {
751764
localOnlyRecordedSegmentsRef.current.push(localOnlyBlob);
752765
}
@@ -808,7 +821,12 @@ export const useRecording = (
808821
currentRecordingStreamRef.current = null;
809822
localStreamRef.current = null;
810823
if (displayMessageRef.current) {
811-
displayMessageRef.current('Failed to stop local recording', 'error');
824+
displayMessageRef.current(
825+
error instanceof TimeoutError
826+
? 'Stopping local recording took too long and was aborted'
827+
: 'Failed to stop local recording',
828+
'error',
829+
);
812830
}
813831
return null;
814832
}

src/hooks/useVirtualBackground.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { useState, useCallback, useEffect, useRef, MutableRefObject } from 'reac
44
import { getVirtualBackgroundConfigs } from '../utils/conferenceConfig';
55
import { VirtualBackgroundTypes } from 'red5pro-conference-sdk';
66
import log from 'loglevel';
7+
import { withTimeout } from '../utils/withTimeout';
8+
9+
// Some browsers (notably Safari, which falls back to an OffscreenCanvas/WASM
10+
// segmentation pipeline instead of insertable streams) can hang indefinitely
11+
// when initializing or applying a virtual background. Cap the wait so the UI
12+
// never gets stuck reporting the effect as enabled when it isn't working.
13+
const VIRTUAL_BACKGROUND_TIMEOUT_MS = 15000;
714

815
// Type definitions
916
type VirtualBackgroundType = 'none' | 'blur' | 'slight-blur' | 'color' | 'image';
@@ -152,7 +159,11 @@ export const useVirtualBackground = (
152159
const status = conferenceClientRef.current.getVirtualBackgroundStatus();
153160

154161
if (!status.isInitialized) {
155-
await conferenceClientRef.current.initializeVirtualBackground();
162+
await withTimeout(
163+
conferenceClientRef.current.initializeVirtualBackground(),
164+
VIRTUAL_BACKGROUND_TIMEOUT_MS,
165+
'Timed out initializing virtual background',
166+
);
156167
log.log('Virtual background initialized');
157168
setIsVirtualBackgroundInitialized(true);
158169
}
@@ -204,7 +215,11 @@ export const useVirtualBackground = (
204215

205216
// Handle disable case
206217
if (type === VirtualBackgroundTypes.NONE) {
207-
await conferenceClientRef.current.disableVirtualBackground();
218+
await withTimeout(
219+
conferenceClientRef.current.disableVirtualBackground(),
220+
VIRTUAL_BACKGROUND_TIMEOUT_MS,
221+
'Timed out disabling virtual background',
222+
);
208223
setSelectedBackgroundMode('');
209224
return true;
210225
}
@@ -233,14 +248,22 @@ export const useVirtualBackground = (
233248
}
234249

235250
// Apply the background
236-
await conferenceClientRef.current[action](config.type, config.options);
251+
await withTimeout(
252+
conferenceClientRef.current[action](config.type, config.options),
253+
VIRTUAL_BACKGROUND_TIMEOUT_MS,
254+
`Timed out applying virtual background (${action})`,
255+
);
237256
setSelectedBackgroundMode(type);
238257

239258
log.log(`Virtual background ${action} successful: ${type}`);
240259
return true;
241260
} catch (error) {
242261
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
243262
log.error('Failed to handle background replacement:', error);
263+
// Ensure the UI never reports the effect as enabled when the apply call
264+
// hung/failed and the SDK never actually produced the processed frame.
265+
setIsVirtualBackgroundEnabled(false);
266+
setSelectedBackgroundMode('');
244267
if (showWarningRef.current) {
245268
showWarningRef.current('Failed to apply virtual background: ' + errorMessage);
246269
}

src/utils/MediabunnyRecorder.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,6 @@ export class MediabunnyRecorder {
149149
this.recordedBlob = new Blob([this.target.buffer], { type: 'video/mp4' });
150150
}
151151

152-
this._isRecording = false;
153-
this._isPaused = false;
154-
155152
if (this.onstop && this.recordedBlob) {
156153
this.onstop(this.recordedBlob);
157154
}
@@ -163,6 +160,10 @@ export class MediabunnyRecorder {
163160
}
164161
throw error;
165162
} finally {
163+
// Reset regardless of outcome so callers never see a stuck "recording" state
164+
// if finalize() throws or is abandoned via a timeout.
165+
this._isRecording = false;
166+
this._isPaused = false;
166167
this.cleanup();
167168
}
168169
}

src/utils/withTimeout.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Races a promise against a timeout so a hung Safari media API call
3+
* (WebCodecs encoder flush, WASM/OffscreenCanvas segmentation, etc.)
4+
* can't leave callers awaiting forever.
5+
*/
6+
export class TimeoutError extends Error {
7+
constructor(message: string) {
8+
super(message);
9+
this.name = 'TimeoutError';
10+
}
11+
}
12+
13+
export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
14+
return new Promise<T>((resolve, reject) => {
15+
const timer = setTimeout(() => reject(new TimeoutError(message)), ms);
16+
promise.then(
17+
(value) => {
18+
clearTimeout(timer);
19+
resolve(value);
20+
},
21+
(error) => {
22+
clearTimeout(timer);
23+
reject(error);
24+
},
25+
);
26+
});
27+
}

0 commit comments

Comments
 (0)