mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 06:46:09 +00:00
Correct colour handling during recording
Record captured frames with BT.709 conversion and explicit limited-range metadata. Share the Windows conversion code between capture engines and apply matching colour metadata on macOS and FFmpeg fallbacks.
This commit is contained in:
@@ -25,12 +25,22 @@ export function getDisplayWorkAreaForSource(source: SelectedSource) {
|
||||
export async function buildFfmpegCaptureArgs(source: SelectedSource, outputPath: string) {
|
||||
const commonOutputArgs = [
|
||||
"-an",
|
||||
"-vf",
|
||||
"scale=in_range=full:out_range=tv:sws_dither=a_dither",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-colorspace",
|
||||
"bt709",
|
||||
"-color_primaries",
|
||||
"bt709",
|
||||
"-color_trc",
|
||||
"bt709",
|
||||
"-color_range",
|
||||
"tv",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
outputPath,
|
||||
|
||||
@@ -180,6 +180,11 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
|
||||
outputSettings[AVVideoWidthKey] = outputWidth
|
||||
outputSettings[AVVideoHeightKey] = outputHeight
|
||||
outputSettings[AVVideoColorPropertiesKey] = [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2,
|
||||
]
|
||||
|
||||
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
|
||||
videoInput.expectsMediaDataInRealTime = true
|
||||
|
||||
@@ -40,3 +40,12 @@ describe("ScreenCaptureKitRecorder resume timing", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ScreenCaptureKitRecorder colour metadata", () => {
|
||||
it("tags recordings as BT.709", () => {
|
||||
expect(recorderSource).toContain("AVVideoColorPropertiesKey");
|
||||
expect(recorderSource).toContain("AVVideoColorPrimaries_ITU_R_709_2");
|
||||
expect(recorderSource).toContain("AVVideoTransferFunction_ITU_R_709_2");
|
||||
expect(recorderSource).toContain("AVVideoYCbCrMatrix_ITU_R_709_2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <vector>
|
||||
|
||||
inline int clampVideoSample(int value, int minimum, int maximum) {
|
||||
return value < minimum ? minimum : (value > maximum ? maximum : value);
|
||||
}
|
||||
|
||||
inline HRESULT setBt709LimitedVideoAttributes(IMFMediaType* mediaType) {
|
||||
HRESULT result = mediaType->SetUINT32(MF_MT_VIDEO_PRIMARIES, MFVideoPrimaries_BT709);
|
||||
if (FAILED(result)) return result;
|
||||
result = mediaType->SetUINT32(MF_MT_TRANSFER_FUNCTION, MFVideoTransFunc_709);
|
||||
if (FAILED(result)) return result;
|
||||
result = mediaType->SetUINT32(MF_MT_YUV_MATRIX, MFVideoTransferMatrix_BT709);
|
||||
if (FAILED(result)) return result;
|
||||
return mediaType->SetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, MFNominalRange_16_235);
|
||||
}
|
||||
|
||||
inline void convertBgraToBt709LimitedNv12(
|
||||
const uint8_t* bgra,
|
||||
int bgraPitch,
|
||||
int width,
|
||||
int height,
|
||||
std::vector<uint8_t>& nv12Buffer) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uint8_t* pixel = bgra + y * bgraPitch + x * 4;
|
||||
const int blue = pixel[0];
|
||||
const int green = pixel[1];
|
||||
const int red = pixel[2];
|
||||
const int luma = ((47 * red + 157 * green + 16 * blue + 128) >> 8) + 16;
|
||||
nv12Buffer[y * width + x] = static_cast<uint8_t>(clampVideoSample(luma, 16, 235));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* uvPlane = nv12Buffer.data() + width * height;
|
||||
for (int y = 0; y < height; y += 2) {
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
for (int offsetY = 0; offsetY < 2; ++offsetY) {
|
||||
for (int offsetX = 0; offsetX < 2; ++offsetX) {
|
||||
const uint8_t* pixel =
|
||||
bgra + (y + offsetY) * bgraPitch + (x + offsetX) * 4;
|
||||
blue += pixel[0];
|
||||
green += pixel[1];
|
||||
red += pixel[2];
|
||||
}
|
||||
}
|
||||
red = (red + 2) / 4;
|
||||
green = (green + 2) / 4;
|
||||
blue = (blue + 2) / 4;
|
||||
|
||||
// Coefficients sum to zero so neutral greys remain neutral after quantization.
|
||||
const int chromaBlue = ((-26 * red - 87 * green + 113 * blue + 128) >> 8) + 128;
|
||||
const int chromaRed = ((112 * red - 102 * green - 10 * blue + 128) >> 8) + 128;
|
||||
const int uvIndex = (y / 2) * width + x;
|
||||
uvPlane[uvIndex] = static_cast<uint8_t>(clampVideoSample(chromaBlue, 16, 240));
|
||||
uvPlane[uvIndex + 1] = static_cast<uint8_t>(clampVideoSample(chromaRed, 16, 240));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,13 @@
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include "../../common/bt709_video.h"
|
||||
|
||||
#pragma comment(lib, "mfplat.lib")
|
||||
#pragma comment(lib, "mfreadwrite.lib")
|
||||
#pragma comment(lib, "mf.lib")
|
||||
#pragma comment(lib, "mfuuid.lib")
|
||||
|
||||
static int clampByte(int v) {
|
||||
return v < 0 ? 0 : (v > 255 ? 255 : v);
|
||||
}
|
||||
|
||||
static UINT32 calculateScreenRecordingBitrate(int width, int height, int fps) {
|
||||
constexpr uint64_t kFourKPixels = 3840ULL * 2160ULL;
|
||||
constexpr uint64_t kQhdPixels = 2560ULL * 1440ULL;
|
||||
@@ -82,6 +79,8 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height
|
||||
MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, fps_, 1);
|
||||
MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
|
||||
outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
|
||||
hr = setBt709LimitedVideoAttributes(outputType.Get());
|
||||
if (FAILED(hr)) return false;
|
||||
std::cerr << "Encoder bitrate: " << videoBitrate << " bps for "
|
||||
<< width_ << "x" << height_ << "@" << fps_ << "fps" << std::endl;
|
||||
|
||||
@@ -96,6 +95,8 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height
|
||||
MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, fps_, 1);
|
||||
MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
|
||||
inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
|
||||
hr = setBt709LimitedVideoAttributes(inputType.Get());
|
||||
if (FAILED(hr)) return false;
|
||||
|
||||
// Create SinkWriter with MPEG4 container
|
||||
ComPtr<IMFAttributes> writerAttrs;
|
||||
@@ -225,34 +226,10 @@ bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) {
|
||||
HRESULT hr = context_->Map(stagingTexture_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
|
||||
if (FAILED(hr)) return false;
|
||||
|
||||
// Convert BGRA → NV12
|
||||
// Convert full-range desktop BGRA to explicitly tagged BT.709 video-range NV12.
|
||||
const uint8_t* bgra = static_cast<const uint8_t*>(mapped.pData);
|
||||
const int bgraPitch = static_cast<int>(mapped.RowPitch);
|
||||
|
||||
// Y plane
|
||||
for (int y = 0; y < height_; y++) {
|
||||
for (int x = 0; x < width_; x++) {
|
||||
const uint8_t* pixel = bgra + y * bgraPitch + x * 4;
|
||||
uint8_t b = pixel[0], g = pixel[1], r = pixel[2];
|
||||
int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
|
||||
nv12Buffer_[y * width_ + x] = static_cast<uint8_t>(clampByte(yVal));
|
||||
}
|
||||
}
|
||||
|
||||
// UV plane (interleaved, subsampled 2x2)
|
||||
const int ySize = width_ * height_;
|
||||
uint8_t* uvPlane = nv12Buffer_.data() + ySize;
|
||||
for (int y = 0; y < height_; y += 2) {
|
||||
for (int x = 0; x < width_; x += 2) {
|
||||
const uint8_t* pixel = bgra + y * bgraPitch + x * 4;
|
||||
uint8_t b = pixel[0], g = pixel[1], r = pixel[2];
|
||||
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
|
||||
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
|
||||
int uvIdx = (y / 2) * width_ + (x / 2) * 2;
|
||||
uvPlane[uvIdx] = static_cast<uint8_t>(clampByte(u));
|
||||
uvPlane[uvIdx + 1] = static_cast<uint8_t>(clampByte(v));
|
||||
}
|
||||
}
|
||||
convertBgraToBt709LimitedNv12(bgra, bgraPitch, width_, height_, nv12Buffer_);
|
||||
|
||||
context_->Unmap(stagingTexture_.Get(), 0);
|
||||
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
#include <codecapi.h>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include "../../common/bt709_video.h"
|
||||
|
||||
#pragma comment(lib, "mfplat.lib")
|
||||
#pragma comment(lib, "mfreadwrite.lib")
|
||||
#pragma comment(lib, "mf.lib")
|
||||
#pragma comment(lib, "mfuuid.lib")
|
||||
|
||||
static int clampByte(int v) {
|
||||
return v < 0 ? 0 : (v > 255 ? 255 : v);
|
||||
}
|
||||
|
||||
MFEncoder::MFEncoder() {}
|
||||
|
||||
MFEncoder::~MFEncoder() {
|
||||
@@ -53,6 +50,8 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height
|
||||
MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, fps_, 1);
|
||||
MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
|
||||
outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
|
||||
hr = setBt709LimitedVideoAttributes(outputType.Get());
|
||||
if (FAILED(hr)) return false;
|
||||
|
||||
// Input media type (NV12)
|
||||
ComPtr<IMFMediaType> inputType;
|
||||
@@ -65,6 +64,8 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height
|
||||
MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, fps_, 1);
|
||||
MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
|
||||
inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
|
||||
hr = setBt709LimitedVideoAttributes(inputType.Get());
|
||||
if (FAILED(hr)) return false;
|
||||
|
||||
// Create SinkWriter with MPEG4 container
|
||||
ComPtr<IMFAttributes> writerAttrs;
|
||||
@@ -132,34 +133,10 @@ bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) {
|
||||
HRESULT hr = context_->Map(stagingTexture_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
|
||||
if (FAILED(hr)) return false;
|
||||
|
||||
// Convert BGRA → NV12
|
||||
// Convert full-range desktop BGRA to explicitly tagged BT.709 video-range NV12.
|
||||
const uint8_t* bgra = static_cast<const uint8_t*>(mapped.pData);
|
||||
const int bgraPitch = static_cast<int>(mapped.RowPitch);
|
||||
|
||||
// Y plane
|
||||
for (int y = 0; y < height_; y++) {
|
||||
for (int x = 0; x < width_; x++) {
|
||||
const uint8_t* pixel = bgra + y * bgraPitch + x * 4;
|
||||
uint8_t b = pixel[0], g = pixel[1], r = pixel[2];
|
||||
int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
|
||||
nv12Buffer_[y * width_ + x] = static_cast<uint8_t>(clampByte(yVal));
|
||||
}
|
||||
}
|
||||
|
||||
// UV plane (interleaved, subsampled 2x2)
|
||||
const int ySize = width_ * height_;
|
||||
uint8_t* uvPlane = nv12Buffer_.data() + ySize;
|
||||
for (int y = 0; y < height_; y += 2) {
|
||||
for (int x = 0; x < width_; x += 2) {
|
||||
const uint8_t* pixel = bgra + y * bgraPitch + x * 4;
|
||||
uint8_t b = pixel[0], g = pixel[1], r = pixel[2];
|
||||
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
|
||||
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
|
||||
int uvIdx = (y / 2) * width_ + (x / 2) * 2;
|
||||
uvPlane[uvIdx] = static_cast<uint8_t>(clampByte(u));
|
||||
uvPlane[uvIdx + 1] = static_cast<uint8_t>(clampByte(v));
|
||||
}
|
||||
}
|
||||
convertBgraToBt709LimitedNv12(bgra, bgraPitch, width_, height_, nv12Buffer_);
|
||||
|
||||
context_->Unmap(stagingTexture_.Get(), 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user