{"record":{"id":"f1a107790320ffc7","repo":"moeru-ai/airi","slug":"source-with-id-request-sourceid-not-found","errorCode":null,"errorMessage":"Source with id ${request.sourceId} not found.","messagePattern":"Source with id (.+?) not found\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/electron-screen-capture/src/main/index.ts","lineNumber":242,"sourceCode":"    const { timeout } = request\n    if (typeof timeout === 'number' && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {\n      throw new Error('timeout must be a positive finite number')\n    }\n\n    await setSourceMutex.acquire()\n    log.withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) }).debug('setSourceMutex acquired')\n\n    clearTimeout(setSourceMutexTimeoutHandle)\n    const handle = nanoid()\n    setSourceMutexTimeoutHandle = undefined\n    screenCaptureSourceMutexHandle = handle\n\n    try {\n      session.setDisplayMediaRequestHandler(async (_request, callback) => {\n        const sources = await desktopCapturer.getSources(request.options)\n        const source = sources.find(source => source.id === request.sourceId)\n        if (!source) {\n          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(","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/electron-screen-capture/src/main/index.ts#L224-L260","documentation":"Thrown inside the session.setDisplayMediaRequestHandler callback registered by screenCapture.setSource when desktopCapturer.getSources(request.options) returns no source whose id equals request.sourceId. The sourceId must be one of the ids returned by a prior screenCapture.getSources call, and the same enumeration options matter because they can change which sources Electron reports. Since this fires during the renderer's getUserMedia call, the error surfaces as a failed getDisplayMedia media request. Note the handler stays installed with the mutex held until the timeout handle fires or resetSource is called.","triggerScenarios":"Renderer picks a sourceId from an earlier getSources result, then the target window is closed, a display is unplugged/rearranged, or macOS screen-recording permission is revoked before setSource + getUserMedia run. Also: passing a sourceId enumerated with different options (e.g. different types or thumbnail sizes) than the ones in request.options, or passing a source id from a different Electron session/app restart (ids contain window/display handles and are not stable across restarts).","commonSituations":"A source-picker UI that lists windows once at startup and lets the user pick much later; long-running apps that cache a sourceId across display sleep/wake; multi-monitor rearrangement; passing 'window:123:0' style ids persisted from a previous app run.","solutions":["Re-enumerate sources with screenCapture.getSources immediately before setSource and use a fresh id from that result.","Make sure request.options passed to setSource match the options used for the getSources call that produced sourceId.","In the renderer, catch the getUserMedia/getDisplayMedia failure, re-run getSources, and let the user (or a heuristic like same title) re-pick.","If the window was closed or the display removed, refresh your source list UI instead of reusing stale ids."],"exampleFix":"// before\nconst [source] = sourcesFromStartup // stale list\nawait invoke(screenCapture.setSource, { sourceId: source.id, options })\nconst stream = await navigator.mediaDevices.getUserMedia({ video: true })\n\n// after\nconst sources = await invoke(screenCapture.getSources, options)\nconst source = sources.find(s => s.id === wantedId) ?? sources.find(s => s.name === wantedName)\nif (!source) throw new Error('no capturable source available')\nawait invoke(screenCapture.setSource, { sourceId: source.id, options })\nconst stream = await navigator.mediaDevices.getUserMedia({ video: true })","handlingStrategy":"validation","validationCode":"const sources = await invoke(screenCapture.getSources, options)\nconst target = sources.find(s => s.id === candidateId)\n ?? sources.find(s => s.name === candidateName) // fallback after window recreation\nif (!target) {\n  // refresh picker UI; do not call setSource\n}\nawait invoke(screenCapture.setSource, { sourceId: target.id, options })","typeGuard":null,"tryCatchPattern":"try {\n  const stream = await navigator.mediaDevices.getUserMedia({ video: true })\n} catch (error) {\n  // setDisplayMediaRequestHandler failures surface here: re-enumerate once and let the user re-pick\n  const sources = await invoke(screenCapture.getSources, options)\n  showSourcePicker(sources)\n}","preventionTips":["Always enumerate sources immediately before setSource; ids are opaque and unstable across window/display lifecycle and app restarts.","Pass the exact same options object shape to getSources and setSource.","Treat a closed target window or unplugged display as a hard invalidation for any cached source id."],"tags":["electron","desktop-capturer","stale-id","screen-capture"],"backgroundTag":"resource-not-found","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}