feat: Support 10-bit Video HDR (#1827)

* feat: Select 10-bit YUV HDR format if HDR is enabled

* fix: Remove video EDR setting in favor of new 10-bit video HDR

* Format Swift
This commit is contained in:
Marc Rousavy 2023-09-21 16:30:05 +02:00 committed by GitHub
parent cf4882b152
commit 9809075507
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 42 deletions

View File

@ -115,6 +115,7 @@ enum FormatError {
case invalidFps(fps: Int) case invalidFps(fps: Int)
case invalidHdr case invalidHdr
case invalidFormat case invalidFormat
case incompatiblePixelFormatWithHDR
var code: String { var code: String {
switch self { switch self {
@ -124,6 +125,8 @@ enum FormatError {
return "invalid-fps" return "invalid-fps"
case .invalidHdr: case .invalidHdr:
return "invalid-hdr" return "invalid-hdr"
case .incompatiblePixelFormatWithHDR:
return "incompatible-pixel-format-with-hdr-setting"
} }
} }
@ -134,7 +137,9 @@ enum FormatError {
case let .invalidFps(fps): case let .invalidFps(fps):
return "The given format cannot run at \(fps) FPS! Make sure your FPS is lower than `format.maxFps` but higher than `format.minFps`." return "The given format cannot run at \(fps) FPS! Make sure your FPS is lower than `format.maxFps` but higher than `format.minFps`."
case .invalidHdr: case .invalidHdr:
return "The currently selected format does not support HDR capture! Make sure you select a format which includes `supportsPhotoHDR`!" return "The currently selected format does not support HDR capture! Make sure you select a format which includes `supportsPhotoHDR`/`supportsVideoHDR`!"
case .incompatiblePixelFormatWithHDR:
return "The currently selected pixelFormat is not compatible with HDR! HDR only works with the `yuv` pixelFormat."
} }
} }
} }

View File

