Skip to content

Commit 8cc9803

Browse files
authored
feat: Add Android support to videoBitRate (move it to props) (#3269)
* feat: Add Android support to `videoBitRate` (move it to props) * chore: Update Podfile.lock * chore: Format * chore: Add `?` to SurfaceHolder again * chore: Make prettier happy `(` * chore: Fix remove question * chore: Update Podfile * feat: Add `bitRateModifier` to Android * chore: Fix conflicing overrides * fix: Replace `bitRateModifier` -> `bitRateMultiplier` * fix: Fix iOS build * fix: Use `CamcorderProfile` to find recommended video bit-rate * fix: Its already in bps
1 parent 48b4300 commit 8cc9803

File tree

16 files changed

+161
-116
lines changed

16 files changed

+161
-116
lines changed

docs/docs/guides/RECORDING_VIDEOS.mdx

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ camera.current.startRecording({
5454
})
5555
```
5656

57-
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.
57+
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.
5858

5959
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.
6060

@@ -119,22 +119,16 @@ If the device does not support `h265`, VisionCamera will automatically fall-back
119119

120120
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.
121121

122-
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%:
122+
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%:
123123

124-
```ts
125-
camera.current.startRecording({
126-
...props,
127-
videoBitRate: 'high'
128-
})
124+
```jsx
125+
<Camera {...props} videoBitRate="high" />
129126
```
130127

131-
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%:
128+
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%:
132129

133-
```ts
134-
camera.current.startRecording({
135-
...props,
136-
videoBitRate: 'low'
137-
})
130+
```jsx
131+
<Camera {...props} videoBitRate="low" />
138132
```
139133

140134
#### Custom Bit Rate
@@ -162,13 +156,10 @@ if (codec === 'h265') bitRate *= 0.8 // H.265
162156
bitRate *= yourCustomFactor // e.g. 0.5x for half the bit-rate
163157
```
164158

165-
And then pass it to the [`startRecording(...)`](/docs/api/classes/Camera#startrecording) function (in Mbps):
159+
And then pass it to the `<Camera>` component (in Mbps):
166160

167-
```ts
168-
camera.current.startRecording({
169-
...props,
170-
videoBitRate: bitRate // Mbps
171-
})
161+
```jsx
162+
<Camera {...props} videoBitRate={bitRate} />
172163
```
173164

174165
### Video Frame Rate (FPS)

example/ios/Podfile.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ PODS:
77
- hermes-engine (0.75.4):
88
- hermes-engine/Pre-built (= 0.75.4)
99
- hermes-engine/Pre-built (0.75.4)
10-
- MMKV (1.3.9):
11-
- MMKVCore (~> 1.3.9)
12-
- MMKVCore (1.3.9)
10+
- MMKV (2.0.0):
11+
- MMKVCore (~> 2.0.0)
12+
- MMKVCore (2.0.0)
1313
- RCT-Folly (2024.01.01.00):
1414
- boost
1515
- DoubleConversion
@@ -2027,8 +2027,8 @@ SPEC CHECKSUMS:
20272027
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
20282028
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
20292029
hermes-engine: ea92f60f37dba025e293cbe4b4a548fd26b610a0
2030-
MMKV: 817ba1eea17421547e01e087285606eb270a8dcb
2031-
MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9
2030+
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
2031+
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390
20322032
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
20332033
RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1
20342034
RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b
@@ -2098,8 +2098,8 @@ SPEC CHECKSUMS:
20982098
RNVectorIcons: 6382277afab3c54658e9d555ee0faa7a37827136
20992099
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
21002100
VisionCamera: 3b7cfecebbcb3871c4dd3fb47791b96a47fe5371
2101-
Yoga: aa3df615739504eebb91925fc9c58b4922ea9a08
2101+
Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6
21022102

2103-
PODFILE CHECKSUM: a43dbce8eba88fb736654cbed5c32f2a764615ef
2103+
PODFILE CHECKSUM: 2ad84241179871ca890f7c65c855d117862f1a68
21042104

2105-
COCOAPODS: 1.15.2
2105+
COCOAPODS: 1.16.1

package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ data class CameraConfiguration(
4848
// Output<T> types, those need to be comparable
4949
data class CodeScanner(val codeTypes: List<CodeType>)
5050
data class Photo(val isMirrored: Boolean, val enableHdr: Boolean, val photoQualityBalance: QualityBalance)
51-
data class Video(val isMirrored: Boolean, val enableHdr: Boolean)
51+
data class Video(val isMirrored: Boolean, val enableHdr: Boolean, val bitRateOverride: Double?, val bitRateMultiplier: Double?)
5252
data class FrameProcessor(val isMirrored: Boolean, val pixelFormat: PixelFormat)
5353
data class Audio(val nothing: Unit)
5454
data class Preview(val surfaceProvider: SurfaceProvider)

package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Configuration.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.mrousavy.camera.core.extensions.*
2222
import com.mrousavy.camera.core.types.CameraDeviceFormat
2323
import com.mrousavy.camera.core.types.Torch
2424
import com.mrousavy.camera.core.types.VideoStabilizationMode
25+
import com.mrousavy.camera.core.utils.CamcorderProfileUtils
2526
import kotlin.math.roundToInt
2627

2728
private fun assertFormatRequirement(
@@ -44,7 +45,8 @@ private fun assertFormatRequirement(
4445
@SuppressLint("RestrictedApi")
4546
@Suppress("LiftReturnOrAssignment")
4647
internal fun CameraSession.configureOutputs(configuration: CameraConfiguration) {
47-
Log.i(CameraSession.TAG, "Creating new Outputs for Camera #${configuration.cameraId}...")
48+
val cameraId = configuration.cameraId!!
49+
Log.i(CameraSession.TAG, "Creating new Outputs for Camera #$cameraId...")
4850
val fpsRange = configuration.targetFpsRange
4951
val format = configuration.format
5052

@@ -120,11 +122,24 @@ internal fun CameraSession.configureOutputs(configuration: CameraConfiguration)
120122
// We are currently not recording, so we can re-create a recorder instance if needed.
121123
Log.i(CameraSession.TAG, "Creating new Recorder...")
122124
Recorder.Builder().also { recorder ->
123-
configuration.format?.let { format ->
125+
format?.let { format ->
124126
recorder.setQualitySelector(format.videoQualitySelector)
125127
}
126-
// TODO: Make videoBitRate a Camera Prop
127-
// video.setTargetVideoEncodingBitRate()
128+
videoConfig.config.bitRateOverride?.let { bitRateOverride ->
129+
val bps = bitRateOverride * 1_000_000
130+
recorder.setTargetVideoEncodingBitRate(bps.toInt())
131+
}
132+
videoConfig.config.bitRateMultiplier?.let { bitRateMultiplier ->
133+
if (format == null) {
134+
// We need to get the videoSize to estimate the bitRate modifier
135+
throw PropRequiresFormatToBeNonNullError("videoBitRate")
136+
}
137+
val recommendedBitRate = CamcorderProfileUtils.getRecommendedBitRate(cameraId, format.videoSize)
138+
if (recommendedBitRate != null) {
139+
val targetBitRate = recommendedBitRate.toDouble() * bitRateMultiplier
140+
recorder.setTargetVideoEncodingBitRate(targetBitRate.toInt())
141+
}
142+
}
128143
}.build()
129144
}
130145

package/android/src/main/java/com/mrousavy/camera/core/types/RecordVideoOptions.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,16 @@ import com.facebook.react.bridge.ReadableMap
55
import com.mrousavy.camera.core.utils.FileUtils
66
import com.mrousavy.camera.core.utils.OutputFile
77

8-
class RecordVideoOptions(
9-
val file: OutputFile,
10-
val videoCodec: VideoCodec,
11-
val videoBitRateOverride: Double?,
12-
val videoBitRateMultiplier: Double?
13-
) {
8+
class RecordVideoOptions(val file: OutputFile, val videoCodec: VideoCodec) {
149

1510
companion object {
1611
fun fromJSValue(context: Context, map: ReadableMap): RecordVideoOptions {
1712
val directory = if (map.hasKey("path")) FileUtils.getDirectory(map.getString("path")) else context.cacheDir
1813
val fileType = if (map.hasKey("fileType")) VideoFileType.fromUnionValue(map.getString("fileType")) else VideoFileType.MOV
1914
val videoCodec = if (map.hasKey("videoCodec")) VideoCodec.fromUnionValue(map.getString("videoCodec")) else VideoCodec.H264
20-
val videoBitRateOverride = if (map.hasKey("videoBitRateOverride")) map.getDouble("videoBitRateOverride") else null
21-
val videoBitRateMultiplier = if (map.hasKey("videoBitRateMultiplier")) map.getDouble("videoBitRateMultiplier") else null
2215

2316
val outputFile = OutputFile(context, directory, fileType.toExtension())
24-
return RecordVideoOptions(outputFile, videoCodec, videoBitRateOverride, videoBitRateMultiplier)
17+
return RecordVideoOptions(outputFile, videoCodec)
2518
}
2619
}
2720
}

package/android/src/main/java/com/mrousavy/camera/core/utils/CamcorderProfileUtils.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.mrousavy.camera.core.utils
22

3+
import android.annotation.SuppressLint
34
import android.media.CamcorderProfile
45
import android.os.Build
6+
import android.util.Log
57
import android.util.Size
68
import kotlin.math.abs
79

810
class CamcorderProfileUtils {
911
companion object {
12+
private const val TAG = "CamcorderProfileUtils"
13+
1014
private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int =
1115
when (camcorderProfile) {
1216
CamcorderProfile.QUALITY_QCIF -> 176 * 144
@@ -29,6 +33,7 @@ class CamcorderProfileUtils {
2933
val targetResolution = resolution.width * resolution.height
3034
val cameraIdInt = cameraId.toIntOrNull()
3135

36+
@SuppressLint("InlinedApi")
3237
var profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile ->
3338
if (cameraIdInt != null) {
3439
return@filter CamcorderProfile.hasProfile(cameraIdInt, profile)
@@ -70,6 +75,7 @@ class CamcorderProfileUtils {
7075
return null
7176
} catch (e: Throwable) {
7277
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
78+
Log.e(TAG, "Failed to get maximum video size for Camera ID $cameraId! ${e.message}", e)
7379
return null
7480
}
7581
}
@@ -94,6 +100,32 @@ class CamcorderProfileUtils {
94100
return null
95101
} catch (e: Throwable) {
96102
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
103+
Log.e(TAG, "Failed to get maximum FPS for Camera ID $cameraId! ${e.message}", e)
104+
return null
105+
}
106+
}
107+
108+
fun getRecommendedBitRate(cameraId: String, videoSize: Size): Int? {
109+
try {
110+
val quality = findClosestCamcorderProfileQuality(cameraId, videoSize, true)
111+
112+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
113+
val profiles = CamcorderProfile.getAll(cameraId, quality)
114+
if (profiles != null) {
115+
return profiles.videoProfiles.maxOf { profile -> profile.bitrate }
116+
}
117+
}
118+
119+
val cameraIdInt = cameraId.toIntOrNull()
120+
if (cameraIdInt != null) {
121+
val profile = CamcorderProfile.get(cameraIdInt, quality)
122+
return profile.videoBitRate
123+
}
124+
125+
return null
126+
} catch (e: Throwable) {
127+
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
128+
Log.e(TAG, "Failed to get recommended video bit-rate for Camera ID $cameraId! ${e.message}", e)
97129
return null
98130
}
99131
}

package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
3737
// TODO: takePhoto() depth data
3838
// TODO: takePhoto() raw capture
3939
// TODO: takePhoto() return with jsi::Value Image reference for faster capture
40-
// TODO: Support videoCodec and videoBitRate on Android
40+
// TODO: Support videoCodec on Android
4141

4242
@SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission")
4343
class CameraView(context: Context) :
@@ -75,6 +75,8 @@ class CameraView(context: Context) :
7575
var videoStabilizationMode: VideoStabilizationMode? = null
7676
var videoHdr = false
7777
var photoHdr = false
78+
var videoBitRateOverride: Double? = null
79+
var videoBitRateMultiplier: Double? = null
7880

7981
// TODO: Use .BALANCED once CameraX fixes it https://issuetracker.google.com/issues/337214687
8082
var photoQualityBalance = QualityBalance.SPEED
@@ -180,7 +182,10 @@ class CameraView(context: Context) :
180182

181183
// Video
182184
if (video || enableFrameProcessor) {
183-
config.video = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Video(isMirrored, videoHdr))
185+
config.video =
186+
CameraConfiguration.Output.Enabled.create(
187+
CameraConfiguration.Video(isMirrored, videoHdr, videoBitRateOverride, videoBitRateMultiplier)
188+
)
184189
} else {
185190
config.video = CameraConfiguration.Output.Disabled.create()
186191
}

package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,24 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
191191
view.videoHdr = videoHdr
192192
}
193193

194+
@ReactProp(name = "videoBitRateOverride", defaultDouble = -1.0)
195+
fun setVideoBitRateOverride(view: CameraView, videoBitRateOverride: Double) {
196+
if (videoBitRateOverride != -1.0) {
197+
view.videoBitRateOverride = videoBitRateOverride
198+
} else {
199+
view.videoBitRateOverride = null
200+
}
201+
}
202+
203+
@ReactProp(name = "videoBitRateMultiplier", defaultDouble = -1.0)
204+
fun setVideoBitRateMultiplier(view: CameraView, videoBitRateMultiplier: Double) {
205+
if (videoBitRateMultiplier != -1.0) {
206+
view.videoBitRateMultiplier = videoBitRateMultiplier
207+
} else {
208+
view.videoBitRateMultiplier = null
209+
}
210+
}
211+
194212
@ReactProp(name = "lowLightBoost")
195213
fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean) {
196214
view.lowLightBoost = lowLightBoost

package/ios/Core/Types/RecordVideoOptions.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct RecordVideoOptions {
2424
*/
2525
var bitRateMultiplier: Double?
2626

27-
init(fromJSValue dictionary: NSDictionary) throws {
27+
init(fromJSValue dictionary: NSDictionary, bitRateOverride: Double? = nil, bitRateMultiplier: Double? = nil) throws {
2828
// File Type (.mov or .mp4)
2929
if let fileTypeOption = dictionary["fileType"] as? String {
3030
fileType = try AVFileType(withString: fileTypeOption)
@@ -38,13 +38,9 @@ struct RecordVideoOptions {
3838
codec = try AVVideoCodecType(withString: codecOption)
3939
}
4040
// BitRate Override
41-
if let parsed = dictionary["videoBitRateOverride"] as? Double {
42-
bitRateOverride = parsed
43-
}
41+
self.bitRateOverride = bitRateOverride
4442
// BitRate Multiplier
45-
if let parsed = dictionary["videoBitRateMultiplier"] as? Double {
46-
bitRateMultiplier = parsed
47-
}
43+
self.bitRateMultiplier = bitRateMultiplier
4844
// Custom Path
4945
let fileExtension = fileType.descriptor ?? "mov"
5046
if let customPath = dictionary["path"] as? String {

package/ios/React/CameraView+RecordVideo.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
1616
let callback = Callback(jsCallback)
1717

1818
do {
19-
let options = try RecordVideoOptions(fromJSValue: options)
19+
let options = try RecordVideoOptions(fromJSValue: options,
20+
bitRateOverride: videoBitRateOverride?.doubleValue,
21+
bitRateMultiplier: videoBitRateMultiplier?.doubleValue)
2022

2123
// Start Recording with success and error callbacks
2224
cameraSession.startRecording(

0 commit comments

Comments
 (0)