feat: Add customizable Video Bit Rate (videoBitRate
) (#1882)
* feat: Add `videoBitRate` option to `RecordVideoOptions` * feat: Implement `videoBitRate` on iOS * feat: Implement `videoBitRate` on Android * chore: Format * docs: Separate recording and photo docs * docs: Fix links * docs: Add docs about bitrate and quality * docs: Add blob * fix: Don't use inline style for CI * fix: Correctly log default bitRate * fix: Fix typo * fix: Calculate default bit-rate on Android depending on resolution * Update RecordingSession.kt
This commit is contained in:
parent
1c8c081e11
commit
902dc634a4
@ -71,7 +71,7 @@ function App() {
|
|||||||
|
|
||||||
### Capture Errors
|
### Capture Errors
|
||||||
|
|
||||||
The `CameraCaptureError` represents any kind of error that occured only while capturing a photo or recording a video.
|
The `CameraCaptureError` represents any kind of error that occured only while taking a photo or recording a video.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -21,7 +21,7 @@ There are formats specifically designed for high-resolution photo capture (but l
|
|||||||
|
|
||||||
If you don't want to specify a Camera Format, you don't have to. The Camera automatically chooses the best matching format for the current camera device. This is why the Camera's `format` property is _optional_.
|
If you don't want to specify a Camera Format, you don't have to. The Camera automatically chooses the best matching format for the current camera device. This is why the Camera's `format` property is _optional_.
|
||||||
|
|
||||||
**🚀 Continue with: [Taking Photos/Recording Videos](./capturing)**
|
**🚀 Continue with: [Taking Photos](./taking-photos)**
|
||||||
|
|
||||||
## Choosing custom formats
|
## Choosing custom formats
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ To understand a bit more about camera formats, you first need to understand a fe
|
|||||||
|
|
||||||
To get all available formats, simply use the `CameraDevice`'s [`formats` property](/docs/api/interfaces/CameraDevice#formats). These are a [CameraFormat's](/docs/api/interfaces/CameraDeviceFormat) props:
|
To get all available formats, simply use the `CameraDevice`'s [`formats` property](/docs/api/interfaces/CameraDevice#formats). These are a [CameraFormat's](/docs/api/interfaces/CameraDeviceFormat) props:
|
||||||
|
|
||||||
- [`photoHeight`](/docs/api/interfaces/CameraDeviceFormat#photoHeight)/[`photoWidth`](/docs/api/interfaces/CameraDeviceFormat#photoWidth): The resolution that will be used for capturing photos. Choose a format with your desired resolution.
|
- [`photoHeight`](/docs/api/interfaces/CameraDeviceFormat#photoHeight)/[`photoWidth`](/docs/api/interfaces/CameraDeviceFormat#photoWidth): The resolution that will be used for taking photos. Choose a format with your desired resolution.
|
||||||
- [`videoHeight`](/docs/api/interfaces/CameraDeviceFormat#videoHeight)/[`videoWidth`](/docs/api/interfaces/CameraDeviceFormat#videoWidth): The resolution that will be used for recording videos. Choose a format with your desired resolution.
|
- [`videoHeight`](/docs/api/interfaces/CameraDeviceFormat#videoHeight)/[`videoWidth`](/docs/api/interfaces/CameraDeviceFormat#videoWidth): The resolution that will be used for recording videos. Choose a format with your desired resolution.
|
||||||
- [`minFps`](/docs/api/interfaces/CameraDeviceFormat#minFps)/[`maxFps`](/docs/api/interfaces/CameraDeviceFormat#maxFps): A range of possible values for the `fps` property. For example, if your format has `minFps: 1` and `maxFps: 60`, you can either use `fps={30}`, `fps={60}` or any other value in between for recording videos.
|
- [`minFps`](/docs/api/interfaces/CameraDeviceFormat#minFps)/[`maxFps`](/docs/api/interfaces/CameraDeviceFormat#maxFps): A range of possible values for the `fps` property. For example, if your format has `minFps: 1` and `maxFps: 60`, you can either use `fps={30}`, `fps={60}` or any other value in between for recording videos.
|
||||||
- [`videoStabilizationModes`](/docs/api/interfaces/CameraDeviceFormat#videoStabilizationModes): All supported Video Stabilization Modes, digital and optical. If this specific format contains your desired [`VideoStabilizationMode`](/docs/api/#videostabilizationmode), you can pass it to your `<Camera>` via the [`videoStabilizationMode` property](/docs/api/interfaces/CameraProps#videoStabilizationMode).
|
- [`videoStabilizationModes`](/docs/api/interfaces/CameraDeviceFormat#videoStabilizationModes): All supported Video Stabilization Modes, digital and optical. If this specific format contains your desired [`VideoStabilizationMode`](/docs/api/#videostabilizationmode), you can pass it to your `<Camera>` via the [`videoStabilizationMode` property](/docs/api/interfaces/CameraProps#videoStabilizationMode).
|
||||||
@ -180,4 +180,4 @@ Other props that depend on the `format`:
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
#### 🚀 Next section: [Taking Photos/Recording Videos](./capturing)
|
#### 🚀 Next section: [Taking Photos](./taking-photos)
|
||||||
|
166
docs/docs/guides/RECORDING_VIDEOS.mdx
Normal file
166
docs/docs/guides/RECORDING_VIDEOS.mdx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
id: recording-videos
|
||||||
|
title: Recording Videos
|
||||||
|
sidebar_label: Recording Videos
|
||||||
|
---
|
||||||
|
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl'
|
||||||
|
|
||||||
|
<div class="image-container">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="283" height="535">
|
||||||
|
<image href={useBaseUrl("img/demo_capture.gif")} x="18" y="33" width="247" height="469" />
|
||||||
|
<image href={useBaseUrl("img/frame.png")} width="283" height="535" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Camera Functions
|
||||||
|
|
||||||
|
The Camera provides certain functions which are available through a [ref object](https://reactjs.org/docs/refs-and-the-dom.html):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function App() {
|
||||||
|
const camera = useRef<Camera>(null)
|
||||||
|
// ...
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Camera
|
||||||
|
ref={camera}
|
||||||
|
{...cameraProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To use these functions, you need to wait until the [`onInitialized`](/docs/api/interfaces/CameraProps#oninitialized) event has been fired.
|
||||||
|
|
||||||
|
## Recording Videos
|
||||||
|
|
||||||
|
To start a video recording you first have to enable video capture:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Camera
|
||||||
|
{...props}
|
||||||
|
video={true}
|
||||||
|
audio={true} // <-- optional
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, simply use the Camera's [`startRecording(...)`](/docs/api/classes/Camera#startrecording) function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
camera.current.startRecording({
|
||||||
|
onRecordingFinished: (video) => console.log(video),
|
||||||
|
onRecordingError: (error) => console.error(error)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To stop the video recording, you can call [`stopRecording(...)`](/docs/api/classes/Camera#stoprecording):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await camera.current.stopRecording()
|
||||||
|
```
|
||||||
|
|
||||||
|
Once a recording has been stopped, the `onRecordingFinished` callback passed to the [`stopRecording(...)`](/docs/api/classes/Camera#stoprecording) function will be invoked with a [`VideoFile`](/docs/api/interfaces/VideoFile) which you can then use to display in a [`<Video>`](https://github.com/react-native-video/react-native-video) component, uploaded to a backend, or saved to the Camera Roll using [react-native-cameraroll](https://github.com/react-native-cameraroll/react-native-cameraroll).
|
||||||
|
|
||||||
|
### Pause/Resume
|
||||||
|
|
||||||
|
To pause/resume the recordings, you can use [`pauseRecording()`](/docs/api/classes/Camera#pauserecording) and [`resumeRecording()`](/docs/api/classes/Camera#resumerecording):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await camera.current.pauseRecording()
|
||||||
|
...
|
||||||
|
await camera.current.resumeRecording()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video Codec
|
||||||
|
|
||||||
|
By default, videos are recorded in the H.264 video codec which is a widely adopted video codec.
|
||||||
|
|
||||||
|
VisionCamera also supports H.265 ([HEVC](https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding)), which is much more efficient in encoding performance as well as an up to 20% smaller file size:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
camera.current.startRecording({
|
||||||
|
...props,
|
||||||
|
videoCodec: 'h265'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video Bit Rate
|
||||||
|
|
||||||
|
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%:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
camera.current.startRecording({
|
||||||
|
...props,
|
||||||
|
videoBitRate: 'high'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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%:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
camera.current.startRecording({
|
||||||
|
...props,
|
||||||
|
videoBitRate: 'low'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Bit Rate
|
||||||
|
|
||||||
|
If you want to use a custom bit-rate, you first need to understand how bit-rate is calculated.
|
||||||
|
|
||||||
|
The bit-rate is a product of multiple factors such as resolution, FPS, pixel-format (HDR or non HDR), and video codec.
|
||||||
|
As a good starting point, those are the recommended base bit-rates for their respective resolutions:
|
||||||
|
|
||||||
|
- 480p: 2 Mbps
|
||||||
|
- 720p: 5 Mbps
|
||||||
|
- 1080p: 10 Mbps
|
||||||
|
- 4K: 30 Mbps
|
||||||
|
- 8K: 100 Mbps
|
||||||
|
|
||||||
|
These bit-rates assume a frame rate of 30 FPS, a non-HDR pixel-format, and a H.264 video codec.
|
||||||
|
|
||||||
|
To calculate your target bit-rate, you can use this formula:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
let bitRate = baseBitRate
|
||||||
|
bitRate = bitRate / 30 * fps // FPS
|
||||||
|
if (hdr === true) bitRate *= 1.2 // HDR
|
||||||
|
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):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
camera.current.startRecording({
|
||||||
|
...props,
|
||||||
|
videoBitRate: bitRate // Mbps
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Saving the Video to the Camera Roll
|
||||||
|
|
||||||
|
Since the Video is stored as a temporary file, you need save it to the Camera Roll to permanentely store it. You can use [react-native-cameraroll](https://github.com/react-native-cameraroll/react-native-cameraroll) for this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
camera.current.startRecording({
|
||||||
|
...props,
|
||||||
|
onRecordingFinished: (video) => {
|
||||||
|
const path = video.path
|
||||||
|
await CameraRoll.save(`file://${path}`, {
|
||||||
|
type: 'video',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
#### 🚀 Next section: [Frame Processors](frame-processors)
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: capturing
|
id: taking-photos
|
||||||
title: Taking Photos/Recording Videos
|
title: Taking Photos
|
||||||
sidebar_label: Taking Photos/Recording Videos
|
sidebar_label: Taking Photos
|
||||||
---
|
---
|
||||||
|
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl'
|
import useBaseUrl from '@docusaurus/useBaseUrl'
|
||||||
@ -33,7 +33,7 @@ function App() {
|
|||||||
|
|
||||||
To use these functions, you need to wait until the [`onInitialized`](/docs/api/interfaces/CameraProps#oninitialized) event has been fired.
|
To use these functions, you need to wait until the [`onInitialized`](/docs/api/interfaces/CameraProps#oninitialized) event has been fired.
|
||||||
|
|
||||||
### Taking Photos
|
## Taking Photos
|
||||||
|
|
||||||
To take a photo you first have to enable photo capture:
|
To take a photo you first have to enable photo capture:
|
||||||
|
|
||||||
@ -47,57 +47,46 @@ To take a photo you first have to enable photo capture:
|
|||||||
Then, simply use the Camera's [`takePhoto(...)`](/docs/api/classes/Camera#takephoto) function:
|
Then, simply use the Camera's [`takePhoto(...)`](/docs/api/classes/Camera#takephoto) function:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const photo = await camera.current.takePhoto({
|
const photo = await camera.current.takePhoto()
|
||||||
flash: 'on'
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can customize capture options such as [automatic red-eye reduction](/docs/api/interfaces/TakePhotoOptions#enableautoredeyereduction), [automatic image stabilization](/docs/api/interfaces/TakePhotoOptions#enableautostabilization), [enable flash](/docs/api/interfaces/TakePhotoOptions#flash), [prioritize speed over quality](/docs/api/interfaces/TakePhotoOptions#qualityprioritization), [disable the shutter sound](/docs/api/interfaces/TakePhotoOptions#enableshuttersound) and more using the [`TakePhotoOptions`](/docs/api/interfaces/TakePhotoOptions) parameter.
|
You can customize capture options such as [automatic red-eye reduction](/docs/api/interfaces/TakePhotoOptions#enableautoredeyereduction), [automatic image stabilization](/docs/api/interfaces/TakePhotoOptions#enableautostabilization), [enable flash](/docs/api/interfaces/TakePhotoOptions#flash), [prioritize speed over quality](/docs/api/interfaces/TakePhotoOptions#qualityprioritization), [disable the shutter sound](/docs/api/interfaces/TakePhotoOptions#enableshuttersound) and more using the [`TakePhotoOptions`](/docs/api/interfaces/TakePhotoOptions) parameter.
|
||||||
|
|
||||||
This function returns a [`PhotoFile`](/docs/api/interfaces/PhotoFile) which is stored in a temporary directory and can either be displayed using `<Image>` or `<FastImage>`, uploaded to a backend, or saved to the Camera Roll using [react-native-cameraroll](https://github.com/react-native-cameraroll/react-native-cameraroll).
|
This function returns a [`PhotoFile`](/docs/api/interfaces/PhotoFile) which is stored in a temporary directory and can either be displayed using `<Image>` or `<FastImage>`, uploaded to a backend, or saved to the Camera Roll using [react-native-cameraroll](https://github.com/react-native-cameraroll/react-native-cameraroll).
|
||||||
|
|
||||||
### Recording Videos
|
### Fast Capture
|
||||||
|
|
||||||
To start a video recording you first have to enable video capture:
|
The [`takePhoto(...)`](/docs/api/classes/Camera#takephoto) function can be configured for faster capture at the cost of lower quality:
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Camera
|
|
||||||
{...props}
|
|
||||||
video={true}
|
|
||||||
audio={true} // <-- optional
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, simply use the Camera's [`startRecording(...)`](/docs/api/classes/Camera#startrecording) function:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
camera.current.startRecording({
|
const photo = await camera.current.takePhoto({
|
||||||
flash: 'on',
|
qualityPrioritization: 'speed',
|
||||||
onRecordingFinished: (video) => console.log(video),
|
flash: 'off',
|
||||||
onRecordingError: (error) => console.error(error),
|
enableShutterSound: false
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
## Saving the Photo to the Camera Roll
|
||||||
|
|
||||||
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.
|
Since the Photo is stored as a temporary file, you need to save it to the Camera Roll to permanentely store it. You can use [react-native-cameraroll](https://github.com/react-native-cameraroll/react-native-cameraroll) for this:
|
||||||
|
|
||||||
To stop the video recording, you can call [`stopRecording(...)`](/docs/api/classes/Camera#stoprecording):
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await camera.current.stopRecording()
|
const path = await camera.current.takePhoto()
|
||||||
|
await CameraRoll.save(`file://${path}`, {
|
||||||
|
type: 'photo',
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Once a recording has been stopped, the `onRecordingFinished` callback passed to the `startRecording(..)` function will be invoked with a [`VideoFile`](/docs/api/interfaces/VideoFile) which you can then use to display in a [`<Video>`](https://github.com/react-native-video/react-native-video) component, uploaded to a backend, or saved to the Camera Roll using [react-native-cameraroll](https://github.com/react-native-cameraroll/react-native-cameraroll).
|
## Getting the Photo's data
|
||||||
|
|
||||||
To pause/resume the recordings, you can use `pauseRecording()` and `resumeRecording()`:
|
To get the Photo's pixel data, you can use [`fetch(...)`](https://reactnative.dev/docs/network#using-fetch) to read the local file as a Blob:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await camera.current.pauseRecording()
|
const path = await camera.current.takePhoto()
|
||||||
...
|
const result = await fetch(`file://${path}`)
|
||||||
await camera.current.resumeRecording()
|
const data = await result.blob();
|
||||||
```
|
```
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
#### 🚀 Next section: [Frame Processors](frame-processors)
|
#### 🚀 Next section: [Recording Videos](recording-videos)
|
@ -5,7 +5,8 @@ module.exports = {
|
|||||||
'guides/devices',
|
'guides/devices',
|
||||||
'guides/lifecycle',
|
'guides/lifecycle',
|
||||||
'guides/formats',
|
'guides/formats',
|
||||||
'guides/capturing',
|
'guides/taking-photos',
|
||||||
|
'guides/recording-videos',
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Realtime Frame Processing',
|
label: 'Realtime Frame Processing',
|
||||||
|
@ -33,6 +33,10 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
|
|||||||
if (options.hasKey("fileType")) {
|
if (options.hasKey("fileType")) {
|
||||||
fileType = VideoFileType.fromUnionValue(options.getString("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 callback = { video: RecordingSession.Video ->
|
||||||
val map = Arguments.createMap()
|
val map = Arguments.createMap()
|
||||||
@ -44,7 +48,7 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
|
|||||||
val errorMap = makeErrorMap(error.code, error.message)
|
val errorMap = makeErrorMap(error.code, error.message)
|
||||||
onRecordCallback(null, errorMap)
|
onRecordCallback(null, errorMap)
|
||||||
}
|
}
|
||||||
cameraSession.startRecording(audio == true, codec, fileType, callback, onError)
|
cameraSession.startRecording(audio == true, codec, fileType, bitRate, callback, onError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
|
@ -279,6 +279,7 @@ class CameraSession(
|
|||||||
enableAudio: Boolean,
|
enableAudio: Boolean,
|
||||||
codec: VideoCodec,
|
codec: VideoCodec,
|
||||||
fileType: VideoFileType,
|
fileType: VideoFileType,
|
||||||
|
bitRate: Double?,
|
||||||
callback: (video: RecordingSession.Video) -> Unit,
|
callback: (video: RecordingSession.Video) -> Unit,
|
||||||
onError: (error: RecorderError) -> Unit
|
onError: (error: RecorderError) -> Unit
|
||||||
) {
|
) {
|
||||||
@ -287,7 +288,8 @@ class CameraSession(
|
|||||||
val outputs = outputs ?: throw CameraNotReadyError()
|
val outputs = outputs ?: throw CameraNotReadyError()
|
||||||
val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError()
|
val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError()
|
||||||
|
|
||||||
val recording = RecordingSession(context, videoOutput.size, enableAudio, fps, codec, orientation, fileType, callback, onError)
|
val recording =
|
||||||
|
RecordingSession(context, videoOutput.size, enableAudio, fps, codec, orientation, fileType, bitRate, callback, onError)
|
||||||
recording.start()
|
recording.start()
|
||||||
this.recording = recording
|
this.recording = recording
|
||||||
}
|
}
|
||||||
|
@ -22,14 +22,13 @@ class RecordingSession(
|
|||||||
private val codec: VideoCodec = VideoCodec.H264,
|
private val codec: VideoCodec = VideoCodec.H264,
|
||||||
private val orientation: Orientation,
|
private val orientation: Orientation,
|
||||||
private val fileType: VideoFileType = VideoFileType.MP4,
|
private val fileType: VideoFileType = VideoFileType.MP4,
|
||||||
|
videoBitRate: Double? = null,
|
||||||
private val callback: (video: Video) -> Unit,
|
private val callback: (video: Video) -> Unit,
|
||||||
private val onError: (error: RecorderError) -> Unit
|
private val onError: (error: RecorderError) -> Unit
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RecordingSession"
|
private const val TAG = "RecordingSession"
|
||||||
|
|
||||||
// bits per second
|
|
||||||
private const val VIDEO_BIT_RATE = 10_000_000
|
|
||||||
private const val AUDIO_SAMPLING_RATE = 44_100
|
private const val AUDIO_SAMPLING_RATE = 44_100
|
||||||
private const val AUDIO_BIT_RATE = 16 * AUDIO_SAMPLING_RATE
|
private const val AUDIO_BIT_RATE = 16 * AUDIO_SAMPLING_RATE
|
||||||
private const val AUDIO_CHANNELS = 1
|
private const val AUDIO_CHANNELS = 1
|
||||||
@ -37,6 +36,7 @@ class RecordingSession(
|
|||||||
|
|
||||||
data class Video(val path: String, val durationMs: Long)
|
data class Video(val path: String, val durationMs: Long)
|
||||||
|
|
||||||
|
private val bitRate = videoBitRate ?: getDefaultBitRate()
|
||||||
private val recorder: MediaRecorder
|
private val recorder: MediaRecorder
|
||||||
private val outputFile: File
|
private val outputFile: File
|
||||||
private var startTime: Long? = null
|
private var startTime: Long? = null
|
||||||
@ -44,7 +44,6 @@ class RecordingSession(
|
|||||||
val surface: Surface = MediaCodec.createPersistentInputSurface()
|
val surface: Surface = MediaCodec.createPersistentInputSurface()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
outputFile = File.createTempFile("mrousavy", fileType.toExtension(), context.cacheDir)
|
outputFile = File.createTempFile("mrousavy", fileType.toExtension(), context.cacheDir)
|
||||||
|
|
||||||
Log.i(TAG, "Creating RecordingSession for ${outputFile.absolutePath}")
|
Log.i(TAG, "Creating RecordingSession for ${outputFile.absolutePath}")
|
||||||
@ -56,11 +55,11 @@ class RecordingSession(
|
|||||||
|
|
||||||
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||||
recorder.setOutputFile(outputFile.absolutePath)
|
recorder.setOutputFile(outputFile.absolutePath)
|
||||||
recorder.setVideoEncodingBitRate(VIDEO_BIT_RATE)
|
recorder.setVideoEncodingBitRate((bitRate * 1_000_000).toInt())
|
||||||
recorder.setVideoSize(size.height, size.width)
|
recorder.setVideoSize(size.height, size.width)
|
||||||
if (fps != null) recorder.setVideoFrameRate(fps)
|
if (fps != null) recorder.setVideoFrameRate(fps)
|
||||||
|
|
||||||
Log.i(TAG, "Using $codec Video Codec..")
|
Log.i(TAG, "Using $codec Video Codec at $bitRate Mbps..")
|
||||||
recorder.setVideoEncoder(codec.toVideoCodec())
|
recorder.setVideoEncoder(codec.toVideoCodec())
|
||||||
if (enableAudio) {
|
if (enableAudio) {
|
||||||
Log.i(TAG, "Adding Audio Channel..")
|
Log.i(TAG, "Adding Audio Channel..")
|
||||||
@ -131,8 +130,22 @@ 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
|
||||||
|
}
|
||||||
|
baseBitRate = baseBitRate / 30.0 * (fps ?: 30).toDouble()
|
||||||
|
if (this.codec == VideoCodec.H265) baseBitRate *= 0.8
|
||||||
|
return baseBitRate
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val audio = if (enableAudio) "with audio" else "without audio"
|
val audio = if (enableAudio) "with audio" else "without audio"
|
||||||
return "${size.width} x ${size.height} @ $fps FPS $codec $fileType $orientation RecordingSession ($audio)"
|
return "${size.width} x ${size.height} @ $fps FPS $codec $fileType $orientation $bitRate Mbps RecordingSession ($audio)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,17 @@ PODS:
|
|||||||
- hermes-engine/Pre-built (= 0.72.3)
|
- hermes-engine/Pre-built (= 0.72.3)
|
||||||
- hermes-engine/Pre-built (0.72.3)
|
- hermes-engine/Pre-built (0.72.3)
|
||||||
- libevent (2.1.12)
|
- libevent (2.1.12)
|
||||||
- libwebp (1.3.1):
|
- libwebp (1.3.2):
|
||||||
- libwebp/demux (= 1.3.1)
|
- libwebp/demux (= 1.3.2)
|
||||||
- libwebp/mux (= 1.3.1)
|
- libwebp/mux (= 1.3.2)
|
||||||
- libwebp/sharpyuv (= 1.3.1)
|
- libwebp/sharpyuv (= 1.3.2)
|
||||||
- libwebp/webp (= 1.3.1)
|
- libwebp/webp (= 1.3.2)
|
||||||
- libwebp/demux (1.3.1):
|
- libwebp/demux (1.3.2):
|
||||||
- libwebp/webp
|
- libwebp/webp
|
||||||
- libwebp/mux (1.3.1):
|
- libwebp/mux (1.3.2):
|
||||||
- libwebp/demux
|
- libwebp/demux
|
||||||
- libwebp/sharpyuv (1.3.1)
|
- libwebp/sharpyuv (1.3.2)
|
||||||
- libwebp/webp (1.3.1):
|
- libwebp/webp (1.3.2):
|
||||||
- libwebp/sharpyuv
|
- libwebp/sharpyuv
|
||||||
- RCT-Folly (2021.07.22.00):
|
- RCT-Folly (2021.07.22.00):
|
||||||
- boost
|
- boost
|
||||||
@ -501,7 +501,7 @@ PODS:
|
|||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.10)
|
- SDWebImage/Core (~> 5.10)
|
||||||
- SocketRocket (0.6.1)
|
- SocketRocket (0.6.1)
|
||||||
- VisionCamera (3.0.0):
|
- VisionCamera (3.1.0):
|
||||||
- React
|
- React
|
||||||
- React-callinvoker
|
- React-callinvoker
|
||||||
- React-Core
|
- React-Core
|
||||||
@ -686,7 +686,7 @@ SPEC CHECKSUMS:
|
|||||||
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
||||||
hermes-engine: 10fbd3f62405c41ea07e71973ea61e1878d07322
|
hermes-engine: 10fbd3f62405c41ea07e71973ea61e1878d07322
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
libwebp: 33dc822fbbf4503668d09f7885bbfedc76c45e96
|
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||||
RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18
|
RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18
|
||||||
RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3
|
RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3
|
||||||
@ -733,7 +733,7 @@ SPEC CHECKSUMS:
|
|||||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||||
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
||||||
VisionCamera: 4780262974d65e89883a9b374d15459359e56ab3
|
VisionCamera: 5716d5c3f2e5cbcf0e58e30e91b09b2fbb1f12ef
|
||||||
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
|
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
|
||||||
|
|
||||||
PODFILE CHECKSUM: ab9c06b18c63e741c04349c0fd630c6d3145081c
|
PODFILE CHECKSUM: ab9c06b18c63e741c04349c0fd630c6d3145081c
|
||||||
|
@ -7,6 +7,7 @@ import { CameraPage } from './CameraPage'
|
|||||||
import type { Routes } from './Routes'
|
import type { Routes } from './Routes'
|
||||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera'
|
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera'
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||||
|
import { StyleSheet } from 'react-native'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<Routes>()
|
const Stack = createNativeStackNavigator<Routes>()
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ export function App(): React.ReactElement | null {
|
|||||||
const showPermissionsPage = cameraPermission !== 'granted' || microphonePermission === 'not-determined'
|
const showPermissionsPage = cameraPermission !== 'granted' || microphonePermission === 'not-determined'
|
||||||
return (
|
return (
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={styles.root}>
|
||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@ -52,3 +53,9 @@ export function App(): React.ReactElement | null {
|
|||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ -116,12 +116,20 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init Video
|
// Init Video
|
||||||
guard let videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput, fileType: fileType, videoCodec: videoCodec),
|
guard var videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput, fileType: fileType, videoCodec: videoCodec),
|
||||||
!videoSettings.isEmpty else {
|
!videoSettings.isEmpty else {
|
||||||
callback.reject(error: .capture(.createRecorderError(message: "Failed to get video settings!")))
|
callback.reject(error: .capture(.createRecorderError(message: "Failed to get video settings!")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom Video Bit Rate (Mbps -> bps)
|
||||||
|
if let videoBitRate = options["videoBitRate"] as? NSNumber {
|
||||||
|
let bitsPerSecond = videoBitRate.doubleValue * 1_000_000
|
||||||
|
videoSettings[AVVideoCompressionPropertiesKey] = [
|
||||||
|
AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// get pixel format (420f, 420v, x420)
|
// get pixel format (420f, 420v, x420)
|
||||||
let pixelFormat = CMFormatDescriptionGetMediaSubType(videoInput.device.activeFormat.formatDescription)
|
let pixelFormat = CMFormatDescriptionGetMediaSubType(videoInput.device.activeFormat.formatDescription)
|
||||||
recordingSession.initializeVideoWriter(withSettings: videoSettings,
|
recordingSession.initializeVideoWriter(withSettings: videoSettings,
|
||||||
|
@ -114,6 +114,32 @@ 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'!`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// HDR (10-bit) instead of SDR (8-bit) takes up 20% more pixels
|
||||||
|
if (this.props.hdr) result = result * 1.2
|
||||||
|
// Return overall result
|
||||||
|
return result * factor
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new video recording.
|
* Start a new video recording.
|
||||||
*
|
*
|
||||||
@ -135,6 +161,9 @@ export class Camera extends React.PureComponent<CameraProps> {
|
|||||||
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
|
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
|
||||||
throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!')
|
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 onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
|
const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
|
||||||
if (error != null) return onRecordingError(error)
|
if (error != null) return onRecordingError(error)
|
||||||
if (video != null) return onRecordingFinished(video)
|
if (video != null) return onRecordingFinished(video)
|
||||||
|
@ -46,17 +46,17 @@ export interface CameraProps extends ViewProps {
|
|||||||
|
|
||||||
//#region Use-cases
|
//#region Use-cases
|
||||||
/**
|
/**
|
||||||
* Enables **photo capture** with the `takePhoto` function (see ["Taking Photos"](https://react-native-vision-camera.com/docs/guides/capturing#taking-photos))
|
* Enables **photo capture** with the `takePhoto` function (see ["Taking Photos"](https://react-native-vision-camera.com/docs/guides/taking-photos))
|
||||||
*/
|
*/
|
||||||
photo?: boolean
|
photo?: boolean
|
||||||
/**
|
/**
|
||||||
* Enables **video capture** with the `startRecording` function (see ["Recording Videos"](https://react-native-vision-camera.com/docs/guides/capturing/#recording-videos))
|
* Enables **video capture** with the `startRecording` function (see ["Recording Videos"](https://react-native-vision-camera.com/docs/guides/recording-videos))
|
||||||
*
|
*
|
||||||
* Note: If both the `photo` and `video` properties are enabled at the same time and the device is running at a `hardwareLevel` of `'legacy'` or `'limited'`, VisionCamera _might_ use a lower resolution for video capture due to hardware constraints.
|
* Note: If both the `photo` and `video` properties are enabled at the same time and the device is running at a `hardwareLevel` of `'legacy'` or `'limited'`, VisionCamera _might_ use a lower resolution for video capture due to hardware constraints.
|
||||||
*/
|
*/
|
||||||
video?: boolean
|
video?: boolean
|
||||||
/**
|
/**
|
||||||
* Enables **audio capture** for video recordings (see ["Recording Videos"](https://react-native-vision-camera.com/docs/guides/capturing/#recording-videos))
|
* Enables **audio capture** for video recordings (see ["Recording Videos"](https://react-native-vision-camera.com/docs/guides/recording-videos))
|
||||||
*/
|
*/
|
||||||
audio?: boolean
|
audio?: boolean
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,17 @@ export interface RecordVideoOptions {
|
|||||||
* - `h265`: The HEVC (High-Efficient-Video-Codec) for higher efficient video recordings.
|
* - `h265`: The HEVC (High-Efficient-Video-Codec) for higher efficient video recordings.
|
||||||
*/
|
*/
|
||||||
videoCodec?: 'h264' | 'h265'
|
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 codec.
|
||||||
|
*
|
||||||
|
* By default, it will be calculated using those factors.
|
||||||
|
* For example, at 1080p, 30 FPS, H.264 codec, without HDR it will result to 10 Mbps.
|
||||||
|
*
|
||||||
|
* @default 'normal'
|
||||||
|
*/
|
||||||
|
videoBitRate?: 'low' | 'normal' | 'high' | number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user