Skip to content

Commit 2ad2293

Browse files
committed
fix(macos): pin the H.264 keyframe interval to 1 s and count dropped video frames
The GOP was left to VideoToolbox; it is now one second by frame count and by duration, like Windows. Frames the writer input was not ready for or refused were dropped silently; recording-stopped now carries droppedVideoFrames.
1 parent 32857bb commit 2ad2293

4 files changed

Lines changed: 35 additions & 1 deletion

File tree

‎electron/native/screencapturekit/Sources/OpenScreenCaptureCore/CaptureSizing.swift‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,10 @@ public func defaultVideoBitrate(width: Int, height: Int, fps: Int) -> Int {
8686
let pixelsPerSecond = Double(max(1, width)) * Double(max(1, height)) * Double(max(1, fps))
8787
return min(max(Int(pixelsPerSecond * bitsPerPixel), 2_000_000), 60_000_000)
8888
}
89+
90+
/// The H.264 keyframe interval, in frames: one second of video, the interval Windows uses.
91+
/// Left to VideoToolbox it had no bound, and the editor's scrub cost is the GOP length: a
92+
/// seek decodes from the keyframe before it (getopenscreen/openscreen#937).
93+
public func videoKeyFrameInterval(fps: Int) -> Int {
94+
max(1, fps)
95+
}

‎electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
152152
private var totalPausedDuration = CMTime.zero
153153
/// Sample queue only. See `VideoTimestampGate` for the failure it exists to prevent.
154154
private var videoTimestampGate = VideoTimestampGate()
155+
/// Sample queue only. Frames the writer input was not ready for, or refused, reported in
156+
/// `recording-stopped` so a macOS drop rate can be measured at all (#937).
157+
private var droppedVideoFrames = 0
155158
private var nativeMicrophoneEnabled = false
156159
private var outputWidth = 1920
157160
private var outputHeight = 1080
@@ -454,8 +457,11 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
454457
"captureBounds": captureBoundsPayload(),
455458
])
456459
} else if !appended {
460+
droppedVideoFrames += 1
457461
reportWriterFailure("video append")
458462
}
463+
} else {
464+
droppedVideoFrames += 1
459465
}
460466
}
461467

@@ -786,6 +792,11 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
786792
// particular run happened to die. Reordering off is 3/3 clean across
787793
// both rates, and a SIGKILL at 25s still leaves 27 readable `moof`.
788794
AVVideoAllowFrameReorderingKey: false,
795+
// A keyframe every second, by frame count and by time: a still screen
796+
// delivers fewer frames than the fps, and the count alone would then
797+
// stretch the GOP, which is the editor's scrub cost (#937).
798+
AVVideoMaxKeyFrameIntervalKey: videoKeyFrameInterval(fps: request.video.fps),
799+
AVVideoMaxKeyFrameIntervalDurationKey: 1,
789800
],
790801
]
791802
let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
@@ -908,7 +919,9 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
908919
}
909920
}
910921

911-
let refusedVideoFrames = sampleQueue.sync { videoTimestampGate.rejectedCount }
922+
let (refusedVideoFrames, droppedFrames) = sampleQueue.sync {
923+
(videoTimestampGate.rejectedCount, droppedVideoFrames)
924+
}
912925
if refusedVideoFrames > 1 {
913926
emit([
914927
"event": "warning",
@@ -931,6 +944,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
931944
emit([
932945
"event": "recording-stopped",
933946
"screenPath": request.outputs.screenPath,
947+
"droppedVideoFrames": droppedFrames,
934948
])
935949
} else {
936950
emitError(

‎electron/native/screencapturekit/Tests/OpenScreenCaptureCoreTests/CaptureSizingTests.swift‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,15 @@ final class CaptureSizingTests: XCTestCase {
150150
XCTAssertEqual(defaultVideoBitrate(width: 320, height: 240, fps: 30), 2_000_000)
151151
XCTAssertEqual(defaultVideoBitrate(width: 3840, height: 2160, fps: 120), 60_000_000)
152152
}
153+
154+
// MARK: - Keyframe interval (#937)
155+
156+
func testKeyFrameIntervalIsOneSecondOfFrames() {
157+
XCTAssertEqual(videoKeyFrameInterval(fps: 60), 60)
158+
XCTAssertEqual(videoKeyFrameInterval(fps: 30), 30)
159+
}
160+
161+
func testKeyFrameIntervalNeverFallsBelowOneFrame() {
162+
XCTAssertEqual(videoKeyFrameInterval(fps: 0), 1)
163+
}
153164
}

‎src/lib/nativeMacRecording.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export type NativeMacHelperRecordingStartedEvent = {
6666
export type NativeMacHelperRecordingStoppedEvent = {
6767
event: "recording-stopped";
6868
screenPath: string;
69+
/** Frames the writer was not ready for or refused. Absent from helpers built before #937. */
70+
droppedVideoFrames?: number;
6971
};
7072

7173
export type NativeMacHelperWarningEvent = {

0 commit comments

Comments
 (0)