@ -117,34 +117,10 @@ extension CameraView {
videoOutput!.setSampleBufferDelegate(self, queue: CameraQueues.videoQueue) videoOutput!.setSampleBufferDelegate(self, queue: CameraQueues.videoQueue)
videoOutput!.alwaysDiscardsLateVideoFrames = false videoOutput!.alwaysDiscardsLateVideoFrames = false
if let pixelFormat = pixelFormat as? String { let pixelFormatType = getPixelFormat(videoOutput: videoOutput!)
let supportedPixelFormats = videoOutput!.availableVideoPixelFormatTypes
let defaultFormat = supportedPixelFormats.first! // first value is always the most efficient format
var pixelFormatType: OSType = defaultFormat
switch pixelFormat {
case "yuv":
if supportedPixelFormats.contains(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
} else if supportedPixelFormats.contains(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
} else {
invokeOnError(.device(.pixelFormatNotSupported))
}
case "rgb":
if supportedPixelFormats.contains(kCVPixelFormatType_32BGRA) {
pixelFormatType = kCVPixelFormatType_32BGRA
} else {
invokeOnError(.device(.pixelFormatNotSupported))
}
case "native":
pixelFormatType = defaultFormat
default:
invokeOnError(.parameter(.invalid(unionName: "pixelFormat", receivedValue: pixelFormat)))
}
videoOutput!.videoSettings = [ videoOutput!.videoSettings = [
String(kCVPixelBufferPixelFormatTypeKey): pixelFormatType, String(kCVPixelBufferPixelFormatTypeKey): pixelFormatType,
] ]
}
captureSession.addOutput(videoOutput!) captureSession.addOutput(videoOutput!)
} }
@ -157,6 +133,64 @@ extension CameraView {
ReactLogger.log(level: .info, message: "Session successfully configured!") ReactLogger.log(level: .info, message: "Session successfully configured!")
} }
/**
Returns the pixel format that should be used for the AVCaptureVideoDataOutput.
If HDR is enabled, this will return YUV 4:2:0 10-bit.
If HDR is disabled, this will return whatever the user specified as a pixelFormat, or the most efficient format as a fallback.
*/
private func getPixelFormat(videoOutput: AVCaptureVideoDataOutput) -> OSType {
let supportedPixelFormats = videoOutput.availableVideoPixelFormatTypes
// as per documentation, the first value is always the most efficient format
let defaultFormat = supportedPixelFormats.first!
// If the user enabled HDR, we can only use the YUV 4:2:0 10-bit pixel format.
if hdr == true {
guard pixelFormat == nil || pixelFormat == "yuv" else {
invokeOnError(.format(.incompatiblePixelFormatWithHDR))
return defaultFormat
}
guard supportedPixelFormats.contains(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange) else {
invokeOnError(.format(.invalidHdr))
return defaultFormat
}
// YUV 4:2:0 10-bit
return kCVPixelFormatType_420YpCbCr10BiPlanarFullRange
}
// If the user didn't specify a custom pixelFormat, just return the default one.
guard let pixelFormat = pixelFormat else {
return defaultFormat
}
// If we don't use HDR, we can use any other custom pixel format.
switch pixelFormat {
case "yuv":
if supportedPixelFormats.contains(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
// YUV 4:2:0 8-bit (full video colors)
return kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
} else if supportedPixelFormats.contains(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
// YUV 4:2:0 8-bit (limited video colors)
return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
} else {
invokeOnError(.device(.pixelFormatNotSupported))
return defaultFormat
}
case "rgb":
if supportedPixelFormats.contains(kCVPixelFormatType_32BGRA) {
// RGBA 8-bit
return kCVPixelFormatType_32BGRA
} else {
invokeOnError(.device(.pixelFormatNotSupported))
return defaultFormat
}
case "native":
return defaultFormat
default:
invokeOnError(.parameter(.invalid(unionName: "pixelFormat", receivedValue: pixelFormat as String)))
return defaultFormat
}
}
// pragma MARK: Configure Device // pragma MARK: Configure Device
/** /**
@ -188,17 +222,6 @@ extension CameraView {
device.activeVideoMinFrameDuration = CMTime.invalid device.activeVideoMinFrameDuration = CMTime.invalid
device.activeVideoMaxFrameDuration = CMTime.invalid device.activeVideoMaxFrameDuration = CMTime.invalid
} }
if hdr != nil {
if hdr == true && !device.activeFormat.isVideoHDRSupported {
invokeOnError(.format(.invalidHdr))
return
}
if !device.automaticallyAdjustsVideoHDREnabled {
if device.isVideoHDREnabled != hdr!.boolValue {
device.isVideoHDREnabled = hdr!.boolValue
}
}
}
if lowLightBoost != nil { if lowLightBoost != nil {
if lowLightBoost == true && !device.isLowLightBoostSupported { if lowLightBoost == true && !device.isLowLightBoostSupported {
invokeOnError(.device(.lowLightBoostNotSupported)) invokeOnError(.device(.lowLightBoostNotSupported))

View File

@ -26,9 +26,9 @@ private let propsThatRequireReconfiguration = ["cameraId",
"photo", "photo",
"video", "video",
"enableFrameProcessor", "enableFrameProcessor",
"hdr",
"pixelFormat"] "pixelFormat"]
private let propsThatRequireDeviceReconfiguration = ["fps", private let propsThatRequireDeviceReconfiguration = ["fps",
"hdr",
"lowLightBoost"] "lowLightBoost"]
// MARK: - CameraView // MARK: - CameraView

View File

@ -50,7 +50,7 @@ extension AVCaptureDevice.Format {
"minISO": minISO, "minISO": minISO,
"fieldOfView": videoFieldOfView, "fieldOfView": videoFieldOfView,
"maxZoom": videoMaxZoomFactor, "maxZoom": videoMaxZoomFactor,
"supportsVideoHDR": isVideoHDRSupported, "supportsVideoHDR": availablePixelFormats.contains(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange),
"supportsPhotoHDR": false, "supportsPhotoHDR": false,
"minFps": minFrameRate, "minFps": minFrameRate,
"maxFps": maxFrameRate, "maxFps": maxFrameRate,

View File

@ -18,6 +18,7 @@ export type DeviceError =
export type FormatError = export type FormatError =
| 'format/invalid-fps' | 'format/invalid-fps'
| 'format/invalid-hdr' | 'format/invalid-hdr'
| 'format/incompatible-pixel-format-with-hdr-setting'
| 'format/invalid-low-light-boost' | 'format/invalid-low-light-boost'
| 'format/invalid-format' | 'format/invalid-format'
| 'format/invalid-color-space'; | 'format/invalid-color-space';