Skip to content

fix: Two related bugs in v1.1.8 on macOS (reproducible on M1):#139

Closed
sethsticle wants to merge 1 commit intowebadderall:mainfrom
sethsticle:fix/screencapturekit-endsession-timestamp
Closed

fix: Two related bugs in v1.1.8 on macOS (reproducible on M1):#139
sethsticle wants to merge 1 commit intowebadderall:mainfrom
sethsticle:fix/screencapturekit-endsession-timestamp

Conversation

@sethsticle
Copy link
Copy Markdown

@sethsticle sethsticle commented Mar 30, 2026

Root Cause

Bug 1 — ScreenCaptureKitRecorder.swift

endSession(atSourceTime:) was passed lastSampleBuffer?.presentationTimeStamp — the raw host clock
time (~3600s if Mac has been running an hour). But the session starts at .zero and all
frames are re-timed to 0-based adjusted timestamps. The resulting MP4 has a malformed edit
list, which the new validateRecordedVideo FFmpeg check (added in 1.1.8) correctly rejects.

Bug 2 — handlers.ts finalizeStoredVideo

persistPendingCursorTelemetry was called after validateRecordedVideo. If validation
threw (due to Bug 1), the cursor JSON was never written — silently losing all cursor tracking data.

Fix

  • Use lastVideoPresentationTime + frameDuration(for: lastSampleBuffer) for endSession
    consistent with the existing adjacent code that computes additionalTime the same way
  • Move cursor telemetry persistence to before validation so it's always saved

Testing

  • Recorded 3s test clip, Swift binary exits cleanly, FFmpeg validation passes (exit 0)
  • Video plays back with correct ~3s duration, h264 High profile, valid timestamps

Fixes #135

  • Apologies for the .claude - I didn't have the time to do it myself and this tool was so good with the tiniest bug that it seemed futile not to get a quick fix out the way.

Summary by CodeRabbit

  • Bug Fixes
    • Improved cursor telemetry capture during video finalization
    • Enhanced macOS source enumeration by filtering invalid sources
    • Refined macOS window detection and bounds resolution
    • Fixed video session timing calculation for accurate recording finalization

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "*" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

The changes address macOS video recording and cursor issues by improving video finalization with cursor telemetry persistence, refining window source enumeration and bounds resolution, and adjusting native recorder session timing calculations. Configuration files are updated with line-ending standardization.

Changes

Cohort / File(s) Summary
Configuration
.gitignore
Updated line endings to CRLF; re-added native build artifact patterns and debug script exclusions; added .claude/ directory ignore for local editor settings.
Video Finalization & Cursor Handling
electron/ipc/handlers.ts
Modified finalizeStoredVideo() to snapshot and persist cursor telemetry before validation with logged errors; made validation results mandatory in file size calculation; removed nullable validation fallback. Updated macOS window highlight flow to fetch and use window bounds from AppleScript. Simplified FFmpeg recording path timestamping by inlining Date.now(). Added thumbnail filtering to source enumeration on macOS to drop sources with missing thumbnails.
Native Session Timing
electron/native/ScreenCaptureKitRecorder.swift
Changed finishCapture() to compute AVAssetWriter session end time from last recorded frame timing and duration rather than directly from sample buffer timestamp; returns zero when no frames recorded.

Sequence Diagram(s)

sequenceDiagram
    participant Handler as IPC Handler
    participant Telemetry as Cursor Telemetry
    participant Validation as Video Validation
    participant Native as Native Recorder

    Handler->>Telemetry: snapshot() & persist()
    Note over Telemetry: Log errors if persist fails
    
    Handler->>Validation: validateRecordedVideo(path)
    Validation-->>Handler: {fileSizeBytes, durationSeconds}
    
    Handler->>Handler: Assert fileSizeBytes not null
    Handler->>Handler: Build success message<br/>(always include size,<br/>conditionally include duration)
    
    Handler->>Native: finishCapture()
    Note over Native: Calculate sessionEndTime<br/>from frame timing
    Native-->>Handler: confirm
    
    Handler-->>Handler: Return finalized result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~23 minutes

Suggested labels

Checked

Poem

🐰 Hops through macOS frames with glee,
Cursor captured, now we see!
Timing mended, bounds are bright,
Window sources filtered right—
Video finalized just so, what a sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the specific bugs being fixed (two related macOS bugs in v1.1.8 on M1) and mentions the main areas affected, directly relating to the core changes.
Description check ✅ Passed The PR description comprehensively covers root causes, fixes, and testing performed. While it doesn't strictly follow the template structure (no checkboxes, no explicit sections), it provides all critical information needed: motivation, changes, and validation.
Linked Issues check ✅ Passed The PR fixes both primary requirements from issue #135: it corrects the malformed MP4 edit list bug causing FFmpeg validation failures [ScreenCaptureKitRecorder.swift], and preserves cursor telemetry persistence [handlers.ts] to ensure cursor data is saved.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the two identified bugs. The .gitignore updates (Claude local settings) and additional handler optimizations are minimal and incidental to the core fixes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
electron/ipc/handlers.ts (1)

