diff --git a/electron/native/ScreenCaptureKitRecorder.swift b/electron/native/ScreenCaptureKitRecorder.swift index 99980f86..6e4b0efd 100644 --- a/electron/native/ScreenCaptureKitRecorder.swift +++ b/electron/native/ScreenCaptureKitRecorder.swift @@ -394,13 +394,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { guard sessionStarted, sampleBuffer.isValid, isRecording else { return } - guard let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } if outputType == .screen { - if frameCount > 0 && CMTimeCompare(presentationTime, lastVideoPresentationTime) <= 0 { - return - } - guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], let attachment = attachments.first, let statusRawValue = attachment[SCStreamFrameInfo.status] as? Int, @@ -413,8 +408,10 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { assetWriter?.status == .writing, videoInput.isReadyForMoreMediaData else { return } - if firstSampleTime == .zero { - firstSampleTime = sampleBuffer.presentationTimeStamp + // Only a complete frame that the writer can accept may establish time zero. + guard let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } + if frameCount > 0 && CMTimeCompare(presentationTime, lastVideoPresentationTime) <= 0 { + return } lastSampleBuffer = sampleBuffer @@ -439,10 +436,16 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { print("Recording started") fflush(stdout) } + } else if frameCount == 0 { + // A failed crop/append must not leave an empty interval before frame one. + firstSampleTime = .zero } return } + guard frameCount > 0, + let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } + if outputType == .audio { guard let systemAudioInput else { return } appendAudioSampleBuffer(sampleBuffer, to: systemAudioInput, of: systemAudioWriter, firstSampleTime: &firstSystemAudioSampleTime, lastPresentationTime: &lastSystemAudioPresentationTime, presentationTime: presentationTime) diff --git a/electron/native/ScreenCaptureKitRecorder.test.ts b/electron/native/ScreenCaptureKitRecorder.test.ts index a885d75c..54ae01b5 100644 --- a/electron/native/ScreenCaptureKitRecorder.test.ts +++ b/electron/native/ScreenCaptureKitRecorder.test.ts @@ -84,3 +84,19 @@ describe("ScreenCaptureKitRecorder window capture", () => { expect(recorderSource).toContain("self.windowCropRect = cropRect"); }); }); + + +describe("ScreenCaptureKitRecorder first frame timing", () => { + const callback = recorderSource.slice(recorderSource.indexOf("func stream(_ stream:"), recorderSource.indexOf("func stream(_ stream:") + 5000); + it("validates a complete frame and writer readiness before setting time zero", () => { + const clock = callback.indexOf("adjustedPresentationTime(for:"); + expect(clock).toBeGreaterThan(callback.indexOf("status == .complete")); + expect(clock).toBeGreaterThan(callback.indexOf("videoInput.isReadyForMoreMediaData")); + }); + it("resets the origin after a rejected first frame and gates audio on accepted video", () => { + expect(callback).toMatch(/else if frameCount == 0\s*\{[^}]*firstSampleTime = \.zero/); + const audioGuard = callback.indexOf("guard frameCount > 0,"); + expect(audioGuard).toBeGreaterThan(0); + expect(audioGuard).toBeLessThan(callback.indexOf("if outputType == .audio")); + }); +}); diff --git a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper index 52c23c0e..5a89a2a4 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper and b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper differ