Compare commits
7 Commits
3d4221fdde
...
loewy/impr
| Author | SHA1 | Date | |
|---|---|---|---|
| b269e9c493 | |||
| 5fe7f35127 | |||
| 61863149c0 | |||
| 09b50938d2 | |||
| a158ed8350 | |||
|
|
e7b295546a | ||
|
|
d87ed8ced2 |
@@ -426,16 +426,18 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
|
|
||||||
val fps = configuration?.fps ?: 30
|
val fps = configuration?.fps ?: 30
|
||||||
|
|
||||||
// Get actual device rotation from WindowManager instead of relying on
|
// Get actual device rotation from WindowManager since the React Native orientation hook
|
||||||
// the orientation prop from React, which may not update on Android when
|
// doesn't update when rotating between landscape-left and landscape-right on Android.
|
||||||
// rotating between landscape-left and landscape-right.
|
// Map device rotation to the correct orientationHint for video recording:
|
||||||
|
// - Counter-clockwise (ROTATION_90) → 270° hint
|
||||||
|
// - Clockwise (ROTATION_270) → 90° hint
|
||||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
val deviceRotation = windowManager.defaultDisplay.rotation
|
val deviceRotation = windowManager.defaultDisplay.rotation
|
||||||
val actualOrientation = when (deviceRotation) {
|
val recordingOrientation = when (deviceRotation) {
|
||||||
Surface.ROTATION_0 -> Orientation.PORTRAIT
|
Surface.ROTATION_0 -> Orientation.PORTRAIT
|
||||||
Surface.ROTATION_90 -> Orientation.LANDSCAPE_LEFT
|
Surface.ROTATION_90 -> Orientation.LANDSCAPE_RIGHT
|
||||||
Surface.ROTATION_180 -> Orientation.PORTRAIT_UPSIDE_DOWN
|
Surface.ROTATION_180 -> Orientation.PORTRAIT_UPSIDE_DOWN
|
||||||
Surface.ROTATION_270 -> Orientation.LANDSCAPE_RIGHT
|
Surface.ROTATION_270 -> Orientation.LANDSCAPE_LEFT
|
||||||
else -> Orientation.PORTRAIT
|
else -> Orientation.PORTRAIT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
muxerContext?.finish()
|
muxerContext?.finish()
|
||||||
chunkIndex++
|
chunkIndex++
|
||||||
|
|
||||||
|
val format = this.encodedFormat
|
||||||
|
if (format == null) {
|
||||||
|
Log.e(TAG, "Cannot create muxer: encodedFormat is null (onOutputFormatChanged not called yet)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val newFileName = "$chunkIndex.mp4"
|
val newFileName = "$chunkIndex.mp4"
|
||||||
val newOutputFile = File(this.outputDirectory, newFileName)
|
val newOutputFile = File(this.outputDirectory, newFileName)
|
||||||
Log.i(TAG, "Creating new muxer for file: $newFileName")
|
Log.i(TAG, "Creating new muxer for file: $newFileName")
|
||||||
@@ -114,7 +120,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
)
|
)
|
||||||
muxer.setOrientationHint(orientationHint)
|
muxer.setOrientationHint(orientationHint)
|
||||||
muxerContext = MuxerContext(
|
muxerContext = MuxerContext(
|
||||||
muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!!, this.callbacks
|
muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, format, this.callbacks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +129,8 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun chunkLengthUs(bufferInfo: BufferInfo): Long {
|
private fun chunkLengthUs(bufferInfo: BufferInfo): Long {
|
||||||
return bufferInfo.presentationTimeUs - muxerContext!!.startTimeUs
|
val context = muxerContext ?: return 0L
|
||||||
|
return bufferInfo.presentationTimeUs - context.startTimeUs
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -155,7 +162,13 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
|
|||||||
if (muxerContext == null || (atKeyframe(bufferInfo) && chunkLengthUs(bufferInfo) >= targetDurationUs)) {
|
if (muxerContext == null || (atKeyframe(bufferInfo) && chunkLengthUs(bufferInfo) >= targetDurationUs)) {
|
||||||
this.createNextMuxer(bufferInfo)
|
this.createNextMuxer(bufferInfo)
|
||||||
}
|
}
|
||||||
muxerContext!!.muxer.writeSampleData(muxerContext!!.videoTrack, encodedData, bufferInfo)
|
val context = muxerContext
|
||||||
|
if (context == null) {
|
||||||
|
Log.e(TAG, "Cannot write sample data: muxerContext is null")
|
||||||
|
encoder.releaseOutputBuffer(index, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
context.muxer.writeSampleData(context.videoTrack, encodedData, bufferInfo)
|
||||||
encoder.releaseOutputBuffer(index, false)
|
encoder.releaseOutputBuffer(index, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ inline fun withPromise(promise: Promise, closure: () -> Any?) {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
val error = if (e is CameraError) e else UnknownCameraError(e)
|
val error = if (e is CameraError) e else UnknownCameraError(e)
|
||||||
promise.reject("${error.domain}/${error.id}", error.message, error.cause)
|
promise.reject("${error.domain}/${error.id}", error.message ?: "Unknown error", error.cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,28 +56,32 @@ extension ChunkedRecorder: AVAssetWriterDelegate {
|
|||||||
|
|
||||||
private func saveInitSegment(_ data: Data) {
|
private func saveInitSegment(_ data: Data) {
|
||||||
let url = outputURL.appendingPathComponent("init.mp4")
|
let url = outputURL.appendingPathComponent("init.mp4")
|
||||||
save(data: data, url: url)
|
if save(data: data, url: url) {
|
||||||
onChunkReady(url: url, type: .initialization)
|
onChunkReady(url: url, type: .initialization)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveSegment(_ data: Data, report: AVAssetSegmentReport?) {
|
private func saveSegment(_ data: Data, report: AVAssetSegmentReport?) {
|
||||||
let name = "\(chunkIndex).mp4"
|
let name = "\(chunkIndex).mp4"
|
||||||
let url = outputURL.appendingPathComponent(name)
|
let url = outputURL.appendingPathComponent(name)
|
||||||
save(data: data, url: url)
|
if save(data: data, url: url) {
|
||||||
let duration = report?
|
let duration = report?
|
||||||
.trackReports
|
.trackReports
|
||||||
.filter { $0.mediaType == .video }
|
.filter { $0.mediaType == .video }
|
||||||
.first?
|
.first?
|
||||||
.duration
|
.duration
|
||||||
onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration))
|
onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration))
|
||||||
chunkIndex += 1
|
chunkIndex += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save(data: Data, url: URL) {
|
private func save(data: Data, url: URL) -> Bool {
|
||||||
do {
|
do {
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)")
|
ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user