3381-3388: Consider reusing one timestamp for all artifacts in a recording.

Calling Date.now() three times can give the video and sidecars different basenames, which makes diagnostics and manual recovery noisier. Capturing the timestamp once keeps the files grouped.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 3381 - 3388, The three separate
Date.now() calls create mismatched basenames; replace them by capturing a single
timestamp (e.g., const ts = Date.now()) before constructing paths and use that
ts when building outputPath, systemAudioOutputPath and microphoneOutputPath
(still honoring recordingsDir and options?.capturesSystemAudio /
options?.capturesMicrophone) so all recording artifacts share the same basename.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Around line 1-5: The .gitignore file contains Windows-style CRLF line endings;
revert them to LF (Unix-style) so Git ignores work consistently across
macOS/Linux; open the .gitignore, convert all CRLF to LF (or run git
normalization with core.autocrlf=input), save and re-stage the .gitignore so the
commit only changes line endings back to LF and remove any unrelated whitespace
edits.

In `@electron/ipc/handlers.ts`:
- Line 2949: Replace the current filter predicate that only checks
Boolean(source.thumbnail) with the usable-thumbnail gate so unusable Electron
thumbnails are excluded before data-URL conversion; specifically, change the
filter on the array (currently .filter((source) => Boolean(source.thumbnail)))
to use hasUsableSourceThumbnail(source) or run hasUsableSourceThumbnail(source)
prior to calling source.toDataURL() (the place where toDataURL() is invoked),
ensuring only sources passing hasUsableSourceThumbnail() are mapped to data
URLs.
- Around line 3055-3066: The current logic prefers asBounds then falls back to
platform-specific resolution which can pick the frontmost app window rather than
the user-selected window; change the branch so for window sources you call
resolveMacWindowBounds(source) (on darwin) or resolveLinuxWindowBounds(source)
(on linux) first and set bounds from that result only if it returns a valid
value, and only then fall back to asBounds; keep the screen source path using
getDisplayBoundsForSource(source) unchanged and ensure you still check isWindow
before the platform-specific resolution.
- Around line 3015-3038: The AppleScript invocations in the execFileAsync calls
interpolate appName directly and are vulnerable to code injection; change both
usages to pass appName via osascript's argv (use an 'on run argv' wrapper that
assigns the first argv to a local variable) and remove any direct string
interpolation of appName. Specifically, update the main execFileAsync (the block
that returns x,y,w,h and assigns asBounds) and the fallback execFileAsync (the
'activate' fallback) to use an AppleScript script that starts with "on run argv"
/ "set appName to item 1 of argv" / ... / "end run" and then pass [appName] as
the argv argument to execFileAsync, keeping the same timeouts and the
surrounding logic (execFileAsync, stdout parsing, asBounds assignment)
unchanged.

---

Nitpick comments:
In `@electron/ipc/handlers.ts`:
- Around line 3381-3388: The three separate Date.now() calls create mismatched
basenames; replace them by capturing a single timestamp (e.g., const ts =
Date.now()) before constructing paths and use that ts when building outputPath,
systemAudioOutputPath and microphoneOutputPath (still honoring recordingsDir and
options?.capturesSystemAudio / options?.capturesMicrophone) so all recording
artifacts share the same basename.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a4a493ff-c24d-44d3-a9aa-8809e82d3894

📥 Commits

Reviewing files that changed from the base of the PR and between fc34288 and e7affcf.

📒 Files selected for processing (3)
  • .gitignore
  • electron/ipc/handlers.ts
  • electron/native/ScreenCaptureKitRecorder.swift

Comment on lines +1 to +5
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Revert CRLF line endings to LF (Unix-style).

The AI summary indicates these lines were changed to use Windows-style CRLF line endings. This is problematic for a cross-platform project, especially one addressing macOS bugs. .gitignore files should use LF (Unix) line endings to ensure consistent behavior across macOS, Linux, and Windows (Git automatically handles line ending conversions for repository files).

CRLF line endings in .gitignore can cause Git to malfunction on Unix-like systems, potentially failing to ignore specified files and directories.

Additionally, these formatting changes appear unrelated to the PR's stated objectives (fixing macOS video recording and cursor telemetry bugs).

🔧 Recommended action

Ensure the entire .gitignore file uses LF line endings. Most editors and Git tools can automatically convert line endings. If you're using Git, you can configure:

git config core.autocrlf input

