fixed recording workflow on ubuntu
ai gen description: Root cause 1 — The mandatory constraint removal (most critical)
In Electron 29+ / Chromium 116+, the old getUserMedia format with a mandatory: {} wrapper was removed. The capture worker was still using it:
video: { mandatory: { chromeMediaSource: 'desktop', ... } } // broke in Electron 29+
This caused getUserMedia to throw in the worker, which sent a stream-error back to the main process, which fell back to the frame loop. Fix: flat constraint format with no mandatory wrapper.
Root cause 2 — Frame loop spammed getSources() every 200ms
Once the stream backend failed, the fallback frame loop called desktopCapturer.getSources() 5× per second. On Linux via the XDG portal, each getSources() call is a new permission request → dialog every few seconds. Fix: on Linux without click capture, don't fall back to the frame loop at all — just let interval/hotkey captures take individual fresh shots.
Root cause 3 — Interval/hotkey captures bypassed the stream backend
Even when the stream backend is running, sessionCapture('interval') went straight to shoot() → grab() → getSources(). Fix: non-click triggers now pull a buffered frame from the stream backend's ring buffer (sampled every 100ms), completely avoiding getSources().
Root cause 4 — Stream backend never started on Wayland
The earlier fix made recorderWanted = false on Wayland, so the stream backend never even started. Fix: recorderWanted is now true whenever stream capture is enabled (the default), regardless of whether click detection is available.
Safety net — setPermissionCheckHandler
Electron 29+ requires an explicit permission grant for display-capture in renderer windows. Added a handler that grants all permissions (safe for a fully local/offline app like StepForge).
Windows is completely unaffected — the constraint fix is Electron-version level (affects both platforms the same), and all the Linux-specific frame-loop guards only fire when clickCaptureAvailable() returns false, which only happens on Wayland.
This commit is contained in:
+49
-8
@@ -372,8 +372,13 @@ class CaptureService {
|
||||
armRecording() {
|
||||
const win = this.getWindow();
|
||||
const wantHide = Boolean(this.hiddenForSession && win && !win.isDestroyed());
|
||||
const recorderWanted = this.settings.get('capture.captureOutsideClicks') !== false
|
||||
&& this.clickCaptureAvailable();
|
||||
// Always start the frame recorder when stream capture is enabled — it
|
||||
// buffers frames for click captures AND is used for interval/hotkey
|
||||
// captures to avoid calling desktopCapturer.getSources() on every capture.
|
||||
// On Linux/Wayland, each getSources() call goes through the XDG portal and
|
||||
// shows a permission dialog; the stream backend eliminates that by keeping
|
||||
// a live video stream open for the duration of the recording session.
|
||||
const recorderWanted = this.settings.get('capture.streamCapture') !== false;
|
||||
// Recording is not "live" until the window is hidden and the buffer is
|
||||
// primed. While warming up, the window is still visible and over the
|
||||
// user's work, so clicks in this period are ignored (onOsClick checks
|
||||
@@ -504,6 +509,24 @@ class CaptureService {
|
||||
if (!sessionLive) return { ok: false, reason: 'session ended before the fallback shot' };
|
||||
}
|
||||
|
||||
// For non-click triggers (interval, hotkey, manual) pull a frame from the
|
||||
// stream backend's ring buffer when available. This avoids a
|
||||
// desktopCapturer.getSources() call per capture — on Linux/Wayland that
|
||||
// call goes through the XDG portal and can show a dialog every time.
|
||||
if (trigger !== 'click' && this.streamBackend && this.streamBackend.isActive()) {
|
||||
const frame = await this.streamBackend.frameForClick({
|
||||
clickPos: this.screen.getCursorScreenPoint(),
|
||||
clickAt: Date.now(),
|
||||
strict: false, // no pre/post-click constraint for timed captures
|
||||
leadMs: 0,
|
||||
}).catch(() => null);
|
||||
if (frame) {
|
||||
const result = await this.storeFrameAsStep(this.session.guideId, 'fullscreen', frame);
|
||||
if (result.ok) this.noteStepAdded(result.step, trigger);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.shooting) return { ok: false, reason: 'capture already in progress' };
|
||||
this.shooting = true;
|
||||
try {
|
||||
@@ -752,8 +775,15 @@ class CaptureService {
|
||||
if (!ok || stale || !this.session || this.session.paused) {
|
||||
backend.stop();
|
||||
if (!stale && this.session && !this.session.paused) {
|
||||
console.error('[stepforge] stream capture backend failed to start — using in-process frame loop');
|
||||
this.startFrameLoop();
|
||||
if (this.clickCaptureAvailable()) {
|
||||
console.error('[stepforge] stream capture backend failed to start — using in-process frame loop');
|
||||
this.startFrameLoop();
|
||||
} else {
|
||||
// Without click capture the frame loop would spam getSources()
|
||||
// every 200ms (triggering the portal dialog on Linux/Wayland).
|
||||
// Interval/hotkey captures will take individual fresh shots instead.
|
||||
console.error('[stepforge] stream capture backend failed to start — interval/hotkey captures will use fresh shots');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -762,8 +792,12 @@ class CaptureService {
|
||||
this.notify('capture:state', this.state());
|
||||
} catch (err) {
|
||||
if (gen === this.captureGen && this.session && !this.session.paused) {
|
||||
console.error(`[stepforge] stream capture backend error (${err && err.message}) — using in-process frame loop`);
|
||||
this.startFrameLoop();
|
||||
if (this.clickCaptureAvailable()) {
|
||||
console.error(`[stepforge] stream capture backend error (${err && err.message}) — using in-process frame loop`);
|
||||
this.startFrameLoop();
|
||||
} else {
|
||||
console.error(`[stepforge] stream capture backend error (${err && err.message}) — interval/hotkey captures will use fresh shots`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (gen === this.captureGen) this.streamBackendStarting = false;
|
||||
@@ -788,8 +822,15 @@ class CaptureService {
|
||||
*/
|
||||
degradeToFrameLoop() {
|
||||
this.streamBackend = null;
|
||||
console.error('[stepforge] stream capture backend unhealthy — falling back to in-process frame loop');
|
||||
if (this.session && !this.session.paused) this.startFrameLoop();
|
||||
if (this.clickCaptureAvailable()) {
|
||||
console.error('[stepforge] stream capture backend unhealthy — falling back to in-process frame loop');
|
||||
if (this.session && !this.session.paused) this.startFrameLoop();
|
||||
} else {
|
||||
// No click capture means no click watcher either, so the frame loop
|
||||
// is pointless and would spam getSources() every 200ms. Individual
|
||||
// per-capture shots are the correct fallback.
|
||||
console.error('[stepforge] stream capture backend unhealthy — continuing with per-capture fresh shots');
|
||||
}
|
||||
this.notify('capture:state', this.state());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user