3 Commits

Author SHA1 Message Date
0329e7976d Account for orientation in PreviewView 2024-07-29 00:02:24 -06:00
7c162fecb1 Remove trailing whitespace 2024-07-28 16:37:20 -06:00
b28a152471 Fix last segment issue 2024-07-24 21:00:27 -06:00
7 changed files with 41 additions and 25 deletions

View File

@@ -72,6 +72,10 @@ class CameraView(context: Context) :
var zoom: Float = 1f // in "factor" var zoom: Float = 1f // in "factor"
var exposure: Double = 1.0 var exposure: Double = 1.0
var orientation: Orientation = Orientation.PORTRAIT var orientation: Orientation = Orientation.PORTRAIT
set(value) {
field = value
previewView.orientation = value
}
var enableZoomGesture = false var enableZoomGesture = false
set(value) { set(value) {
field = value field = value

View File

@@ -10,6 +10,7 @@ import com.mrousavy.camera.types.CodeScannerOptions
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.PixelFormat import com.mrousavy.camera.types.PixelFormat
import com.mrousavy.camera.types.ResizeMode import com.mrousavy.camera.types.ResizeMode
import android.util.Log
import com.mrousavy.camera.types.Torch import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode import com.mrousavy.camera.types.VideoStabilizationMode
@@ -182,6 +183,7 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
fun setOrientation(view: CameraView, orientation: String?) { fun setOrientation(view: CameraView, orientation: String?) {
if (orientation != null) { if (orientation != null) {
val newMode = Orientation.fromUnionValue(orientation) val newMode = Orientation.fromUnionValue(orientation)
Log.i(TAG, "Orientation set to: $newMode")
view.orientation = newMode view.orientation = newMode
} else { } else {
view.orientation = Orientation.PORTRAIT view.orientation = Orientation.PORTRAIT

View File

@@ -312,7 +312,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
enableHdr enableHdr
) )
outputs.add(output) outputs.add(output)
// Size is usually landscape, so we flip it here
previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation) previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation)
} }

View File

@@ -83,7 +83,7 @@ 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, startTimeUs: Long, encodedFormat: MediaFormat, val callbacks: CameraSession.Callback,) {
val videoTrack: Int = muxer.addTrack(encodedFormat) val videoTrack: Int = muxer.addTrack(encodedFormat)
val startTimeUs: Long = startTimeUs val startTimeUs: Long = startTimeUs
@@ -95,16 +95,14 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
fun finish() { fun finish() {
muxer.stop() muxer.stop()
muxer.release() muxer.release()
callbacks.onVideoChunkReady(filepath, chunkIndex)
} }
} }
private var muxerContext: MuxerContext? = null private var muxerContext: MuxerContext? = null
private fun createNextMuxer(bufferInfo: BufferInfo) { private fun createNextMuxer(bufferInfo: BufferInfo) {
muxerContext?.let { muxerContext?.finish()
it.finish()
this.callbacks.onVideoChunkReady(it.filepath, it.chunkIndex)
}
chunkIndex++ chunkIndex++
val newFileName = "$chunkIndex.mp4" val newFileName = "$chunkIndex.mp4"
@@ -116,7 +114,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!! muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!!, this.callbacks
) )
} }

View File

