diff --git a/electron/native/ScreenCaptureKitRecorder.swift b/electron/native/ScreenCaptureKitRecorder.swift index ae8f9a77..d97cb304 100644 --- a/electron/native/ScreenCaptureKitRecorder.swift +++ b/electron/native/ScreenCaptureKitRecorder.swift @@ -378,7 +378,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { } } - assetWriter?.endSession(atSourceTime: lastSampleBuffer?.presentationTimeStamp ?? .zero) + let endTime = lastVideoPresentationTime + (lastSampleBuffer.map { frameDuration(for: $0) } ?? .zero) + assetWriter?.endSession(atSourceTime: endTime) videoInput?.markAsFinished() inlineAudioInput?.markAsFinished() await assetWriter?.finishWriting() @@ -438,10 +439,19 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { if firstSampleTime == .zero { firstSampleTime = sampleTime } - return max(.zero, sampleTime - firstSampleTime - accumulatedPausedDuration) } - return sampleTime - accumulatedPausedDuration + // Use video's first sample time as the common time base for ALL tracks. + // This ensures audio files contain leading silence when audio hardware + // delivers its first sample after the first video frame (e.g. iPhone mic + // over Continuity Camera can lag 1-2 seconds behind). + if firstSampleTime == .zero { + // Video hasn't started yet — drop this audio sample to avoid + // negative timestamps. + return nil + } + + return max(.zero, sampleTime - firstSampleTime - accumulatedPausedDuration) } private func frameDuration(for sampleBuffer: CMSampleBuffer) -> CMTime { @@ -463,9 +473,9 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { firstSampleTime = presentationTime } - guard let firstSampleTime else { return } - let relativePresentationTime = max(.zero, presentationTime - firstSampleTime) - let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: relativePresentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp) + // presentationTime is already relative to the video's first frame + // (computed by adjustedPresentationTime), so use it directly. + let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: presentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp) if let retimedSampleBuffer = try? CMSampleBuffer(copying: sampleBuffer, withNewTiming: [timing]) { input.append(retimedSampleBuffer) } diff --git a/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper b/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper index 67e5898f..ef056fdb 100755 Binary files a/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper and b/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-x64/openscreen-native-cursor-monitor b/electron/native/bin/darwin-x64/openscreen-native-cursor-monitor new file mode 100755 index 00000000..58bdf488 Binary files /dev/null and b/electron/native/bin/darwin-x64/openscreen-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-x64/openscreen-screencapturekit-helper b/electron/native/bin/darwin-x64/openscreen-screencapturekit-helper new file mode 100755 index 00000000..35532ca1 Binary files /dev/null and b/electron/native/bin/darwin-x64/openscreen-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-x64/openscreen-system-cursors b/electron/native/bin/darwin-x64/openscreen-system-cursors new file mode 100755 index 00000000..2c53fd68 Binary files /dev/null and b/electron/native/bin/darwin-x64/openscreen-system-cursors differ diff --git a/electron/native/bin/darwin-x64/openscreen-window-list b/electron/native/bin/darwin-x64/openscreen-window-list new file mode 100755 index 00000000..949a66bd Binary files /dev/null and b/electron/native/bin/darwin-x64/openscreen-window-list differ