diff --git a/electron/native/screencapturekit/Sources/OpenScreenCaptureCore/VideoColor.swift b/electron/native/screencapturekit/Sources/OpenScreenCaptureCore/VideoColor.swift new file mode 100644 index 000000000..35b30eed0 --- /dev/null +++ b/electron/native/screencapturekit/Sources/OpenScreenCaptureCore/VideoColor.swift @@ -0,0 +1,22 @@ +import AVFoundation +import CoreGraphics + +// The colour every recording is decoded as: BT.709 primaries, transfer and matrix, studio +// range. The compositor assumes it for every file, and the Windows (#929) and Linux (#932) +// helpers write it. Left to the defaults, ScreenCaptureKit hands over the display's own colour +// space (P3 on most Macs) and VideoToolbox picks the matrix and the tags +// (getopenscreen/openscreen#943). +// +// Range has no key here: it comes from the pixel format, and the stream captures `420v`, the +// studio-range one. + +/// The matrix ScreenCaptureKit converts to YCbCr with. It has to be the one the file is tagged +/// with in `videoColorProperties`, which `VideoColorTests` pins. +public let captureYCbCrMatrix: CFString = CGDisplayStream.yCbCrMatrix_ITU_R_709_2 + +/// `AVVideoColorPropertiesKey` for the H.264 writer input. +public let videoColorProperties: [String: String] = [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2, +] diff --git a/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift b/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift index 054b29904..af19675d1 100644 --- a/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift +++ b/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift @@ -701,7 +701,15 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { configuration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(max(1, request.video.fps))) configuration.queueDepth = 6 configuration.showsCursor = !request.video.hideSystemCursor - configuration.pixelFormat = kCVPixelFormatType_32BGRA + // Studio-range BT.709 YCbCr from ScreenCaptureKit itself, which the encoder takes as is + // and the compositor decodes (#943). BGRA left the matrix to VideoToolbox. Nothing here + // reads the pixels: a frame goes through `retimedSampleBuffer` (timing only) to + // `append`, and `isCompleteFrame` reads attachments. 1.5 bytes a pixel instead of 4, + // times `queueDepth`. + configuration.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + configuration.colorMatrix = captureYCbCrMatrix + // Unset, the buffers carry the display's colour space: P3 on most Macs. + configuration.colorSpaceName = CGColorSpace.sRGB configuration.sampleRate = 48_000 configuration.channelCount = 2 configuration.excludesCurrentProcessAudio = true @@ -754,6 +762,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: outputWidth, AVVideoHeightKey: outputHeight, + // BT.709 tags, the colour the stream is captured in (#943). + AVVideoColorPropertiesKey: videoColorProperties, AVVideoCompressionPropertiesKey: [ // From the size this stream really got. The renderer sends none (#924). AVVideoAverageBitRateKey: request.video.bitrate diff --git a/electron/native/screencapturekit/Tests/OpenScreenCaptureCoreTests/VideoColorTests.swift b/electron/native/screencapturekit/Tests/OpenScreenCaptureCoreTests/VideoColorTests.swift new file mode 100644 index 000000000..2a423d2b9 --- /dev/null +++ b/electron/native/screencapturekit/Tests/OpenScreenCaptureCoreTests/VideoColorTests.swift @@ -0,0 +1,18 @@ +import AVFoundation +import XCTest + +import OpenScreenCaptureCore + +/// The file has to say what its samples are: a matrix that converts with one standard and tags +/// another is the shifted-hue defect #943 exists to remove. +final class VideoColorTests: XCTestCase { + func testCaptureMatrixIsTheOneTheFileIsTaggedWith() { + XCTAssertEqual(captureYCbCrMatrix as String, videoColorProperties[AVVideoYCbCrMatrixKey]) + } + + func testFileIsTaggedBT709() { + XCTAssertEqual(videoColorProperties[AVVideoColorPrimariesKey], "ITU_R_709_2") + XCTAssertEqual(videoColorProperties[AVVideoTransferFunctionKey], "ITU_R_709_2") + XCTAssertEqual(videoColorProperties[AVVideoYCbCrMatrixKey], "ITU_R_709_2") + } +}