@@ -112,6 +112,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
112112 const restarting = useRef ( false ) ;
113113 const countdownRunId = useRef ( 0 ) ;
114114 const [ countdownActive , setCountdownActive ] = useState ( false ) ;
115+ const webcamReady = useRef ( false ) ;
116+ const webcamAcquireId = useRef ( 0 ) ;
115117
116118 const getRecordingDurationMs = useCallback ( ( ) => {
117119 const segmentDuration =
@@ -160,10 +162,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
160162 microphoneStream . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
161163 microphoneStream . current = null ;
162164 }
163- if ( webcamStream . current ) {
164- webcamStream . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
165- webcamStream . current = null ;
166- }
167165 if ( mixingContext . current ) {
168166 mixingContext . current . close ( ) . catch ( ( ) => {
169167 // Ignore close errors during recorder teardown.
@@ -196,6 +194,85 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
196194 [ t ] ,
197195 ) ;
198196
197+ useEffect ( ( ) => {
198+ if ( ! webcamEnabled ) return ;
199+
200+ let cancelled = false ;
201+ let acquiredStream : MediaStream | null = null ;
202+ const thisAcquireId = ++ webcamAcquireId . current ;
203+ webcamReady . current = false ;
204+
205+ const acquire = async ( ) => {
206+ try {
207+ const stream = await navigator . mediaDevices . getUserMedia ( {
208+ audio : false ,
209+ video : webcamDeviceId
210+ ? {
211+ deviceId : { exact : webcamDeviceId } ,
212+ width : { ideal : WEBCAM_TARGET_WIDTH } ,
213+ height : { ideal : WEBCAM_TARGET_HEIGHT } ,
214+ frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
215+ }
216+ : {
217+ width : { ideal : WEBCAM_TARGET_WIDTH } ,
218+ height : { ideal : WEBCAM_TARGET_HEIGHT } ,
219+ frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
220+ } ,
221+ } ) ;
222+
223+ if ( cancelled || thisAcquireId !== webcamAcquireId . current ) {
224+ stream . getTracks ( ) . forEach ( ( track ) => {
225+ track . onended = null ;
226+ track . stop ( ) ;
227+ } ) ;
228+ return ;
229+ }
230+
231+ acquiredStream = stream ;
232+ stream . getVideoTracks ( ) . forEach ( ( track ) => {
233+ track . onended = ( ) => {
234+ webcamStream . current = null ;
235+ if ( ! restarting . current ) {
236+ setWebcamEnabledState ( false ) ;
237+ toast . error ( t ( "recording.cameraDisconnected" ) ) ;
238+ }
239+ } ;
240+ } ) ;
241+ webcamStream . current = stream ;
242+ webcamReady . current = true ;
243+ } catch ( cameraError ) {
244+ if ( ! cancelled ) {
245+ console . warn ( "Failed to get webcam access:" , cameraError ) ;
246+ setWebcamEnabledState ( false ) ;
247+ const isDeviceError =
248+ cameraError instanceof DOMException &&
249+ [
250+ "NotFoundError" ,
251+ "DevicesNotFoundError" ,
252+ "OverconstrainedError" ,
253+ "NotReadableError" ,
254+ ] . includes ( cameraError . name ) ;
255+ toast . error ( t ( isDeviceError ? "recording.cameraNotFound" : "recording.cameraBlocked" ) ) ;
256+ webcamReady . current = true ;
257+ }
258+ }
259+ } ;
260+
261+ void acquire ( ) ;
262+
263+ return ( ) => {
264+ cancelled = true ;
265+ webcamReady . current = false ;
266+ if ( acquiredStream ) {
267+ acquiredStream . getTracks ( ) . forEach ( ( track ) => {
268+ track . onended = null ;
269+ track . stop ( ) ;
270+ } ) ;
271+ webcamStream . current = null ;
272+ }
273+ } ;
274+ } , [ webcamEnabled , webcamDeviceId , t ] ) ;
275+
199276 const finalizeRecording = useCallback (
200277 (
201278 activeScreenRecorder : RecorderHandle ,
@@ -568,30 +645,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
568645 }
569646
570647 if ( webcamEnabled ) {
571- try {
572- webcamStream . current = await navigator . mediaDevices . getUserMedia ( {
573- audio : false ,
574- video : webcamDeviceId
575- ? {
576- deviceId : { exact : webcamDeviceId } ,
577- width : { ideal : WEBCAM_TARGET_WIDTH } ,
578- height : { ideal : WEBCAM_TARGET_HEIGHT } ,
579- frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
580- }
581- : {
582- width : { ideal : WEBCAM_TARGET_WIDTH } ,
583- height : { ideal : WEBCAM_TARGET_HEIGHT } ,
584- frameRate : { ideal : WEBCAM_TARGET_FRAME_RATE , max : WEBCAM_TARGET_FRAME_RATE } ,
585- } ,
648+ if ( ! webcamReady . current ) {
649+ await new Promise < void > ( ( resolve ) => {
650+ const interval = setInterval ( ( ) => {
651+ if ( webcamReady . current ) {
652+ clearInterval ( interval ) ;
653+ resolve ( ) ;
654+ }
655+ } , 50 ) ;
656+ setTimeout ( ( ) => {
657+ clearInterval ( interval ) ;
658+ resolve ( ) ;
659+ } , 5000 ) ;
586660 } ) ;
587- } catch ( cameraError ) {
588- console . warn ( "Failed to get webcam access:" , cameraError ) ;
589- if ( webcamStream . current ) {
590- webcamStream . current . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
591- webcamStream . current = null ;
592- }
661+ }
662+ if ( ! webcamStream . current ) {
663+ webcamAcquireId . current ++ ;
593664 setWebcamEnabledState ( false ) ;
594- toast . error ( t ( "recording.cameraDenied" ) ) ;
595665 }
596666 }
597667
0 commit comments