Then re-stage the file to normalize line endings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 1 - 5, The .gitignore file contains Windows-style
CRLF line endings; revert them to LF (Unix-style) so Git ignores work
consistently across macOS/Linux; open the .gitignore, convert all CRLF to LF (or
run git normalization with core.autocrlf=input), save and re-stage the
.gitignore so the commit only changes line endings back to LF and remove any
unrelated whitespace edits.

sourceType: 'window' as const,
}
})
.filter((source) => Boolean(source.thumbnail))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Apply the usable-thumbnail check before converting to a data URL.

Boolean(source.thumbnail) only checks that a string exists after toDataURL(). It does not enforce the hasUsableSourceThumbnail() gate you already use in the fallback path, so unusable Electron thumbnails can still survive the merged macOS path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` at line 2949, Replace the current filter predicate
that only checks Boolean(source.thumbnail) with the usable-thumbnail gate so
unusable Electron thumbnails are excluded before data-URL conversion;
specifically, change the filter on the array (currently .filter((source) =>
Boolean(source.thumbnail))) to use hasUsableSourceThumbnail(source) or run
hasUsableSourceThumbnail(source) prior to calling source.toDataURL() (the place
where toDataURL() is invoked), ensuring only sources passing
hasUsableSourceThumbnail() are mapped to data URLs.

Comment on lines +3015 to +3038
const { stdout } = await execFileAsync('osascript', ['-e',
`tell application "${appName}"\n` +
` activate\n` +
`end tell\n` +
`delay 0.3\n` +
`tell application "System Events"\n` +
` tell process "${appName}"\n` +
` set frontWindow to front window\n` +
` set {x1, y1} to position of frontWindow\n` +
` set {w1, h1} to size of frontWindow\n` +
` return (x1 as text) & "," & (y1 as text) & "," & (w1 as text) & "," & (h1 as text)\n` +
` end tell\n` +
`end tell`
], { timeout: 4000 })
const parts = stdout.trim().split(',').map(Number)
if (parts.length === 4 && parts.every(n => Number.isFinite(n))) {
asBounds = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] }
}
} catch {
// Fallback: just activate without bounds
try {
await execFileAsync('osascript', ['-e',
`tell application "${appName}" to activate`
], { timeout: 2000 })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /root && find . -name "handlers.ts" -type f | head -20

Repository: webadderall/Recordly

Length of output: 119


🏁 Script executed:

# Check if the file exists and its size
wc -l electron/ipc/handlers.ts 2>/dev/null || find . -name "handlers.ts" -path "*/ipc/*" -type f

Repository: webadderall/Recordly

Length of output: 93


🏁 Script executed:

# Read the specific section mentioned
sed -n '3000,3050p' electron/ipc/handlers.ts

Repository: webadderall/Recordly

Length of output: 2356


🏁 Script executed:

# Also search for the appName variable definition and context around those lines
sed -n '2950,3100p' electron/ipc/handlers.ts

Repository: webadderall/Recordly

Length of output: 5555


🏁 Script executed:

# Search for other osascript calls in the file to see if there are similar patterns
rg -n "execFileAsync.*osascript" electron/ipc/handlers.ts -A 3

Repository: webadderall/Recordly

Length of output: 508


🏁 Script executed:

# Also check if there are other vulnerable string interpolation patterns with execFileAsync and shell commands
rg -n "execFileAsync.*\$\{.*\}" electron/ipc/handlers.ts | head -20

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

# Verify the AppleScript on run argv pattern is valid by checking if there are any existing uses in the codebase
rg -n "on run argv" . --type typescript --type javascript --type applescript

Repository: webadderall/Recordly

Length of output: 98


Fix AppleScript code injection vulnerability via appName parameter.

Both osascript calls (lines 3015 and 3036) interpolate appName directly into AppleScript without escaping. A quote or newline character in the app name can break out of the string literal and execute arbitrary AppleScript code. Pass the value via on run argv instead.

🔒 Safer pattern
-            const { stdout } = await execFileAsync('osascript', ['-e',
-              `tell application "${appName}"\n` +
-              `  activate\n` +
-              `end tell\n` +
-              `delay 0.3\n` +
-              `tell application "System Events"\n` +
-              `  tell process "${appName}"\n` +
-              `    set frontWindow to front window\n` +
-              `    set {x1, y1} to position of frontWindow\n` +
-              `    set {w1, h1} to size of frontWindow\n` +
-              `    return (x1 as text) & "," & (y1 as text) & "," & (w1 as text) & "," & (h1 as text)\n` +
-              `  end tell\n` +
-              `end tell`
-            ], { timeout: 4000 })
+            const script = `
+on run argv
+  set appName to item 1 of argv
+  tell application appName to activate
+  delay 0.3
+  tell application "System Events"
+    tell process appName
+      set frontWindow to front window
+      set {x1, y1} to position of frontWindow
+      set {w1, h1} to size of frontWindow
+      return (x1 as text) & "," & (y1 as text) & "," & (w1 as text) & "," & (h1 as text)
+    end tell
+  end tell
+end run`
+            const { stdout } = await execFileAsync('osascript', ['-e', script, appName], { timeout: 4000 })

Also apply the same fix to the fallback call at line 3036.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 3015 - 3038, The AppleScript
invocations in the execFileAsync calls interpolate appName directly and are
vulnerable to code injection; change both usages to pass appName via osascript's
argv (use an 'on run argv' wrapper that assigns the first argv to a local
variable) and remove any direct string interpolation of appName. Specifically,
update the main execFileAsync (the block that returns x,y,w,h and assigns
asBounds) and the fallback execFileAsync (the 'activate' fallback) to use an
AppleScript script that starts with "on run argv" / "set appName to item 1 of
argv" / ... / "end run" and then pass [appName] as the argv argument to
execFileAsync, keeping the same timeouts and the surrounding logic
(execFileAsync, stdout parsing, asBounds assignment) unchanged.

Comment on lines +3055 to 3066
let bounds = asBounds

if (source.id?.startsWith('screen:')) {
bounds = getDisplayBoundsForSource(source)
} else if (isWindow) {
if (process.platform === 'darwin') {
bounds = await resolveMacWindowBounds(source)
} else if (process.platform === 'linux') {
bounds = await resolveLinuxWindowBounds(source)
if (!bounds) {
if (source.id?.startsWith('screen:')) {
bounds = getDisplayBoundsForSource(source)
} else if (isWindow) {
if (process.platform === 'darwin') {
bounds = await resolveMacWindowBounds(source)
} else if (process.platform === 'linux') {
bounds = await resolveLinuxWindowBounds(source)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prefer the selected window's bounds over the app's front window.

asBounds is for whichever window becomes frontmost after activate, not necessarily the windowId the user selected. In multi-window apps this can highlight the wrong window. Use resolveMacWindowBounds(source) first and fall back to asBounds only if that lookup fails.

🎯 Suggested fix
-      let bounds = asBounds
-
-      if (!bounds) {
-        if (source.id?.startsWith('screen:')) {
-          bounds = getDisplayBoundsForSource(source)
-        } else if (isWindow) {
-          if (process.platform === 'darwin') {
-            bounds = await resolveMacWindowBounds(source)
-          } else if (process.platform === 'linux') {
-            bounds = await resolveLinuxWindowBounds(source)
-          }
-        }
-      }
+      let bounds: WindowBounds | null = null
+
+      if (source.id?.startsWith('screen:')) {
+        bounds = getDisplayBoundsForSource(source)
+      } else if (isWindow) {
+        if (process.platform === 'darwin') {
+          bounds = await resolveMacWindowBounds(source) ?? asBounds
+        } else if (process.platform === 'linux') {
+          bounds = await resolveLinuxWindowBounds(source)
+        }
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let bounds = asBounds
if (source.id?.startsWith('screen:')) {
bounds = getDisplayBoundsForSource(source)
} else if (isWindow) {
if (process.platform === 'darwin') {
bounds = await resolveMacWindowBounds(source)
} else if (process.platform === 'linux') {
bounds = await resolveLinuxWindowBounds(source)
if (!bounds) {
if (source.id?.startsWith('screen:')) {
bounds = getDisplayBoundsForSource(source)
} else if (isWindow) {
if (process.platform === 'darwin') {
bounds = await resolveMacWindowBounds(source)
} else if (process.platform === 'linux') {
bounds = await resolveLinuxWindowBounds(source)
}
}
let bounds: WindowBounds | null = null
if (source.id?.startsWith('screen:')) {
bounds = getDisplayBoundsForSource(source)
} else if (isWindow) {
if (process.platform === 'darwin') {
bounds = await resolveMacWindowBounds(source) ?? asBounds
} else if (process.platform === 'linux') {
bounds = await resolveLinuxWindowBounds(source)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 3055 - 3066, The current logic prefers
asBounds then falls back to platform-specific resolution which can pick the
frontmost app window rather than the user-selected window; change the branch so
for window sources you call resolveMacWindowBounds(source) (on darwin) or
resolveLinuxWindowBounds(source) (on linux) first and set bounds from that
result only if it returns a valid value, and only then fall back to asBounds;
keep the screen source path using getDisplayBoundsForSource(source) unchanged
and ensure you still check isWindow before the platform-specific resolution.

@webadderall
Copy link
Copy Markdown
Owner

issue seems to be resolved but thank you for the PR!

@webadderall webadderall closed this Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v 1.1.8 on MacOS Decoding & Cursor Bugs

2 participants