Start recordings at the first accepted video frame

This commit is contained in:
webadderall
2026-09-15 17:36:21 +10:00
parent 709388fa79
commit 8eba37c792
3 changed files with 26 additions and 7 deletions
+10 -7
View File
@@ -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)
@@ -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"));
});
});