{"record":{"id":"afc95b6d9cbf957c","repo":"moeru-ai/airi","slug":"setsourcemutex-released-for-window-due-to-timeout","errorCode":null,"errorMessage":"setSourceMutex released for window due to timeout. Please make sure to invoke screenCaptureResetSource when getDisplayMedia is completed.","messagePattern":"setSourceMutex released for window due to timeout\\. Please make sure to invoke screenCaptureResetSource when getDisplayMedia is completed\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/electron-screen-capture/src/main/index.ts","lineNumber":260,"sourceCode":"          throw new Error(`Source with id ${request.sourceId} not found.`)\n        }\n\n        callback({\n          video: source,\n          audio: options?.loopbackWithMute ? LoopbackAudioTypes.LoopbackWithMute : LoopbackAudioTypes.Loopback,\n        })\n      })\n\n      setSourceMutexTimeoutHandle = setTimeout(() => {\n        if (screenCaptureSourceMutexHandle !== handle)\n          return\n\n        resetScreenCaptureSource()\n        setSourceMutex.release()\n\n        log\n          .withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) })\n          .warn(\n            `setSourceMutex released for window due to timeout. `\n            + 'Please make sure to invoke screenCaptureResetSource when getDisplayMedia is completed.',\n          )\n      }, timeout ?? 5000)\n\n      return handle\n    }\n    catch (e) {\n      log\n        .withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) })\n        .withError(e)\n        .error('screenCaptureSetSourceEx failed for window')\n\n      resetScreenCaptureSource()\n      setSourceMutex.release()\n      throw e\n    }\n  })","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/electron-screen-capture/src/main/index.ts#L242-L278","documentation":"When a renderer invokes screenCapture.setSource, main acquires setSourceMutex and arms a timeout (request.timeout, default 5000 ms) that force-releases the source if the renderer never calls screenCapture.resetSource with the returned handle. This warning means the safety net fired: the capture source was reset and the mutex released because getDisplayMedia completed (or died) without the matching reset call.","triggerScenarios":"Renderer calls setSource, gets a stream from getDisplayMedia, then stops the tracks without invoking screenCapture.resetSource(handle); or an error path between acquiring the stream and releasing; or the renderer crashes/navigates away mid-capture; or negotiation legitimately takes longer than the timeout (default 5s).","commonSituations":"A missing finally block around getDisplayMedia; early returns on permission denial that skip reset; slow machines where source enumeration exceeds 5s.","solutions":["Always call screenCapture.resetSource(mutexHandle) in a finally after getDisplayMedia resolves or rejects, using the handle returned by setSource","Pass a larger timeout in the setSource request if capture setup legitimately exceeds 5000 ms","Treat each occurrence as a leak detector: find the code path that stopped the stream without resetting"],"exampleFix":"// before\nconst handle = await screenCaptureInvoke.setSource({ sourceId })\nconst stream = await navigator.mediaDevices.getUserMedia({\n  video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } },\n})\n// no reset on stop -> mutex times out after 5s\n\n// after\nconst handle = await screenCaptureInvoke.setSource({ sourceId })\ntry {\n  const stream = await navigator.mediaDevices.getUserMedia({\n    video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } },\n  })\n  stream.getVideoTracks()[0].addEventListener('ended', () => screenCaptureInvoke.resetSource(handle))\n  return stream\n}\ncatch (error) {\n  await screenCaptureInvoke.resetSource(handle)\n  throw error\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"const handle = await screenCaptureInvoke.setSource({ sourceId, timeout: 10_000 })\ntry {\n  return await navigator.mediaDevices.getUserMedia({\n    video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } },\n  })\n}\nfinally {\n  // releases the mutex and disarms the timeout on every path\n  await screenCaptureInvoke.resetSource(handle)\n}","preventionTips":["Always pair setSource with resetSource(handle) in try/finally","Reset on stream 'ended' too, so user-initiated stops release the mutex immediately","Pass an explicit timeout sized to your slowest real capture setup"],"tags":["electron","screen-capture","mutex","resource-leak","getdisplaymedia"],"backgroundTag":"resource-not-released","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}