@@ -110,6 +110,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
110110 const allowAutoFinalize = useRef ( false ) ;
111111 const discardRecordingId = useRef < number | null > ( null ) ;
112112 const restarting = useRef ( false ) ;
113+ const webcamReady = useRef ( false ) ;
114+ const webcamAcquireId = useRef ( 0 ) ;
113115
114116 const getRecordingDurationMs = useCallback ( ( ) => {
115117 const segmentDuration =
@@ -158,10 +160,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
158160 microphoneStream . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
159161 microphoneStream . current = null ;
160162 }
161- if ( webcamStream . current ) {
162- webcamStream . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
163- webcamStream . current = null ;
164- }
165163 if ( mixingContext . current ) {
166164 mixingContext . current . close ( ) . catch ( ( ) => {
167165 // Ignore close errors during recorder teardown.
@@ -194,6 +192,85 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
194192 [ t ] ,
195193 ) ;
196194
195+ useEffect ( ( ) => {
196+ if ( ! webcamEnabled ) return ;
197+
198+ let cancelled = false ;
199+ let acquiredStream : MediaStream | null = null ;
200+ const thisAcquireId = ++ webcamAcquireId . current ;
201+ webcamReady . current = false ;
202+
203+ const acquire = async ( ) => {
204+ try {
205+ const stream = await navigator . mediaDevices . getUserMedia ( {
206+ audio : false ,
207+ video : webcamDeviceId
208+ ? {
209+ deviceId : { exact : webcamDeviceId } ,
210+ width : { ideal : WEBCAM_TARGET_WIDTH } ,
211+ height : { ideal : WEBCAM_TARGET_HEIGHT } ,
212+ frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
213+ }
214+ : {
215+ width : { ideal : WEBCAM_TARGET_WIDTH } ,
216+ height : { ideal : WEBCAM_TARGET_HEIGHT } ,
217+ frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
218+ } ,
219+ } ) ;
220+
221+ if ( cancelled || thisAcquireId !== webcamAcquireId . current ) {
222+ stream . getTracks ( ) . forEach ( ( track ) => {
223+ track . onended = null ;
224+ track . stop ( ) ;
225+ } ) ;
226+ return ;
227+ }
228+
229+ acquiredStream = stream ;
230+ stream . getVideoTracks ( ) . forEach ( ( track ) => {
231+ track . onended = ( ) => {
232+ webcamStream . current = null ;
233+ if ( ! restarting . current ) {
234+ setWebcamEnabledState ( false ) ;
235+ toast . error ( t ( "recording.cameraDisconnected" ) ) ;
236+ }
237+ } ;
238+ } ) ;
239+ webcamStream . current = stream ;
240+ webcamReady . current = true ;
241+ } catch ( cameraError ) {
242+ if ( ! cancelled ) {
243+ console . warn ( "Failed to get webcam access:" , cameraError ) ;
244+ setWebcamEnabledState ( false ) ;
245+ const isDeviceError =
246+ cameraError instanceof DOMException &&
247+ [
248+ "NotFoundError" ,
249+ "DevicesNotFoundError" ,
250+ "OverconstrainedError" ,
251+ "NotReadableError" ,
252+ ] . includes ( cameraError . name ) ;
253+ toast . error ( t ( isDeviceError ? "recording.cameraNotFound" : "recording.cameraBlocked" ) ) ;
254+ webcamReady . current = true ;
255+ }
256+ }
257+ } ;
258+
259+ void acquire ( ) ;
260+
261+ return ( ) => {
262+ cancelled = true ;
263+ webcamReady . current = false ;
264+ if ( acquiredStream ) {
265+ acquiredStream . getTracks ( ) . forEach ( ( track ) => {
266+ track . onended = null ;
267+ track . stop ( ) ;
268+ } ) ;
269+ webcamStream . current = null ;
270+ }
271+ } ;
272+ } , [ webcamEnabled , webcamDeviceId , t ] ) ;
273+
197274 const finalizeRecording = useCallback (
198275 (
199276 activeScreenRecorder : RecorderHandle ,
@@ -438,30 +515,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
438515 }
439516
440517 if ( webcamEnabled ) {
441- try {
442- webcamStream . current = await navigator . mediaDevices . getUserMedia ( {
443- audio : false ,
444- video : webcamDeviceId
445- ? {
446- deviceId : { exact : webcamDeviceId } ,
447- width : { ideal : WEBCAM_TARGET_WIDTH } ,
448- height : { ideal : WEBCAM_TARGET_HEIGHT } ,
449- frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
450- }
451- : {
452- width : { ideal : WEBCAM_TARGET_WIDTH } ,
453- height : { ideal : WEBCAM_TARGET_HEIGHT } ,
454- frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
455- } ,
518+ if ( ! webcamReady . current ) {
519+ await new Promise < void > ( ( resolve ) => {
520+ const interval = setInterval ( ( ) => {
521+ if ( webcamReady . current ) {
522+ clearInterval ( interval ) ;
523+ resolve ( ) ;
524+ }
525+ } , 50 ) ;
526+ setTimeout ( ( ) => {
527+ clearInterval ( interval ) ;
528+ resolve ( ) ;
529+ } , 5000 ) ;
456530 } ) ;
457- } catch ( cameraError ) {
458- console . warn ( "Failed to get webcam access:" , cameraError ) ;
459- if ( webcamStream . current ) {
460- webcamStream . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
461- webcamStream . current = null ;
462- }
531+ }
532+ if ( ! webcamStream . current ) {
533+ webcamAcquireId . current ++ ;
463534 setWebcamEnabledState ( false ) ;
464- toast . error ( t ( "recording.cameraDenied" ) ) ;
465535 }
466536 }
467537
0 commit comments