@@ -36,6 +36,13 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
updateLayout() updateLayout()
} }
} }
var orientation: Orientation = Orientation.PORTRAIT
set(value) {
if (field != value) {
Log.i(TAG, "View Orientation changed: $field -> $value")
field = value
}
}
private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT
set(value) { set(value) {
if (field != value) { if (field != value) {
@@ -101,6 +108,11 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
} }
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
var contentSize = contentSize
// Swap dimensions if orientation is landscape
if (orientation.isLandscape()) {
contentSize = Size(contentSize.height, contentSize.width)
}
val contentAspectRatio = contentSize.width.toDouble() / contentSize.height val contentAspectRatio = contentSize.width.toDouble() / contentSize.height
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
if (!(contentAspectRatio > 0 && containerAspectRatio > 0)) { if (!(contentAspectRatio > 0 && containerAspectRatio > 0)) {
@@ -128,11 +140,11 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) val measuredViewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
val surfaceSize = size.rotatedBy(inputOrientation) val surfaceSize = size.rotatedBy(inputOrientation)
val fittedSize = getSize(surfaceSize, viewSize, resizeMode) val fittedSize = getSize(surfaceSize, measuredViewSize, resizeMode)
Log.i(TAG, "PreviewView is $viewSize, rendering $surfaceSize content ($inputOrientation). Resizing to: $fittedSize ($resizeMode)") Log.i(TAG, "PreviewView is $measuredViewSize rendering $surfaceSize orientation ($orientation). Resizing to: $fittedSize ($resizeMode)")
setMeasuredDimension(fittedSize.width, fittedSize.height) setMeasuredDimension(fittedSize.width, fittedSize.height)
} }

View File

@@ -11,7 +11,7 @@ import AVFoundation
class ChunkedRecorder: NSObject { class ChunkedRecorder: NSObject {
enum ChunkType { enum ChunkType {
case initialization case initialization
case data(index: UInt64, duration: CMTime?) case data(index: UInt64, duration: CMTime?)
@@ -21,12 +21,12 @@ class ChunkedRecorder: NSObject {
let url: URL let url: URL
let type: ChunkType let type: ChunkType
} }
let outputURL: URL let outputURL: URL
let onChunkReady: ((Chunk) -> Void) let onChunkReady: ((Chunk) -> Void)
private var chunkIndex: UInt64 = 0 private var chunkIndex: UInt64 = 0
init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws { init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
self.outputURL = outputURL self.outputURL = outputURL
self.onChunkReady = onChunkReady self.onChunkReady = onChunkReady
@@ -34,16 +34,16 @@ class ChunkedRecorder: NSObject {
throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil) throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil)
} }
} }
} }
extension ChunkedRecorder: AVAssetWriterDelegate { extension ChunkedRecorder: AVAssetWriterDelegate {
func assetWriter(_ writer: AVAssetWriter, func assetWriter(_ writer: AVAssetWriter,
didOutputSegmentData segmentData: Data, didOutputSegmentData segmentData: Data,
segmentType: AVAssetSegmentType, segmentType: AVAssetSegmentType,
segmentReport: AVAssetSegmentReport?) { segmentReport: AVAssetSegmentReport?) {
switch segmentType { switch segmentType {
case .initialization: case .initialization:
saveInitSegment(segmentData) saveInitSegment(segmentData)
@@ -53,13 +53,13 @@ extension ChunkedRecorder: AVAssetWriterDelegate {
fatalError("Unknown AVAssetSegmentType!") fatalError("Unknown AVAssetSegmentType!")
} }
} }
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) 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)
@@ -72,7 +72,7 @@ extension ChunkedRecorder: AVAssetWriterDelegate {
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) {
do { do {
try data.write(to: url) try data.write(to: url)
@@ -80,9 +80,9 @@ extension ChunkedRecorder: AVAssetWriterDelegate {
ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)") ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)")
} }
} }
private func onChunkReady(url: URL, type: ChunkType) { private func onChunkReady(url: URL, type: ChunkType) {
onChunkReady(Chunk(url: url, type: type)) onChunkReady(Chunk(url: url, type: type))
} }
} }

View File

@@ -84,7 +84,7 @@ class RecordingSession {
assetWriter.shouldOptimizeForNetworkUse = false assetWriter.shouldOptimizeForNetworkUse = false
assetWriter.outputFileTypeProfile = .mpeg4AppleHLS assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 6, preferredTimescale: 1) assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 6, preferredTimescale: 1)
/* /*
Apple HLS fMP4 does not have an Edit List Box ('elst') in an initialization segment to remove Apple HLS fMP4 does not have an Edit List Box ('elst') in an initialization segment to remove
audio priming duration which advanced audio formats like AAC have, since the sample tables audio priming duration which advanced audio formats like AAC have, since the sample tables
@@ -95,7 +95,7 @@ class RecordingSession {
*/ */
let startTimeOffset = CMTime(value: 10, timescale: 1) let startTimeOffset = CMTime(value: 10, timescale: 1)
assetWriter.initialSegmentStartTime = startTimeOffset assetWriter.initialSegmentStartTime = startTimeOffset
assetWriter.delegate = recorder assetWriter.delegate = recorder
} catch let error as NSError { } catch let error as NSError {
throw CameraError.capture(.createRecorderError(message: error.description)) throw CameraError.capture(.createRecorderError(message: error.description))