moeru-ai/airi · warning

setSourceMutex released for window due to timeout. Please ma

Error message

setSourceMutex released for window due to timeout. Please make sure to invoke screenCaptureResetSource when getDisplayMedia is completed.

What it means

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.

Source

Thrown at packages/electron-screen-capture/src/main/index.ts:260

          throw new Error(`Source with id ${request.sourceId} not found.`)
        }

        callback({
          video: source,
          audio: options?.loopbackWithMute ? LoopbackAudioTypes.LoopbackWithMute : LoopbackAudioTypes.Loopback,
        })
      })

      setSourceMutexTimeoutHandle = setTimeout(() => {
        if (screenCaptureSourceMutexHandle !== handle)
          return

        resetScreenCaptureSource()
        setSourceMutex.release()

        log
          .withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) })
          .warn(
            `setSourceMutex released for window due to timeout. `
            + 'Please make sure to invoke screenCaptureResetSource when getDisplayMedia is completed.',
          )
      }, timeout ?? 5000)

      return handle
    }
    catch (e) {
      log
        .withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) })
        .withError(e)
        .error('screenCaptureSetSourceEx failed for window')

      resetScreenCaptureSource()
      setSourceMutex.release()
      throw e
    }
  })

View on GitHub (pinned to 677329427f)

Solutions

  1. Always call screenCapture.resetSource(mutexHandle) in a finally after getDisplayMedia resolves or rejects, using the handle returned by setSource
  2. Pass a larger timeout in the setSource request if capture setup legitimately exceeds 5000 ms
  3. Treat each occurrence as a leak detector: find the code path that stopped the stream without resetting

Example fix

// before
const handle = await screenCaptureInvoke.setSource({ sourceId })
const stream = await navigator.mediaDevices.getUserMedia({
  video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } },
})
// no reset on stop -> mutex times out after 5s

// after
const handle = await screenCaptureInvoke.setSource({ sourceId })
try {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } },
  })
  stream.getVideoTracks()[0].addEventListener('ended', () => screenCaptureInvoke.resetSource(handle))
  return stream
}
catch (error) {
  await screenCaptureInvoke.resetSource(handle)
  throw error
}
Defensive patterns

Strategy: try-catch

Try / catch

const handle = await screenCaptureInvoke.setSource({ sourceId, timeout: 10_000 })
try {
  return await navigator.mediaDevices.getUserMedia({
    video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } },
  })
}
finally {
  // releases the mutex and disarms the timeout on every path
  await screenCaptureInvoke.resetSource(handle)
}

Prevention

When it happens

Trigger: 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).

Common situations: A missing finally block around getDisplayMedia; early returns on permission denial that skip reset; slow machines where source enumeration exceeds 5s.

Understand the failure class

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/afc95b6d9cbf957c. Report an issue: GitHub.