3 Commits

6 changed files with 436 additions and 433 deletions

View File

@@ -13,7 +13,7 @@ import com.mrousavy.camera.types.RecordVideoOptions
import com.mrousavy.camera.utils.makeErrorMap import com.mrousavy.camera.utils.makeErrorMap
import java.util.* import java.util.*
suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) { suspend fun CameraView.startRecording(options: RecordVideoOptions, filePath: String, onRecordCallback: Callback) {
// check audio permission // check audio permission
if (audio == true) { if (audio == true) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
@@ -33,7 +33,7 @@ suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallb
val errorMap = makeErrorMap(error.code, error.message) val errorMap = makeErrorMap(error.code, error.message)
onRecordCallback(null, errorMap) onRecordCallback(null, errorMap)
} }
cameraSession.startRecording(audio == true, options, callback, onError) cameraSession.startRecording(audio == true, options, filePath, callback, onError)
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")

View File

@@ -95,12 +95,12 @@ 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 // TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
@ReactMethod @ReactMethod
fun startRecording(viewTag: Int, jsOptions: ReadableMap, onRecordCallback: Callback) { fun startRecording(viewTag: Int, jsOptions: ReadableMap, filePath: String, onRecordCallback: Callback) {
coroutineScope.launch { coroutineScope.launch {
val view = findCameraView(viewTag) val view = findCameraView(viewTag)
try { try {
val options = RecordVideoOptions(jsOptions) val options = RecordVideoOptions(jsOptions)
view.startRecording(options, onRecordCallback) view.startRecording(options, filePath, onRecordCallback)
} catch (error: CameraError) { } catch (error: CameraError) {
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error) val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
onRecordCallback(null, map) onRecordCallback(null, map)

View File

@@ -621,6 +621,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
suspend fun startRecording( suspend fun startRecording(
enableAudio: Boolean, enableAudio: Boolean,
options: RecordVideoOptions, options: RecordVideoOptions,
filePath: String,
callback: (video: RecordingSession.Video) -> Unit, callback: (video: RecordingSession.Video) -> Unit,
onError: (error: CameraError) -> Unit onError: (error: CameraError) -> Unit
) { ) {
@@ -640,6 +641,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
videoOutput.enableHdr, videoOutput.enableHdr,
orientation, orientation,
options, options,
filePath,
callback, callback,
onError, onError,
this.callback, this.callback,

View File

@@ -12,26 +12,31 @@ import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.RecordVideoOptions import com.mrousavy.camera.types.RecordVideoOptions
import java.io.File import java.io.File
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.math.ceil
class ChunkedRecordingManager(private val encoder: MediaCodec, private val outputDirectory: File, private val orientationHint: Int, private val iFrameInterval: Int, private val callbacks: CameraSession.Callback) : class ChunkedRecordingManager(private val encoder: MediaCodec, private val outputDirectory: File, private val orientationHint: Int, private val iFrameInterval: Float, private val callbacks: CameraSession.Callback) :
MediaCodec.Callback() { MediaCodec.Callback() {
companion object { companion object {
private const val TAG = "ChunkedRecorder" private const val TAG = "ChunkedRecorder"
private fun roundIntervalLengthUp(fps: Float, interval: Float): Float {
return ceil(fps * interval) / fps
}
fun fromParams( fun fromParams(
callbacks: CameraSession.Callback, callbacks: CameraSession.Callback,
size: Size, size: Size,
enableAudio: Boolean, enableAudio: Boolean,
fps: Int? = null, fps: Float = 30.0f,
cameraOrientation: Orientation, cameraOrientation: Orientation,
bitRate: Int, bitRate: Int,
options: RecordVideoOptions, options: RecordVideoOptions,
outputDirectory: File, outputDirectory: File,
iFrameInterval: Int = 5 iFrameInterval: Float = 5.0f
): ChunkedRecordingManager { ): ChunkedRecordingManager {
val mimeType = options.videoCodec.toMimeType() val mimeType = options.videoCodec.toMimeType()
val cameraOrientationDegrees = cameraOrientation.toDegrees() val cameraOrientationDegrees = cameraOrientation.toDegrees()
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees(); val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees()
val (width, height) = if (cameraOrientation.isLandscape()) { val (width, height) = if (cameraOrientation.isLandscape()) {
size.height to size.width size.height to size.width
} else { } else {
@@ -42,25 +47,25 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
val codec = MediaCodec.createEncoderByType(mimeType) val codec = MediaCodec.createEncoderByType(mimeType)
val roundedInterval = roundIntervalLengthUp(fps, iFrameInterval)
// Set some properties. Failing to specify some of these can cause the MediaCodec // Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception. // configure() call to throw an unhelpful exception.
format.setInteger( format.setInteger(
MediaFormat.KEY_COLOR_FORMAT, MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
) )
fps?.apply { format.setFloat(MediaFormat.KEY_FRAME_RATE, fps)
format.setInteger(MediaFormat.KEY_FRAME_RATE, this)
}
// TODO: Pull this out into configuration // TODO: Pull this out into configuration
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval) format.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, roundedInterval)
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate) format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees, recordingOrientation: $recordingOrientationDegrees") Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees, recordingOrientation: $recordingOrientationDegrees, Set fps: $fps")
// Create a MediaCodec encoder, and configure it with our format. Get a Surface // Create a MediaCodec encoder, and configure it with our format. Get a Surface
// we can use for input and wrap it with a class that handles the EGL work. // we can use for input and wrap it with a class that handles the EGL work.
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
return ChunkedRecordingManager( return ChunkedRecordingManager(
codec, outputDirectory, recordingOrientationDegrees, iFrameInterval, callbacks codec, outputDirectory, recordingOrientationDegrees, roundedInterval, callbacks
) )
} }
} }
@@ -69,7 +74,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
private var currentFrameNumber: Int = 0 private var currentFrameNumber: Int = 0
private var chunkIndex = -1 private var chunkIndex = -1
private var encodedFormat: MediaFormat? = null private var encodedFormat: MediaFormat? = null
private var recording = false; private var recording = false
private val targetDurationUs = iFrameInterval * 1000000 private val targetDurationUs = iFrameInterval * 1000000
@@ -83,15 +88,15 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
} }
// Muxer specific // Muxer specific
private class MuxerContext(val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int, startTimeUs: Long, encodedFormat: MediaFormat) { private class MuxerContext(
val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int,
val startTimeUs: Long, encodedFormat: MediaFormat
) {
val videoTrack: Int = muxer.addTrack(encodedFormat) val videoTrack: Int = muxer.addTrack(encodedFormat)
val startTimeUs: Long = startTimeUs
init { init {
muxer.start() muxer.start()
} }
fun finish() { fun finish() {
muxer.stop() muxer.stop()
muxer.release() muxer.release()
@@ -146,7 +151,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
} }
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, bufferInfo: MediaCodec.BufferInfo) { override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, bufferInfo: BufferInfo) {
synchronized(this) { synchronized(this) {
if (!recording) { if (!recording) {
return return

View File

@@ -23,6 +23,7 @@ class RecordingSession(
private val hdr: Boolean = false, private val hdr: Boolean = false,
private val cameraOrientation: Orientation, private val cameraOrientation: Orientation,
private val options: RecordVideoOptions, private val options: RecordVideoOptions,
private val filePath: String,
private val callback: (video: Video) -> Unit, private val callback: (video: Video) -> Unit,
private val onError: (error: CameraError) -> Unit, private val onError: (error: CameraError) -> Unit,
private val allCallbacks: CameraSession.Callback, private val allCallbacks: CameraSession.Callback,
@@ -37,19 +38,14 @@ class RecordingSession(
data class Video(val path: String, val durationMs: Long, val size: Size) data class Video(val path: String, val durationMs: Long, val size: Size)
private val outputPath = run { private val outputPath: File = File(filePath)
val videoDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
val videoFileName = "VID_${sdf.format(Date())}"
File(videoDir!!, videoFileName)
}
private val bitRate = getBitRate() private val bitRate = getBitRate()
private val recorder = ChunkedRecordingManager.fromParams( private val recorder = ChunkedRecordingManager.fromParams(
allCallbacks, allCallbacks,
size, size,
enableAudio, enableAudio,
fps, (fps ?: 30).toFloat(),
cameraOrientation, cameraOrientation,
bitRate, bitRate,
options, options,

View File

@@ -173,7 +173,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
* }, 5000) * }, 5000)
* ``` * ```
*/ */
public startRecording(options: RecordVideoOptions): void { public startRecording(options: RecordVideoOptions, filePath: string): void {
const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options
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!')
@@ -207,7 +207,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
} }
try { try {
// TODO: Use TurboModules to make this awaitable. // TODO: Use TurboModules to make this awaitable.
CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback) CameraModule.startRecording(this.handle, nativeOptions, filePath, onRecordCallback)
} catch (e) { } catch (e) {
throw tryParseNativeCameraError(e) throw tryParseNativeCameraError(e)
} }