fix: Use bitRate multiplier instead of setting it to an absolute value (#2216)
				
					
				
			* fix: Use `bitRate` multiplier instead of setting it to an absolute value * Pass override * Format * Rename * feat: Also implement Android * fix: Log Mbps properly * fix: Up-/Down-scale bit-rate if different options * fix: Parse in Manager * Update RecordingSession+getRecommendedBitRate.kt
This commit is contained in:
		| @@ -9,13 +9,13 @@ import com.mrousavy.camera.core.MicrophonePermissionError | ||||
| import com.mrousavy.camera.core.RecorderError | ||||
| import com.mrousavy.camera.core.RecordingSession | ||||
| import com.mrousavy.camera.core.code | ||||
| import com.mrousavy.camera.types.Flash | ||||
| import com.mrousavy.camera.types.RecordVideoOptions | ||||
| import com.mrousavy.camera.types.Torch | ||||
| import com.mrousavy.camera.types.VideoCodec | ||||
| import com.mrousavy.camera.types.VideoFileType | ||||
| import com.mrousavy.camera.utils.makeErrorMap | ||||
| import java.util.* | ||||
|  | ||||
| suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Callback) { | ||||
| suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) { | ||||
|   // check audio permission | ||||
|   if (audio == true) { | ||||
|     if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { | ||||
| @@ -23,25 +23,13 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   val enableFlash = options.getString("flash") == "on" | ||||
|   val enableFlash = options.flash == Flash.ON | ||||
|   if (enableFlash) { | ||||
|     // overrides current torch mode value to enable flash while recording | ||||
|     cameraSession.configure { config -> | ||||
|       config.torch = Torch.ON | ||||
|     } | ||||
|   } | ||||
|   var codec = VideoCodec.H264 | ||||
|   if (options.hasKey("videoCodec")) { | ||||
|     codec = VideoCodec.fromUnionValue(options.getString("videoCodec")) | ||||
|   } | ||||
|   var fileType = VideoFileType.MP4 | ||||
|   if (options.hasKey("fileType")) { | ||||
|     fileType = VideoFileType.fromUnionValue(options.getString("fileType")) | ||||
|   } | ||||
|   var bitRate: Double? = null | ||||
|   if (options.hasKey("videoBitRate")) { | ||||
|     bitRate = options.getDouble("videoBitRate") | ||||
|   } | ||||
|  | ||||
|   val callback = { video: RecordingSession.Video -> | ||||
|     val map = Arguments.createMap() | ||||
| @@ -53,7 +41,7 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca | ||||
|     val errorMap = makeErrorMap(error.code, error.message) | ||||
|     onRecordCallback(null, errorMap) | ||||
|   } | ||||
|   cameraSession.startRecording(audio == true, codec, fileType, bitRate, callback, onError) | ||||
|   cameraSession.startRecording(audio == true, options, callback, onError) | ||||
| } | ||||
|  | ||||
| @SuppressLint("RestrictedApi") | ||||
|   | ||||
| @@ -83,10 +83,11 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase | ||||
|  | ||||
|   // TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that | ||||
|   @ReactMethod | ||||
|   fun startRecording(viewTag: Int, options: ReadableMap, onRecordCallback: Callback) { | ||||
|   fun startRecording(viewTag: Int, jsOptions: ReadableMap, onRecordCallback: Callback) { | ||||
|     coroutineScope.launch { | ||||
|       val view = findCameraView(viewTag) | ||||
|       try { | ||||
|         val options = RecordVideoOptions(jsOptions) | ||||
|         view.startRecording(options, onRecordCallback) | ||||
|       } catch (error: CameraError) { | ||||
|         val map = makeErrorMap("${error.domain}/${error.id}", error.message, error) | ||||
|   | ||||
| @@ -41,9 +41,8 @@ import com.mrousavy.camera.frameprocessor.FrameProcessor | ||||
| import com.mrousavy.camera.types.Flash | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import com.mrousavy.camera.types.QualityPrioritization | ||||
| import com.mrousavy.camera.types.RecordVideoOptions | ||||
| import com.mrousavy.camera.types.Torch | ||||
| import com.mrousavy.camera.types.VideoCodec | ||||
| import com.mrousavy.camera.types.VideoFileType | ||||
| import com.mrousavy.camera.types.VideoStabilizationMode | ||||
| import java.io.Closeable | ||||
| import java.util.concurrent.CancellationException | ||||
| @@ -516,20 +515,21 @@ class CameraSession(private val context: Context, private val cameraManager: Cam | ||||
|  | ||||
|   suspend fun startRecording( | ||||
|     enableAudio: Boolean, | ||||
|     codec: VideoCodec, | ||||
|     fileType: VideoFileType, | ||||
|     bitRate: Double?, | ||||
|     options: RecordVideoOptions, | ||||
|     callback: (video: RecordingSession.Video) -> Unit, | ||||
|     onError: (error: RecorderError) -> Unit | ||||
|   ) { | ||||
|     mutex.withLock { | ||||
|       if (recording != null) throw RecordingInProgressError() | ||||
|       val videoOutput = videoOutput ?: throw VideoNotEnabledError() | ||||
|       val cameraDevice = cameraDevice ?: throw CameraNotReadyError() | ||||
|  | ||||
|       // TODO: Implement HDR | ||||
|       val hdr = configuration?.videoHdr ?: false | ||||
|       val fps = configuration?.fps ?: 30 | ||||
|  | ||||
|       val recording = | ||||
|         RecordingSession(context, videoOutput.size, enableAudio, fps, codec, orientation, fileType, bitRate, callback, onError) | ||||
|         RecordingSession(context, cameraDevice.id, videoOutput.size, enableAudio, fps, hdr, orientation, options, callback, onError) | ||||
|       recording.start() | ||||
|       this.recording = recording | ||||
|     } | ||||
|   | ||||
| @@ -7,20 +7,20 @@ import android.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Size | ||||
| import android.view.Surface | ||||
| import com.mrousavy.camera.extensions.getRecommendedBitRate | ||||
| import com.mrousavy.camera.types.Orientation | ||||
| import com.mrousavy.camera.types.VideoCodec | ||||
| import com.mrousavy.camera.types.VideoFileType | ||||
| import com.mrousavy.camera.types.RecordVideoOptions | ||||
| import java.io.File | ||||
|  | ||||
| class RecordingSession( | ||||
|   context: Context, | ||||
|   val cameraId: String, | ||||
|   val size: Size, | ||||
|   private val enableAudio: Boolean, | ||||
|   private val fps: Int? = null, | ||||
|   private val codec: VideoCodec = VideoCodec.H264, | ||||
|   private val hdr: Boolean = false, | ||||
|   private val orientation: Orientation, | ||||
|   private val fileType: VideoFileType = VideoFileType.MP4, | ||||
|   videoBitRate: Double? = null, | ||||
|   private val options: RecordVideoOptions, | ||||
|   private val callback: (video: Video) -> Unit, | ||||
|   private val onError: (error: RecorderError) -> Unit | ||||
| ) { | ||||
| @@ -34,14 +34,14 @@ class RecordingSession( | ||||
|  | ||||
|   data class Video(val path: String, val durationMs: Long) | ||||
|  | ||||
|   private val bitRate = videoBitRate ?: getDefaultBitRate() | ||||
|   private val bitRate = getBitRate() | ||||
|   private val recorder: MediaRecorder | ||||
|   private val outputFile: File | ||||
|   private var startTime: Long? = null | ||||
|   val surface: Surface = MediaCodec.createPersistentInputSurface() | ||||
|  | ||||
|   init { | ||||
|     outputFile = File.createTempFile("mrousavy", fileType.toExtension(), context.cacheDir) | ||||
|     outputFile = File.createTempFile("mrousavy", options.fileType.toExtension(), context.cacheDir) | ||||
|  | ||||
|     Log.i(TAG, "Creating RecordingSession for ${outputFile.absolutePath}") | ||||
|  | ||||
| @@ -52,12 +52,12 @@ class RecordingSession( | ||||
|  | ||||
|     recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) | ||||
|     recorder.setOutputFile(outputFile.absolutePath) | ||||
|     recorder.setVideoEncodingBitRate((bitRate * 1_000_000).toInt()) | ||||
|     recorder.setVideoEncodingBitRate(bitRate) | ||||
|     recorder.setVideoSize(size.height, size.width) | ||||
|     if (fps != null) recorder.setVideoFrameRate(fps) | ||||
|  | ||||
|     Log.i(TAG, "Using $codec Video Codec at $bitRate Mbps..") | ||||
|     recorder.setVideoEncoder(codec.toVideoCodec()) | ||||
|     Log.i(TAG, "Using ${options.videoCodec} Video Codec at ${bitRate / 1_000_000.0} Mbps..") | ||||
|     recorder.setVideoEncoder(options.videoCodec.toVideoEncoder()) | ||||
|     if (enableAudio) { | ||||
|       Log.i(TAG, "Adding Audio Channel..") | ||||
|       recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) | ||||
| @@ -124,22 +124,26 @@ class RecordingSession( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private fun getDefaultBitRate(): Double { | ||||
|     var baseBitRate = when (size.width * size.height) { | ||||
|       in 0..640 * 480 -> 2.0 | ||||
|       in 640 * 480..1280 * 720 -> 5.0 | ||||
|       in 1280 * 720..1920 * 1080 -> 10.0 | ||||
|       in 1920 * 1080..3840 * 2160 -> 30.0 | ||||
|       in 3840 * 2160..7680 * 4320 -> 100.0 | ||||
|       else -> 100.0 | ||||
|   /** | ||||
|    * Get the bit-rate to use, in bits per seconds. | ||||
|    * This can either be overridden, multiplied, or just left at the recommended value. | ||||
|    */ | ||||
|   private fun getBitRate(): Int { | ||||
|     var bitRate = getRecommendedBitRate(fps ?: 30, options.videoCodec, hdr) | ||||
|     options.videoBitRateOverride?.let { override -> | ||||
|       // Mbps -> bps | ||||
|       bitRate = (override * 1_000_000).toInt() | ||||
|     } | ||||
|     baseBitRate = baseBitRate / 30.0 * (fps ?: 30).toDouble() | ||||
|     if (this.codec == VideoCodec.H265) baseBitRate *= 0.8 | ||||
|     return baseBitRate | ||||
|     options.videoBitRateMultiplier?.let { multiplier -> | ||||
|       // multiply by 1.2, 0.8, ... | ||||
|       bitRate = (bitRate * multiplier).toInt() | ||||
|     } | ||||
|     return bitRate | ||||
|   } | ||||
|  | ||||
|   override fun toString(): String { | ||||
|     val audio = if (enableAudio) "with audio" else "without audio" | ||||
|     return "${size.width} x ${size.height} @ $fps FPS $codec $fileType $orientation $bitRate Mbps RecordingSession ($audio)" | ||||
|     return "${size.width} x ${size.height} @ $fps FPS ${options.videoCodec} ${options.fileType} " + | ||||
|       "$orientation ${bitRate / 1_000_000.0} Mbps RecordingSession ($audio)" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,100 @@ | ||||
| package com.mrousavy.camera.extensions | ||||
|  | ||||
| import android.media.CamcorderProfile | ||||
| import android.media.MediaRecorder.VideoEncoder | ||||
| import android.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Size | ||||
| import com.mrousavy.camera.core.RecordingSession | ||||
| import com.mrousavy.camera.types.VideoCodec | ||||
| import kotlin.math.abs | ||||
|  | ||||
| data class RecommendedProfile( | ||||
|   val bitRate: Int, | ||||
|   // VideoEncoder.H264 or VideoEncoder.HEVC | ||||
|   val codec: Int, | ||||
|   // 8-bit or 10-bit | ||||
|   val bitDepth: Int, | ||||
|   // 30 or 60 FPS | ||||
|   val fps: Int | ||||
| ) | ||||
|  | ||||
| fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boolean): Int { | ||||
|   val targetResolution = size | ||||
|   val encoder = codec.toVideoEncoder() | ||||
|   val bitDepth = if (hdr) 10 else 8 | ||||
|   val quality = findClosestCamcorderProfileQuality(targetResolution) | ||||
|   Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality") | ||||
|  | ||||
|   var recommendedProfile: RecommendedProfile? = null | ||||
|  | ||||
|   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||
|     val profiles = CamcorderProfile.getAll(cameraId, quality) | ||||
|     if (profiles != null) { | ||||
|       val best = profiles.videoProfiles.minBy { abs(it.width * it.height - targetResolution.width * targetResolution.height) } | ||||
|  | ||||
|       recommendedProfile = RecommendedProfile( | ||||
|         best.bitrate, | ||||
|         best.codec, | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) best.bitDepth else 8, | ||||
|         best.frameRate | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (recommendedProfile == null) { | ||||
|     val cameraIdInt = cameraId.toIntOrNull() | ||||
|     val profile = if (cameraIdInt != null) { | ||||
|       CamcorderProfile.get(cameraIdInt, quality) | ||||
|     } else { | ||||
|       CamcorderProfile.get(quality) | ||||
|     } | ||||
|     recommendedProfile = RecommendedProfile( | ||||
|       profile.videoBitRate, | ||||
|       profile.videoCodec, | ||||
|       8, | ||||
|       profile.videoFrameRate | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   var bitRate = recommendedProfile.bitRate.toDouble() | ||||
|   // the target bit-rate is for e.g. 30 FPS, but we use 60 FPS. up-scale it | ||||
|   bitRate = bitRate / recommendedProfile.fps * fps | ||||
|   // the target bit-rate might be in 8-bit SDR, but we record in 10-bit HDR. up-scale it | ||||
|   bitRate = bitRate / recommendedProfile.bitDepth * bitDepth | ||||
|   if (recommendedProfile.codec == VideoEncoder.H264 && encoder == VideoEncoder.HEVC) { | ||||
|     // the target bit-rate is for H.264, but we use H.265, which is 20% smaller | ||||
|     bitRate *= 0.8 | ||||
|   } else if (recommendedProfile.codec == VideoEncoder.HEVC && encoder == VideoEncoder.H264) { | ||||
|     // the target bit-rate is for H.265, but we use H.264, which is 20% larger | ||||
|     bitRate *= 1.2 | ||||
|   } | ||||
|   return bitRate.toInt() | ||||
| } | ||||
|  | ||||
| private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int = | ||||
|   when (camcorderProfile) { | ||||
|     CamcorderProfile.QUALITY_QCIF -> 176 * 144 | ||||
|     CamcorderProfile.QUALITY_QVGA -> 320 * 240 | ||||
|     CamcorderProfile.QUALITY_CIF -> 352 * 288 | ||||
|     CamcorderProfile.QUALITY_VGA -> 640 * 480 | ||||
|     CamcorderProfile.QUALITY_480P -> 720 * 480 | ||||
|     CamcorderProfile.QUALITY_720P -> 1280 * 720 | ||||
|     CamcorderProfile.QUALITY_1080P -> 1920 * 1080 | ||||
|     CamcorderProfile.QUALITY_2K -> 2048 * 1080 | ||||
|     CamcorderProfile.QUALITY_QHD -> 2560 * 1440 | ||||
|     CamcorderProfile.QUALITY_2160P -> 3840 * 2160 | ||||
|     CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160 | ||||
|     CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320 | ||||
|     else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!") | ||||
|   } | ||||
|  | ||||
| private fun findClosestCamcorderProfileQuality(resolution: Size): Int { | ||||
|   // Iterate through all available CamcorderProfiles and find the one that matches the closest | ||||
|   val targetResolution = resolution.width * resolution.height | ||||
|   val closestProfile = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).minBy { profile -> | ||||
|     val currentResolution = getResolutionForCamcorderProfileQuality(profile) | ||||
|     return@minBy abs(currentResolution - targetResolution) | ||||
|   } | ||||
|   return closestProfile | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| package com.mrousavy.camera.types | ||||
|  | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
|  | ||||
| class RecordVideoOptions(map: ReadableMap) { | ||||
|   var fileType: VideoFileType = VideoFileType.MOV | ||||
|   var flash: Flash = Flash.OFF | ||||
|   var videoCodec = VideoCodec.H264 | ||||
|   var videoBitRateOverride: Double? = null | ||||
|   var videoBitRateMultiplier: Double? = null | ||||
|  | ||||
|   init { | ||||
|     if (map.hasKey("fileType")) { | ||||
|       fileType = VideoFileType.fromUnionValue(map.getString("fileType")) | ||||
|     } | ||||
|     if (map.hasKey("flash")) { | ||||
|       flash = Flash.fromUnionValue(map.getString("flash")) | ||||
|     } | ||||
|     if (map.hasKey("videoCodec")) { | ||||
|       videoCodec = VideoCodec.fromUnionValue(map.getString("fileType")) | ||||
|     } | ||||
|     if (map.hasKey("videoBitRateOverride")) { | ||||
|       videoBitRateOverride = map.getDouble("videoBitRateOverride") | ||||
|     } | ||||
|     if (map.hasKey("videoBitRateMultiplier")) { | ||||
|       videoBitRateMultiplier = map.getDouble("videoBitRateMultiplier") | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -6,7 +6,7 @@ enum class VideoCodec(override val unionValue: String) : JSUnionValue { | ||||
|   H264("h264"), | ||||
|   H265("h265"); | ||||
|  | ||||
|   fun toVideoCodec(): Int = | ||||
|   fun toVideoEncoder(): Int = | ||||
|     when (this) { | ||||
|       H264 -> MediaRecorder.VideoEncoder.H264 | ||||
|       H265 -> MediaRecorder.VideoEncoder.HEVC | ||||
|   | ||||
| @@ -24,12 +24,31 @@ extension AVCaptureVideoDataOutput { | ||||
|       throw CameraError.capture(.createRecorderError(message: "Failed to get video settings!")) | ||||
|     } | ||||
|  | ||||
|     if let bitRate = options.bitRate { | ||||
|     if let bitRateOverride = options.bitRateOverride { | ||||
|       // Convert from Mbps -> bps | ||||
|       let bitsPerSecond = bitRate * 1_000_000 | ||||
|       settings[AVVideoCompressionPropertiesKey] = [ | ||||
|         AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond), | ||||
|       ] | ||||
|       let bitsPerSecond = bitRateOverride * 1_000_000 | ||||
|       if settings[AVVideoCompressionPropertiesKey] == nil { | ||||
|         settings[AVVideoCompressionPropertiesKey] = [:] | ||||
|       } | ||||
|       var compressionSettings = settings[AVVideoCompressionPropertiesKey] as? [String: Any] ?? [:] | ||||
|       let currentBitRate = compressionSettings[AVVideoAverageBitRateKey] as? NSNumber | ||||
|       ReactLogger.log(level: .info, message: "Setting Video Bit-Rate from \(currentBitRate?.doubleValue.description ?? "nil") bps to \(bitsPerSecond) bps...") | ||||
|  | ||||
|       compressionSettings[AVVideoAverageBitRateKey] = NSNumber(value: bitsPerSecond) | ||||
|       settings[AVVideoCompressionPropertiesKey] = compressionSettings | ||||
|     } | ||||
|  | ||||
|     if let bitRateMultiplier = options.bitRateMultiplier { | ||||
|       // Check if the bit-rate even exists in the settings | ||||
|       if var compressionSettings = settings[AVVideoCompressionPropertiesKey] as? [String: Any], | ||||
|          let currentBitRate = compressionSettings[AVVideoAverageBitRateKey] as? NSNumber { | ||||
|         // Multiply the current value by the given multiplier | ||||
|         let newBitRate = Int(currentBitRate.doubleValue * bitRateMultiplier) | ||||
|         ReactLogger.log(level: .info, message: "Setting Video Bit-Rate from \(currentBitRate) bps to \(newBitRate) bps...") | ||||
|  | ||||
|         compressionSettings[AVVideoAverageBitRateKey] = NSNumber(value: newBitRate) | ||||
|         settings[AVVideoCompressionPropertiesKey] = compressionSettings | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return settings | ||||
|   | ||||
| @@ -14,9 +14,14 @@ struct RecordVideoOptions { | ||||
|   var flash: Torch = .off | ||||
|   var codec: AVVideoCodecType? | ||||
|   /** | ||||
|    Bit-Rate of the Video, in Megabits per second (Mbps) | ||||
|    * Full Bit-Rate override for the Video Encoder, in Megabits per second (Mbps) | ||||
|    */ | ||||
|   var bitRate: Double? | ||||
|   var bitRateOverride: Double? | ||||
|   /** | ||||
|    * A multiplier applied to whatever the currently set bit-rate is, whether it's automatically computed by the OS Encoder, | ||||
|    * or set via bitRate, in Megabits per second (Mbps) | ||||
|    */ | ||||
|   var bitRateMultiplier: Double? | ||||
|  | ||||
|   init(fromJSValue dictionary: NSDictionary) throws { | ||||
|     // File Type (.mov or .mp4) | ||||
| @@ -31,9 +36,13 @@ struct RecordVideoOptions { | ||||
|     if let codecOption = dictionary["videoCodec"] as? String { | ||||
|       codec = try AVVideoCodecType(withString: codecOption) | ||||
|     } | ||||
|     // BitRate | ||||
|     if let parsed = dictionary["videoBitRate"] as? Double { | ||||
|       bitRate = parsed | ||||
|     // BitRate Override | ||||
|     if let parsed = dictionary["videoBitRateOverride"] as? Double { | ||||
|       bitRateOverride = parsed | ||||
|     } | ||||
|     // BitRate Multiplier | ||||
|     if let parsed = dictionary["videoBitRateMultiplier"] as? Double { | ||||
|       bitRateMultiplier = parsed | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -35,6 +35,10 @@ type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onE | ||||
|   onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void | ||||
|   onViewReady: () => void | ||||
| } | ||||
| type NativeRecordVideoOptions = Omit<RecordVideoOptions, 'onRecordingError' | 'onRecordingFinished' | 'videoBitRate'> & { | ||||
|   videoBitRateOverride?: number | ||||
|   videoBitRateMultiplier?: number | ||||
| } | ||||
| type RefType = React.Component<NativeCameraViewProps> & Readonly<NativeMethods> | ||||
| //#endregion | ||||
|  | ||||
| @@ -122,30 +126,15 @@ export class Camera extends React.PureComponent<CameraProps> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private calculateBitRate(bitRate: 'low' | 'normal' | 'high', codec: 'h264' | 'h265' = 'h264'): number { | ||||
|     const format = this.props.format | ||||
|     if (format == null) { | ||||
|       throw new CameraRuntimeError( | ||||
|         'parameter/invalid-combination', | ||||
|         `A videoBitRate of '${bitRate}' can only be used in combination with a 'format'!`, | ||||
|       ) | ||||
|   private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number { | ||||
|     switch (bitRate) { | ||||
|       case 'low': | ||||
|         return 0.8 | ||||
|       case 'high': | ||||
|         return 1.2 | ||||
|       default: | ||||
|         return 1 | ||||
|     } | ||||
|  | ||||
|     const factor = { | ||||
|       low: 0.8, | ||||
|       normal: 1, | ||||
|       high: 1.2, | ||||
|     }[bitRate] | ||||
|     let result = (30 / (3840 * 2160 * 0.75)) * (format.videoWidth * format.videoHeight) | ||||
|     // FPS - 30 is default, 60 would be 2x, 120 would be 4x | ||||
|     const fps = this.props.fps ?? Math.min(format.maxFps, 30) | ||||
|     result = (result / 30) * fps | ||||
|     // H.265 (HEVC) codec is 20% more efficient | ||||
|     if (codec === 'h265') result = result * 0.8 | ||||
|     // 10-Bit Video HDR takes up 20% more pixels than standard range (8-bit SDR) | ||||
|     if (this.props.videoHdr) result = result * 1.2 | ||||
|     // Return overall result | ||||
|     return result * factor | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -165,12 +154,20 @@ export class Camera extends React.PureComponent<CameraProps> { | ||||
|    * ``` | ||||
|    */ | ||||
|   public startRecording(options: RecordVideoOptions): void { | ||||
|     const { onRecordingError, onRecordingFinished, ...passThroughOptions } = options | ||||
|     const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options | ||||
|     if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') | ||||
|       throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!') | ||||
|  | ||||
|     const videoBitRate = passThroughOptions.videoBitRate | ||||
|     if (typeof videoBitRate === 'string') passThroughOptions.videoBitRate = this.calculateBitRate(videoBitRate, options.videoCodec) | ||||
|     const nativeOptions: NativeRecordVideoOptions = passThruOptions | ||||
|     if (typeof videoBitRate === 'string') { | ||||
|       // If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it | ||||
|       delete nativeOptions.videoBitRateOverride | ||||
|       nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate) | ||||
|     } else { | ||||
|       // If the user passed an absolute number as a bit-rate, we just use this as a full override. | ||||
|       delete nativeOptions.videoBitRateOverride | ||||
|       nativeOptions.videoBitRateOverride = videoBitRate | ||||
|     } | ||||
|  | ||||
|     const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => { | ||||
|       if (error != null) return onRecordingError(error) | ||||
| @@ -178,7 +175,7 @@ export class Camera extends React.PureComponent<CameraProps> { | ||||
|     } | ||||
|     try { | ||||
|       // TODO: Use TurboModules to make this awaitable. | ||||
|       CameraModule.startRecording(this.handle, passThroughOptions, onRecordCallback) | ||||
|       CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback) | ||||
|     } catch (e) { | ||||
|       throw tryParseNativeCameraError(e) | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user