Compare commits
3 Commits
19bf300bbe
...
ivan/try-t
Author | SHA1 | Date | |
---|---|---|---|
1ac1b045fe | |||
952e4a93e1 | |||
|
489171f6f3 |
@@ -13,7 +13,7 @@ import com.mrousavy.camera.types.RecordVideoOptions
|
||||
import com.mrousavy.camera.utils.makeErrorMap
|
||||
import java.util.*
|
||||
|
||||
suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) {
|
||||
suspend fun CameraView.startRecording(options: RecordVideoOptions, filePath: String, onRecordCallback: Callback) {
|
||||
// check audio permission
|
||||
if (audio == true) {
|
||||
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)
|
||||
onRecordCallback(null, errorMap)
|
||||
}
|
||||
cameraSession.startRecording(audio == true, options, callback, onError)
|
||||
cameraSession.startRecording(audio == true, options, filePath, callback, onError)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
|
@@ -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
|
||||
@ReactMethod
|
||||
fun startRecording(viewTag: Int, jsOptions: ReadableMap, onRecordCallback: Callback) {
|
||||
fun startRecording(viewTag: Int, jsOptions: ReadableMap, filePath: String, onRecordCallback: Callback) {
|
||||
coroutineScope.launch {
|
||||
val view = findCameraView(viewTag)
|
||||
try {
|
||||
val options = RecordVideoOptions(jsOptions)
|
||||
view.startRecording(options, onRecordCallback)
|
||||
view.startRecording(options, filePath, onRecordCallback)
|
||||
} catch (error: CameraError) {
|
||||
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
|
||||
onRecordCallback(null, map)
|
||||
|
@@ -621,6 +621,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
||||
suspend fun startRecording(
|
||||
enableAudio: Boolean,
|
||||
options: RecordVideoOptions,
|
||||
filePath: String,
|
||||
callback: (video: RecordingSession.Video) -> Unit,
|
||||
onError: (error: CameraError) -> Unit
|
||||
) {
|
||||
@@ -640,6 +641,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
||||
videoOutput.enableHdr,
|
||||
orientation,
|
||||
options,
|
||||
filePath,
|
||||
callback,
|
||||
onError,
|
||||
this.callback,
|
||||
|
@@ -12,26 +12,31 @@ import com.mrousavy.camera.types.Orientation
|
||||
import com.mrousavy.camera.types.RecordVideoOptions
|
||||
import java.io.File
|
||||
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() {
|
||||
companion object {
|
||||
private const val TAG = "ChunkedRecorder"
|
||||
|
||||
private fun roundIntervalLengthUp(fps: Float, interval: Float): Float {
|
||||
return ceil(fps * interval) / fps
|
||||
}
|
||||
|
||||
fun fromParams(
|
||||
callbacks: CameraSession.Callback,
|
||||
size: Size,
|
||||
enableAudio: Boolean,
|
||||
fps: Int? = null,
|
||||
fps: Float = 30.0f,
|
||||
cameraOrientation: Orientation,
|
||||
bitRate: Int,
|
||||
options: RecordVideoOptions,
|
||||
outputDirectory: File,
|
||||
iFrameInterval: Int = 5
|
||||
iFrameInterval: Float = 5.0f
|
||||
): ChunkedRecordingManager {
|
||||
val mimeType = options.videoCodec.toMimeType()
|
||||
val cameraOrientationDegrees = cameraOrientation.toDegrees()
|
||||
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees();
|
||||
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees()
|
||||
val (width, height) = if (cameraOrientation.isLandscape()) {
|
||||
size.height to size.width
|
||||
} else {
|
||||
@@ -42,25 +47,25 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
||||
|
||||
val codec = MediaCodec.createEncoderByType(mimeType)
|
||||
|
||||
val roundedInterval = roundIntervalLengthUp(fps, iFrameInterval)
|
||||
|
||||
// Set some properties. Failing to specify some of these can cause the MediaCodec
|
||||
// configure() call to throw an unhelpful exception.
|
||||
format.setInteger(
|
||||
MediaFormat.KEY_COLOR_FORMAT,
|
||||
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||
)
|
||||
fps?.apply {
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, this)
|
||||
}
|
||||
format.setFloat(MediaFormat.KEY_FRAME_RATE, fps)
|
||||
// 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)
|
||||
|
||||
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
|
||||
// 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)
|
||||
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 chunkIndex = -1
|
||||
private var encodedFormat: MediaFormat? = null
|
||||
private var recording = false;
|
||||
private var recording = false
|
||||
|
||||
private val targetDurationUs = iFrameInterval * 1000000
|
||||
|
||||
@@ -83,15 +88,15 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
||||
}
|
||||
|
||||
// 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 startTimeUs: Long = startTimeUs
|
||||
|
||||
init {
|
||||
muxer.start()
|
||||
}
|
||||
|
||||
|
||||
fun finish() {
|
||||
muxer.stop()
|
||||
muxer.release()
|
||||
@@ -146,7 +151,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
||||
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) {
|
||||
if (!recording) {
|
||||
return
|
||||
|
@@ -23,6 +23,7 @@ class RecordingSession(
|
||||
private val hdr: Boolean = false,
|
||||
private val cameraOrientation: Orientation,
|
||||
private val options: RecordVideoOptions,
|
||||
private val filePath: String,
|
||||
private val callback: (video: Video) -> Unit,
|
||||
private val onError: (error: CameraError) -> Unit,
|
||||
private val allCallbacks: CameraSession.Callback,
|
||||
@@ -37,19 +38,14 @@ class RecordingSession(
|
||||
|
||||
data class Video(val path: String, val durationMs: Long, val size: Size)
|
||||
|
||||
private val outputPath = run {
|
||||
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 outputPath: File = File(filePath)
|
||||
|
||||
private val bitRate = getBitRate()
|
||||
private val recorder = ChunkedRecordingManager.fromParams(
|
||||
allCallbacks,
|
||||
size,
|
||||
enableAudio,
|
||||
fps,
|
||||
(fps ?: 30).toFloat(),
|
||||
cameraOrientation,
|
||||
bitRate,
|
||||
options,
|
||||
|
@@ -173,7 +173,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
|
||||
* }, 5000)
|
||||
* ```
|
||||
*/
|
||||
public startRecording(options: RecordVideoOptions): void {
|
||||
public startRecording(options: RecordVideoOptions, filePath: string): void {
|
||||
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!')
|
||||
@@ -207,7 +207,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
|
||||
}
|
||||
try {
|
||||
// TODO: Use TurboModules to make this awaitable.
|
||||
CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback)
|
||||
CameraModule.startRecording(this.handle, nativeOptions, filePath, onRecordCallback)
|
||||
} catch (e) {
|
||||
throw tryParseNativeCameraError(e)
|
||||
}
|
||||
|
Reference in New Issue
Block a user