diff --git a/docs/docs/guides/RECORDING_VIDEOS.mdx b/docs/docs/guides/RECORDING_VIDEOS.mdx index 7f5d1ef8f6..99a5dc2f5d 100644 --- a/docs/docs/guides/RECORDING_VIDEOS.mdx +++ b/docs/docs/guides/RECORDING_VIDEOS.mdx @@ -54,7 +54,7 @@ camera.current.startRecording({ }) ``` -You can customize capture options such as [video codec](/docs/api/interfaces/RecordVideoOptions#videocodec), [video bit-rate](/docs/api/interfaces/RecordVideoOptions#videobitrate), [file type](/docs/api/interfaces/RecordVideoOptions#filetype), [enable flash](/docs/api/interfaces/RecordVideoOptions#flash) and more using the [`RecordVideoOptions`](/docs/api/interfaces/RecordVideoOptions) parameter. +You can customize capture options such as [video codec](/docs/api/interfaces/RecordVideoOptions#videocodec), [file type](/docs/api/interfaces/RecordVideoOptions#filetype), [enable flash](/docs/api/interfaces/RecordVideoOptions#flash) and more using the [`RecordVideoOptions`](/docs/api/interfaces/RecordVideoOptions) parameter. For any error that occured _while recording the video_, the `onRecordingError` callback will be invoked with a [`CaptureError`](/docs/api/classes/CameraCaptureError) and the recording is therefore cancelled. @@ -119,22 +119,16 @@ If the device does not support `h265`, VisionCamera will automatically fall-back Videos are recorded with a target bit-rate, which the encoder aims to match as closely as possible. A lower bit-rate means less quality (and less file size), a higher bit-rate means higher quality (and larger file size) since it can assign more bits to moving pixels. -To simply record videos with higher quality, use a [`videoBitRate`](/docs/api/interfaces/RecordVideoOptions#videobitrate) of `'high'`, which effectively increases the bit-rate by 20%: +To simply record videos with higher quality, use a [`videoBitRate`](/docs/api/interfaces/CameraProps#videobitrate) of `'high'`, which effectively increases the bit-rate by 20%: -```ts -camera.current.startRecording({ - ...props, - videoBitRate: 'high' -}) +```jsx + ``` -To use a lower bit-rate for lower quality and lower file-size, use a [`videoBitRate`](/docs/api/interfaces/RecordVideoOptions#videobitrate) of `'low'`, which effectively decreases the bit-rate by 20%: +To use a lower bit-rate for lower quality and lower file-size, use a [`videoBitRate`](/docs/api/interfaces/CameraProps#videobitrate) of `'low'`, which effectively decreases the bit-rate by 20%: -```ts -camera.current.startRecording({ - ...props, - videoBitRate: 'low' -}) +```jsx + ``` #### Custom Bit Rate @@ -162,13 +156,10 @@ if (codec === 'h265') bitRate *= 0.8 // H.265 bitRate *= yourCustomFactor // e.g. 0.5x for half the bit-rate ``` -And then pass it to the [`startRecording(...)`](/docs/api/classes/Camera#startrecording) function (in Mbps): +And then pass it to the `` component (in Mbps): -```ts -camera.current.startRecording({ - ...props, - videoBitRate: bitRate // Mbps -}) +```jsx + ``` ### Video Frame Rate (FPS) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4364e792e8..332caf256e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -7,9 +7,9 @@ PODS: - hermes-engine (0.75.4): - hermes-engine/Pre-built (= 0.75.4) - hermes-engine/Pre-built (0.75.4) - - MMKV (1.3.9): - - MMKVCore (~> 1.3.9) - - MMKVCore (1.3.9) + - MMKV (2.0.0): + - MMKVCore (~> 2.0.0) + - MMKVCore (2.0.0) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -2027,8 +2027,8 @@ SPEC CHECKSUMS: fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: ea92f60f37dba025e293cbe4b4a548fd26b610a0 - MMKV: 817ba1eea17421547e01e087285606eb270a8dcb - MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 + MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 + MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1 RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b @@ -2098,8 +2098,8 @@ SPEC CHECKSUMS: RNVectorIcons: 6382277afab3c54658e9d555ee0faa7a37827136 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d VisionCamera: 3b7cfecebbcb3871c4dd3fb47791b96a47fe5371 - Yoga: aa3df615739504eebb91925fc9c58b4922ea9a08 + Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6 -PODFILE CHECKSUM: a43dbce8eba88fb736654cbed5c32f2a764615ef +PODFILE CHECKSUM: 2ad84241179871ca890f7c65c855d117862f1a68 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.1 diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index 745ed9d214..f91ce830bb 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -48,7 +48,7 @@ data class CameraConfiguration( // Output types, those need to be comparable data class CodeScanner(val codeTypes: List) data class Photo(val isMirrored: Boolean, val enableHdr: Boolean, val photoQualityBalance: QualityBalance) - data class Video(val isMirrored: Boolean, val enableHdr: Boolean) + data class Video(val isMirrored: Boolean, val enableHdr: Boolean, val bitRateOverride: Double?, val bitRateMultiplier: Double?) data class FrameProcessor(val isMirrored: Boolean, val pixelFormat: PixelFormat) data class Audio(val nothing: Unit) data class Preview(val surfaceProvider: SurfaceProvider) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Configuration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Configuration.kt index 381e9481b5..7c5cf05ae2 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Configuration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Configuration.kt @@ -22,6 +22,7 @@ import com.mrousavy.camera.core.extensions.* import com.mrousavy.camera.core.types.CameraDeviceFormat import com.mrousavy.camera.core.types.Torch import com.mrousavy.camera.core.types.VideoStabilizationMode +import com.mrousavy.camera.core.utils.CamcorderProfileUtils import kotlin.math.roundToInt private fun assertFormatRequirement( @@ -44,7 +45,8 @@ private fun assertFormatRequirement( @SuppressLint("RestrictedApi") @Suppress("LiftReturnOrAssignment") internal fun CameraSession.configureOutputs(configuration: CameraConfiguration) { - Log.i(CameraSession.TAG, "Creating new Outputs for Camera #${configuration.cameraId}...") + val cameraId = configuration.cameraId!! + Log.i(CameraSession.TAG, "Creating new Outputs for Camera #$cameraId...") val fpsRange = configuration.targetFpsRange val format = configuration.format @@ -120,11 +122,24 @@ internal fun CameraSession.configureOutputs(configuration: CameraConfiguration) // We are currently not recording, so we can re-create a recorder instance if needed. Log.i(CameraSession.TAG, "Creating new Recorder...") Recorder.Builder().also { recorder -> - configuration.format?.let { format -> + format?.let { format -> recorder.setQualitySelector(format.videoQualitySelector) } - // TODO: Make videoBitRate a Camera Prop - // video.setTargetVideoEncodingBitRate() + videoConfig.config.bitRateOverride?.let { bitRateOverride -> + val bps = bitRateOverride * 1_000_000 + recorder.setTargetVideoEncodingBitRate(bps.toInt()) + } + videoConfig.config.bitRateMultiplier?.let { bitRateMultiplier -> + if (format == null) { + // We need to get the videoSize to estimate the bitRate modifier + throw PropRequiresFormatToBeNonNullError("videoBitRate") + } + val recommendedBitRate = CamcorderProfileUtils.getRecommendedBitRate(cameraId, format.videoSize) + if (recommendedBitRate != null) { + val targetBitRate = recommendedBitRate.toDouble() * bitRateMultiplier + recorder.setTargetVideoEncodingBitRate(targetBitRate.toInt()) + } + } }.build() } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/types/RecordVideoOptions.kt b/package/android/src/main/java/com/mrousavy/camera/core/types/RecordVideoOptions.kt index 5cf4db27a6..1fca8700be 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/types/RecordVideoOptions.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/types/RecordVideoOptions.kt @@ -5,23 +5,16 @@ import com.facebook.react.bridge.ReadableMap import com.mrousavy.camera.core.utils.FileUtils import com.mrousavy.camera.core.utils.OutputFile -class RecordVideoOptions( - val file: OutputFile, - val videoCodec: VideoCodec, - val videoBitRateOverride: Double?, - val videoBitRateMultiplier: Double? -) { +class RecordVideoOptions(val file: OutputFile, val videoCodec: VideoCodec) { companion object { fun fromJSValue(context: Context, map: ReadableMap): RecordVideoOptions { val directory = if (map.hasKey("path")) FileUtils.getDirectory(map.getString("path")) else context.cacheDir val fileType = if (map.hasKey("fileType")) VideoFileType.fromUnionValue(map.getString("fileType")) else VideoFileType.MOV val videoCodec = if (map.hasKey("videoCodec")) VideoCodec.fromUnionValue(map.getString("videoCodec")) else VideoCodec.H264 - val videoBitRateOverride = if (map.hasKey("videoBitRateOverride")) map.getDouble("videoBitRateOverride") else null - val videoBitRateMultiplier = if (map.hasKey("videoBitRateMultiplier")) map.getDouble("videoBitRateMultiplier") else null val outputFile = OutputFile(context, directory, fileType.toExtension()) - return RecordVideoOptions(outputFile, videoCodec, videoBitRateOverride, videoBitRateMultiplier) + return RecordVideoOptions(outputFile, videoCodec) } } } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/utils/CamcorderProfileUtils.kt b/package/android/src/main/java/com/mrousavy/camera/core/utils/CamcorderProfileUtils.kt index a5946e9a6c..791c412426 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/utils/CamcorderProfileUtils.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/utils/CamcorderProfileUtils.kt @@ -1,12 +1,16 @@ package com.mrousavy.camera.core.utils +import android.annotation.SuppressLint import android.media.CamcorderProfile import android.os.Build +import android.util.Log import android.util.Size import kotlin.math.abs class CamcorderProfileUtils { companion object { + private const val TAG = "CamcorderProfileUtils" + private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int = when (camcorderProfile) { CamcorderProfile.QUALITY_QCIF -> 176 * 144 @@ -29,6 +33,7 @@ class CamcorderProfileUtils { val targetResolution = resolution.width * resolution.height val cameraIdInt = cameraId.toIntOrNull() + @SuppressLint("InlinedApi") var profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile -> if (cameraIdInt != null) { return@filter CamcorderProfile.hasProfile(cameraIdInt, profile) @@ -70,6 +75,7 @@ class CamcorderProfileUtils { return null } catch (e: Throwable) { // some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why. + Log.e(TAG, "Failed to get maximum video size for Camera ID $cameraId! ${e.message}", e) return null } } @@ -94,6 +100,32 @@ class CamcorderProfileUtils { return null } catch (e: Throwable) { // some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why. + Log.e(TAG, "Failed to get maximum FPS for Camera ID $cameraId! ${e.message}", e) + return null + } + } + + fun getRecommendedBitRate(cameraId: String, videoSize: Size): Int? { + try { + val quality = findClosestCamcorderProfileQuality(cameraId, videoSize, true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val profiles = CamcorderProfile.getAll(cameraId, quality) + if (profiles != null) { + return profiles.videoProfiles.maxOf { profile -> profile.bitrate } + } + } + + val cameraIdInt = cameraId.toIntOrNull() + if (cameraIdInt != null) { + val profile = CamcorderProfile.get(cameraIdInt, quality) + return profile.videoBitRate + } + + return null + } catch (e: Throwable) { + // some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why. + Log.e(TAG, "Failed to get recommended video bit-rate for Camera ID $cameraId! ${e.message}", e) return null } } diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt index f0eb5b90de..64a751eaab 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.launch // TODO: takePhoto() depth data // TODO: takePhoto() raw capture // TODO: takePhoto() return with jsi::Value Image reference for faster capture -// TODO: Support videoCodec and videoBitRate on Android +// TODO: Support videoCodec on Android @SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission") class CameraView(context: Context) : @@ -75,6 +75,8 @@ class CameraView(context: Context) : var videoStabilizationMode: VideoStabilizationMode? = null var videoHdr = false var photoHdr = false + var videoBitRateOverride: Double? = null + var videoBitRateMultiplier: Double? = null // TODO: Use .BALANCED once CameraX fixes it https://issuetracker.google.com/issues/337214687 var photoQualityBalance = QualityBalance.SPEED @@ -180,7 +182,10 @@ class CameraView(context: Context) : // Video if (video || enableFrameProcessor) { - config.video = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Video(isMirrored, videoHdr)) + config.video = + CameraConfiguration.Output.Enabled.create( + CameraConfiguration.Video(isMirrored, videoHdr, videoBitRateOverride, videoBitRateMultiplier) + ) } else { config.video = CameraConfiguration.Output.Disabled.create() } diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt index d41300e57d..c5845b727f 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt @@ -191,6 +191,24 @@ class CameraViewManager : ViewGroupManager() { view.videoHdr = videoHdr } + @ReactProp(name = "videoBitRateOverride", defaultDouble = -1.0) + fun setVideoBitRateOverride(view: CameraView, videoBitRateOverride: Double) { + if (videoBitRateOverride != -1.0) { + view.videoBitRateOverride = videoBitRateOverride + } else { + view.videoBitRateOverride = null + } + } + + @ReactProp(name = "videoBitRateMultiplier", defaultDouble = -1.0) + fun setVideoBitRateMultiplier(view: CameraView, videoBitRateMultiplier: Double) { + if (videoBitRateMultiplier != -1.0) { + view.videoBitRateMultiplier = videoBitRateMultiplier + } else { + view.videoBitRateMultiplier = null + } + } + @ReactProp(name = "lowLightBoost") fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean) { view.lowLightBoost = lowLightBoost diff --git a/package/ios/Core/Types/RecordVideoOptions.swift b/package/ios/Core/Types/RecordVideoOptions.swift index 65248c45d9..d12e05eb79 100644 --- a/package/ios/Core/Types/RecordVideoOptions.swift +++ b/package/ios/Core/Types/RecordVideoOptions.swift @@ -24,7 +24,7 @@ struct RecordVideoOptions { */ var bitRateMultiplier: Double? - init(fromJSValue dictionary: NSDictionary) throws { + init(fromJSValue dictionary: NSDictionary, bitRateOverride: Double? = nil, bitRateMultiplier: Double? = nil) throws { // File Type (.mov or .mp4) if let fileTypeOption = dictionary["fileType"] as? String { fileType = try AVFileType(withString: fileTypeOption) @@ -38,13 +38,9 @@ struct RecordVideoOptions { codec = try AVVideoCodecType(withString: codecOption) } // BitRate Override - if let parsed = dictionary["videoBitRateOverride"] as? Double { - bitRateOverride = parsed - } + self.bitRateOverride = bitRateOverride // BitRate Multiplier - if let parsed = dictionary["videoBitRateMultiplier"] as? Double { - bitRateMultiplier = parsed - } + self.bitRateMultiplier = bitRateMultiplier // Custom Path let fileExtension = fileType.descriptor ?? "mov" if let customPath = dictionary["path"] as? String { diff --git a/package/ios/React/CameraView+RecordVideo.swift b/package/ios/React/CameraView+RecordVideo.swift index dcb07fd532..913d420efd 100644 --- a/package/ios/React/CameraView+RecordVideo.swift +++ b/package/ios/React/CameraView+RecordVideo.swift @@ -16,7 +16,9 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud let callback = Callback(jsCallback) do { - let options = try RecordVideoOptions(fromJSValue: options) + let options = try RecordVideoOptions(fromJSValue: options, + bitRateOverride: videoBitRateOverride?.doubleValue, + bitRateMultiplier: videoBitRateMultiplier?.doubleValue) // Start Recording with success and error callbacks cameraSession.startRecording( diff --git a/package/ios/React/CameraView.swift b/package/ios/React/CameraView.swift index 6b855381ba..c773975353 100644 --- a/package/ios/React/CameraView.swift +++ b/package/ios/React/CameraView.swift @@ -53,6 +53,8 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat @objc var photoQualityBalance: NSString? @objc var lowLightBoost = false @objc var outputOrientation: NSString? + @objc var videoBitRateOverride: NSNumber? + @objc var videoBitRateMultiplier: NSNumber? // other props @objc var isActive = false diff --git a/package/ios/React/CameraViewManager.m b/package/ios/React/CameraViewManager.m index dc3ad1accd..527c9bc0fd 100644 --- a/package/ios/React/CameraViewManager.m +++ b/package/ios/React/CameraViewManager.m @@ -47,6 +47,8 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage RCT_EXPORT_VIEW_PROPERTY(lowLightBoost, BOOL); RCT_EXPORT_VIEW_PROPERTY(videoStabilizationMode, NSString); RCT_EXPORT_VIEW_PROPERTY(pixelFormat, NSString); +RCT_EXPORT_VIEW_PROPERTY(videoBitRateOverride, NSNumber); +RCT_EXPORT_VIEW_PROPERTY(videoBitRateMultiplier, NSNumber); // other props RCT_EXPORT_VIEW_PROPERTY(torch, NSString); RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber); @@ -71,37 +73,20 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage RCT_REMAP_VIEW_PROPERTY(onCodeScanned, onCodeScannedEvent, RCTDirectEventBlock); // Camera View Functions -RCT_EXTERN_METHOD(startRecording - : (nonnull NSNumber*)node options - : (NSDictionary*)options onRecordCallback - : (RCTResponseSenderBlock)onRecordCallback); -RCT_EXTERN_METHOD(pauseRecording - : (nonnull NSNumber*)node resolve - : (RCTPromiseResolveBlock)resolve reject - : (RCTPromiseRejectBlock)reject); -RCT_EXTERN_METHOD(cancelRecording - : (nonnull NSNumber*)node resolve - : (RCTPromiseResolveBlock)resolve reject - : (RCTPromiseRejectBlock)reject); -RCT_EXTERN_METHOD(resumeRecording - : (nonnull NSNumber*)node resolve - : (RCTPromiseResolveBlock)resolve reject - : (RCTPromiseRejectBlock)reject); +RCT_EXTERN_METHOD(startRecording : (nonnull NSNumber*)node options : (NSDictionary*)options onRecordCallback : (RCTResponseSenderBlock) + onRecordCallback); +RCT_EXTERN_METHOD(pauseRecording : (nonnull NSNumber*)node resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock) + reject); +RCT_EXTERN_METHOD(cancelRecording : (nonnull NSNumber*)node resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock) + reject); +RCT_EXTERN_METHOD(resumeRecording : (nonnull NSNumber*)node resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock) + reject); RCT_EXTERN_METHOD(stopRecording : (nonnull NSNumber*)node resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject); -RCT_EXTERN_METHOD(takePhoto - : (nonnull NSNumber*)node options - : (NSDictionary*)options resolve - : (RCTPromiseResolveBlock)resolve reject - : (RCTPromiseRejectBlock)reject); -RCT_EXTERN_METHOD(takeSnapshot - : (nonnull NSNumber*)node options - : (NSDictionary*)options resolve - : (RCTPromiseResolveBlock)resolve reject - : (RCTPromiseRejectBlock)reject); -RCT_EXTERN_METHOD(focus - : (nonnull NSNumber*)node point - : (NSDictionary*)point resolve - : (RCTPromiseResolveBlock)resolve reject - : (RCTPromiseRejectBlock)reject); +RCT_EXTERN_METHOD(takePhoto : (nonnull NSNumber*)node options : (NSDictionary*)options resolve : (RCTPromiseResolveBlock) + resolve reject : (RCTPromiseRejectBlock)reject); +RCT_EXTERN_METHOD(takeSnapshot : (nonnull NSNumber*)node options : (NSDictionary*)options resolve : (RCTPromiseResolveBlock) + resolve reject : (RCTPromiseRejectBlock)reject); +RCT_EXTERN_METHOD(focus : (nonnull NSNumber*)node point : (NSDictionary*)point resolve : (RCTPromiseResolveBlock) + resolve reject : (RCTPromiseRejectBlock)reject); @end diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index cd2041767f..afe056a761 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -30,10 +30,7 @@ import { RotationHelper } from './RotationHelper' export type CameraPermissionStatus = 'granted' | 'not-determined' | 'denied' | 'restricted' export type CameraPermissionRequestResult = 'granted' | 'denied' -type NativeRecordVideoOptions = Omit & { - videoBitRateOverride?: number - videoBitRateMultiplier?: number -} +type NativeRecordVideoOptions = Omit type RefType = React.Component & Readonly interface CameraState { isRecordingWithFlash: boolean @@ -170,7 +167,7 @@ export class Camera extends React.PureComponent { } } - private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number { + private getBitRateMultiplier(bitRate: CameraProps['videoBitRate']): number { if (typeof bitRate === 'number' || bitRate == null) return 1 switch (bitRate) { case 'extra-low': @@ -204,7 +201,7 @@ export class Camera extends React.PureComponent { * ``` */ public startRecording(options: RecordVideoOptions): void { - const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options + const { onRecordingError, onRecordingFinished, ...passThruOptions } = options if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!') @@ -215,15 +212,6 @@ export class Camera extends React.PureComponent { }) } - const nativeOptions: NativeRecordVideoOptions = passThruOptions - if (typeof videoBitRate === 'number') { - // If the user passed an absolute number as a bit-rate, we just use this as a full override. - nativeOptions.videoBitRateOverride = videoBitRate - } else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') { - // If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it - nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate) - } - const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => { if (this.state.isRecordingWithFlash) { // disable torch again if it was enabled @@ -235,9 +223,11 @@ export class Camera extends React.PureComponent { if (error != null) return onRecordingError(error) if (video != null) return onRecordingFinished(video) } + + const nativeRecordVideoOptions: NativeRecordVideoOptions = passThruOptions try { // TODO: Use TurboModules to make this awaitable. - CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback) + CameraModule.startRecording(this.handle, nativeRecordVideoOptions, onRecordCallback) } catch (e) { throw tryParseNativeCameraError(e) } @@ -626,7 +616,7 @@ export class Camera extends React.PureComponent { /** @internal */ public render(): React.ReactNode { // We remove the big `device` object from the props because we only need to pass `cameraId` to native. - const { device, frameProcessor, codeScanner, enableFpsGraph, fps, ...props } = this.props + const { device, frameProcessor, codeScanner, enableFpsGraph, fps, videoBitRate, ...props } = this.props // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (device == null) { @@ -645,6 +635,17 @@ export class Camera extends React.PureComponent { const minFps = fps == null ? undefined : typeof fps === 'number' ? fps : fps[0] const maxFps = fps == null ? undefined : typeof fps === 'number' ? fps : fps[1] + // bitrate is number (override) or string (multiplier) + let bitRateMultiplier: number | undefined + let bitRateOverride: number | undefined + if (typeof videoBitRate === 'number') { + // If the user passed an absolute number as a bit-rate, we just use this as a full override. + bitRateOverride = videoBitRate + } else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') { + // If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it + bitRateMultiplier = this.getBitRateMultiplier(videoBitRate) + } + return ( { onPreviewStarted={this.onPreviewStarted} onPreviewStopped={this.onPreviewStopped} onShutter={this.onShutter} + videoBitRateMultiplier={bitRateMultiplier} + videoBitRateOverride={bitRateOverride} onOutputOrientationChanged={this.onOutputOrientationChanged} onPreviewOrientationChanged={this.onPreviewOrientationChanged} onError={this.onError} diff --git a/package/src/NativeCameraView.ts b/package/src/NativeCameraView.ts index 903fa1016f..b53009ce56 100644 --- a/package/src/NativeCameraView.ts +++ b/package/src/NativeCameraView.ts @@ -34,6 +34,7 @@ export type NativeCameraViewProps = Omit< | 'frameProcessor' | 'codeScanner' | 'fps' + | 'videoBitRate' > & { // private intermediate props cameraId: string @@ -41,6 +42,8 @@ export type NativeCameraViewProps = Omit< codeScannerOptions?: Omit minFps?: number maxFps?: number + videoBitRateOverride?: number + videoBitRateMultiplier?: number // private events onViewReady: (event: NativeSyntheticEvent) => void onAverageFpsChanged?: (event: NativeSyntheticEvent) => void diff --git a/package/src/types/CameraProps.ts b/package/src/types/CameraProps.ts index 8056d35c95..18bfc9b618 100644 --- a/package/src/types/CameraProps.ts +++ b/package/src/types/CameraProps.ts @@ -200,6 +200,23 @@ export interface CameraProps extends ViewProps { * Make sure the given {@linkcode format} supports HDR (see {@linkcode CameraDeviceFormat.supportsVideoHdr format.supportsVideoHdr}). */ videoHdr?: boolean + /** + * The bit-rate for encoding the video into a file, in Mbps (Megabits per second). + * + * Bit-rate is dependant on various factors such as resolution, FPS, pixel format (whether it's 10 bit HDR or not), and video codec. + * + * By default, it will be calculated by the hardware encoder, which takes all those factors into account. + * + * * `extra-low`: 40% lower than whatever the hardware encoder recommends. + * * `low`: 20% lower than whatever the hardware encoder recommends. + * * `normal`: The recommended value by the hardware encoder. + * * `high`: 20% higher than whatever the hardware encoder recommends. + * * `extra-high`: 40% higher than whatever the hardware encoder recommends. + * * `number`: Any custom number for the bit-rate, in Mbps. + * + * @default 'normal' + */ + videoBitRate?: 'extra-low' | 'low' | 'normal' | 'high' | 'extra-high' | number /** * Enables or disables HDR Photo Capture via a double capture routine that combines low- and high exposure photos. * diff --git a/package/src/types/VideoFile.ts b/package/src/types/VideoFile.ts index 197e4d1122..6391df76fa 100644 --- a/package/src/types/VideoFile.ts +++ b/package/src/types/VideoFile.ts @@ -33,23 +33,6 @@ export interface RecordVideoOptions { * - `h265`: The HEVC (High-Efficient-Video-Codec) for higher efficient video recordings. Results in up to 50% smaller file-sizes. */ videoCodec?: 'h264' | 'h265' - /** - * The bit-rate for encoding the video into a file, in Mbps (Megabits per second). - * - * Bit-rate is dependant on various factors such as resolution, FPS, pixel format (whether it's 10 bit HDR or not), and video codec. - * - * By default, it will be calculated by the hardware encoder, which takes all those factors into account. - * - * * `extra-low`: 40% lower than whatever the hardware encoder recommends. - * * `low`: 20% lower than whatever the hardware encoder recommends. - * * `normal`: The recommended value by the hardware encoder. - * * `high`: 20% higher than whatever the hardware encoder recommends. - * * `extra-high`: 40% higher than whatever the hardware encoder recommends. - * * `number`: Any custom number for the bit-rate, in Mbps. - * - * @default 'normal' - */ - videoBitRate?: 'extra-low' | 'low' | 'normal' | 'high' | 'extra-high' | number } /**