删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。
This commit is contained in:
@@ -42,12 +42,6 @@ kotlin {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val cameraxVersion = "1.4.1"
|
||||
implementation("androidx.camera:camera-core:$cameraxVersion")
|
||||
implementation("androidx.camera:camera-camera2:$cameraxVersion")
|
||||
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
|
||||
implementation("androidx.camera:camera-video:$cameraxVersion")
|
||||
implementation("androidx.camera:camera-view:$cameraxVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-service:2.8.7")
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import android.provider.Settings
|
||||
import androidx.camera.view.PreviewView
|
||||
import com.dronex.rec.recording.RecordingPlatformHandler
|
||||
import com.dronex.rec.recording.RecordingPreviewFactory
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -18,15 +16,8 @@ class MainActivity : FlutterActivity() {
|
||||
private var platformHandler: RecordingPlatformHandler? = null
|
||||
private var platformInfoChannel: MethodChannel? = null
|
||||
|
||||
var recordingPreviewView: PreviewView? = null
|
||||
private set
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.platformViewsController.registry.registerViewFactory(
|
||||
"recording-camera-preview",
|
||||
RecordingPreviewFactory(this),
|
||||
)
|
||||
|
||||
platformInfoChannel =
|
||||
MethodChannel(
|
||||
@@ -51,16 +42,6 @@ class MainActivity : FlutterActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
fun attachRecordingPreview(previewView: PreviewView) {
|
||||
recordingPreviewView = previewView
|
||||
}
|
||||
|
||||
fun detachRecordingPreview(previewView: PreviewView? = null) {
|
||||
if (previewView == null || recordingPreviewView === previewView) {
|
||||
recordingPreviewView = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
platformInfoChannel?.setMethodCallHandler(null)
|
||||
platformInfoChannel = null
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.camera.video.FileOutputOptions
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object RecordingOutputFactory {
|
||||
private const val RELATIVE_PATH = "Movies/飞行极控录像工作台"
|
||||
private const val MIME_TYPE = "video/mp4"
|
||||
|
||||
fun buildSegmentOutputOptions(segmentFile: File): FileOutputOptions {
|
||||
return FileOutputOptions.Builder(segmentFile).build()
|
||||
}
|
||||
|
||||
fun createSegmentFile(
|
||||
context: Context,
|
||||
displayName: String?,
|
||||
index: Int,
|
||||
): File {
|
||||
val directory = File(context.cacheDir, "recording_segments")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
val baseName = resolveFileName(displayName).removeSuffix(".mp4")
|
||||
return File(directory, "${baseName}_${System.currentTimeMillis()}_part$index.mp4")
|
||||
}
|
||||
|
||||
fun createMergeFile(context: Context, displayName: String?): File {
|
||||
val directory = File(context.cacheDir, "recording_segments")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
val baseName = resolveFileName(displayName).removeSuffix(".mp4")
|
||||
return File(directory, "${baseName}_${System.currentTimeMillis()}_merged.mp4")
|
||||
}
|
||||
|
||||
fun publishToMediaStore(
|
||||
context: Context,
|
||||
displayName: String?,
|
||||
sourceFile: File,
|
||||
): String? {
|
||||
val fileName = resolveFileName(displayName)
|
||||
val contentValues =
|
||||
ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, MIME_TYPE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.Video.Media.RELATIVE_PATH, RELATIVE_PATH)
|
||||
put(MediaStore.Video.Media.IS_PENDING, 1)
|
||||
}
|
||||
}
|
||||
|
||||
val resolver = context.contentResolver
|
||||
val uri =
|
||||
resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
?: return null
|
||||
try {
|
||||
val outputStream =
|
||||
resolver.openOutputStream(uri)
|
||||
?: throw IllegalStateException("Cannot open MediaStore output stream")
|
||||
outputStream.use { output ->
|
||||
FileInputStream(sourceFile).use { input -> input.copyTo(output) }
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val publishedValues =
|
||||
ContentValues().apply { put(MediaStore.Video.Media.IS_PENDING, 0) }
|
||||
resolver.update(uri, publishedValues, null, null)
|
||||
}
|
||||
return uri.toString()
|
||||
} catch (error: Exception) {
|
||||
resolver.delete(uri, null, null)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
fun publishPartToMediaStore(
|
||||
context: Context,
|
||||
displayName: String?,
|
||||
sourceFile: File,
|
||||
partIndex: Int,
|
||||
): String? {
|
||||
val resolvedName = resolveFileName(displayName)
|
||||
val partName = resolvedName.replace(".mp4", "_part$partIndex.mp4")
|
||||
return publishToMediaStore(context, partName, sourceFile)
|
||||
}
|
||||
|
||||
fun resolveFileName(displayName: String?): String {
|
||||
val trimmed = displayName?.trim().orEmpty()
|
||||
if (trimmed.isNotEmpty()) {
|
||||
return if (trimmed.lowercase(Locale.US).endsWith(".mp4")) {
|
||||
trimmed
|
||||
} else {
|
||||
"$trimmed.mp4"
|
||||
}
|
||||
}
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
return "REC_$timestamp.mp4"
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,30 @@
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.dronex.rec.AppConstants
|
||||
import com.dronex.rec.MainActivity
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class RecordingPlatformHandler(
|
||||
private val activity: MainActivity,
|
||||
messenger: BinaryMessenger,
|
||||
) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
|
||||
) : MethodChannel.MethodCallHandler {
|
||||
private val methodChannel = MethodChannel(messenger, AppConstants.RECORDING_METHOD_CHANNEL)
|
||||
private val eventChannel = EventChannel(messenger, AppConstants.RECORDING_EVENT_CHANNEL)
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var elapsedTicker: Runnable? = null
|
||||
|
||||
private val controller by lazy { RecordingSession.controller(activity.applicationContext) }
|
||||
|
||||
init {
|
||||
methodChannel.setMethodCallHandler(this)
|
||||
eventChannel.setStreamHandler(this)
|
||||
controller.statusListener = { status ->
|
||||
mainHandler.post { eventSink?.success(status.toMap()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
stopElapsedTicker()
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
eventChannel.setStreamHandler(null)
|
||||
controller.statusListener = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"initializePreview" -> initializePreview(result)
|
||||
"startRecording" -> {
|
||||
val withAudio = call.argument<Boolean>("withAudio") ?: true
|
||||
val enableDnd = call.argument<Boolean>("enableDoNotDisturb") ?: true
|
||||
val displayName = call.argument<String>("displayName")
|
||||
startRecording(withAudio, enableDnd, displayName, result)
|
||||
}
|
||||
"stopRecording" -> stopRecording(result)
|
||||
"getZoomCapabilities" -> result.success(controller.zoomCapabilitiesMap())
|
||||
"setZoomRatio" -> {
|
||||
val ratio = call.argument<Double>("zoomRatio") ?: 1.0
|
||||
setZoomRatio(ratio, result)
|
||||
}
|
||||
"disposePreview" -> {
|
||||
controller.unbind()
|
||||
result.success(null)
|
||||
}
|
||||
"hasNotificationPolicyAccess" -> result.success(DoNotDisturbHelper.hasAccess(activity))
|
||||
"openNotificationPolicySettings" -> {
|
||||
DoNotDisturbHelper.openAccessSettings(activity)
|
||||
@@ -80,130 +46,11 @@ class RecordingPlatformHandler(
|
||||
setImmersiveMode(enabled)
|
||||
result.success(null)
|
||||
}
|
||||
"getStatus" -> result.success(controller.status.toMap())
|
||||
"isForegroundServiceRunning" -> result.success(RecordingForegroundService.isRunning)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializePreview(result: MethodChannel.Result) {
|
||||
val previewView = activity.recordingPreviewView
|
||||
if (previewView == null) {
|
||||
result.error("NO_PREVIEW", "Camera preview is not attached", null)
|
||||
return
|
||||
}
|
||||
|
||||
controller.bindPreview(activity, previewView) { ready ->
|
||||
mainHandler.post {
|
||||
if (ready) {
|
||||
result.success(controller.status.toMap())
|
||||
} else {
|
||||
result.error("PREVIEW_FAILED", "Failed to bind camera preview", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRecording(
|
||||
withAudio: Boolean,
|
||||
enableDnd: Boolean,
|
||||
displayName: String?,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
val previewView = activity.recordingPreviewView
|
||||
if (previewView == null) {
|
||||
result.error("NO_PREVIEW", "Camera preview is not attached", null)
|
||||
return
|
||||
}
|
||||
|
||||
RecordingSession.startForeground(activity)
|
||||
|
||||
fun beginCapture() {
|
||||
if (enableDnd && DoNotDisturbHelper.hasAccess(activity)) {
|
||||
DoNotDisturbHelper.enable(activity)
|
||||
}
|
||||
|
||||
controller.startRecording(withAudio, displayName) { started, message ->
|
||||
mainHandler.post {
|
||||
if (started) {
|
||||
startElapsedTicker()
|
||||
result.success(
|
||||
mapOf(
|
||||
"outputPath" to message,
|
||||
"status" to controller.status.toMap(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
RecordingSession.stopForeground(activity)
|
||||
DoNotDisturbHelper.disable(activity)
|
||||
result.error("START_FAILED", message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun rebindAndCapture() {
|
||||
val lifecycleOwner = RecordingForegroundService.instance ?: activity
|
||||
controller.rebindForRecording(lifecycleOwner, previewView) { ready ->
|
||||
if (ready) {
|
||||
beginCapture()
|
||||
} else {
|
||||
RecordingSession.stopForeground(activity)
|
||||
result.error("REBIND_FAILED", "Failed to bind camera for recording", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (RecordingForegroundService.instance != null) {
|
||||
rebindAndCapture()
|
||||
} else {
|
||||
mainHandler.post { rebindAndCapture() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopRecording(result: MethodChannel.Result) {
|
||||
stopElapsedTicker()
|
||||
controller.stopRecording { path ->
|
||||
RecordingSession.stopForeground(activity)
|
||||
DoNotDisturbHelper.disable(activity)
|
||||
val previewView = activity.recordingPreviewView
|
||||
if (previewView == null) {
|
||||
mainHandler.post { deliverStopResult(result, path) }
|
||||
return@stopRecording
|
||||
}
|
||||
controller.rebindForRecording(activity, previewView) { _ ->
|
||||
mainHandler.post { deliverStopResult(result, path) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setZoomRatio(ratio: Double, result: MethodChannel.Result) {
|
||||
controller.setZoomRatio(ratio) { success, capabilities, message ->
|
||||
mainHandler.post {
|
||||
if (success) {
|
||||
result.success(capabilities)
|
||||
} else {
|
||||
result.error("ZOOM_FAILED", message ?: "Failed to set camera zoom", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deliverStopResult(result: MethodChannel.Result, path: String?) {
|
||||
val fileSaved = path != null && controller.status.state != RecordingState.ERROR
|
||||
val payload =
|
||||
mutableMapOf<String, Any?>(
|
||||
"outputPath" to path,
|
||||
"status" to controller.status.toMap(),
|
||||
"fileSaved" to fileSaved,
|
||||
"segmentOutputPaths" to controller.segmentOutputPaths(),
|
||||
)
|
||||
if (!fileSaved) {
|
||||
payload["fileErrorMessage"] = controller.status.message ?: "保存到文件夹失败"
|
||||
}
|
||||
result.success(payload)
|
||||
}
|
||||
|
||||
private fun setImmersiveMode(enabled: Boolean) {
|
||||
val window = activity.window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, !enabled)
|
||||
@@ -216,41 +63,4 @@ class RecordingPlatformHandler(
|
||||
insetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startElapsedTicker() {
|
||||
stopElapsedTicker()
|
||||
elapsedTicker =
|
||||
object : Runnable {
|
||||
override fun run() {
|
||||
if (controller.status.state == RecordingState.RECORDING) {
|
||||
eventSink?.success(
|
||||
controller
|
||||
.status
|
||||
.copy(
|
||||
elapsedMillis =
|
||||
controller.elapsedMillis(),
|
||||
)
|
||||
.toMap(),
|
||||
)
|
||||
mainHandler.postDelayed(this, 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
.also { mainHandler.post(it) }
|
||||
}
|
||||
|
||||
private fun stopElapsedTicker() {
|
||||
elapsedTicker?.let { mainHandler.removeCallbacks(it) }
|
||||
elapsedTicker = null
|
||||
}
|
||||
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
eventSink = events
|
||||
events?.success(controller.status.toMap())
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
stopElapsedTicker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.camera.view.PreviewView
|
||||
import com.dronex.rec.MainActivity
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
class RecordingPreviewFactory(
|
||||
private val activity: MainActivity,
|
||||
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
|
||||
return RecordingPreviewPlatformView(activity)
|
||||
}
|
||||
}
|
||||
|
||||
class RecordingPreviewPlatformView(
|
||||
private val activity: MainActivity,
|
||||
) : PlatformView {
|
||||
val previewView: PreviewView =
|
||||
PreviewView(activity).apply {
|
||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
}
|
||||
|
||||
init {
|
||||
activity.attachRecordingPreview(previewView)
|
||||
}
|
||||
|
||||
override fun getView(): View = previewView
|
||||
|
||||
override fun dispose() {
|
||||
activity.detachRecordingPreview(previewView)
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMuxer
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object RecordingSegmentMuxer {
|
||||
fun mergeOrCopy(
|
||||
segments: List<File>,
|
||||
outputFile: File,
|
||||
) {
|
||||
val validSegments = segments.filter { it.exists() && it.length() > 0L }
|
||||
require(validSegments.isNotEmpty()) { "No recording segments were generated" }
|
||||
if (validSegments.size == 1) {
|
||||
copyFile(validSegments.first(), outputFile)
|
||||
return
|
||||
}
|
||||
|
||||
MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4).use {
|
||||
muxer ->
|
||||
val firstTracks = readTrackFormats(validSegments.first())
|
||||
val videoTrack = firstTracks.video?.let { muxer.addTrack(it) }
|
||||
val audioTrack = firstTracks.audio?.let { muxer.addTrack(it) }
|
||||
require(videoTrack != null) { "No video track in recording segment" }
|
||||
|
||||
muxer.start()
|
||||
var offsetUs = 0L
|
||||
validSegments.forEach { segment ->
|
||||
val durationUs =
|
||||
copySegmentTracks(
|
||||
segment = segment,
|
||||
muxer = muxer,
|
||||
videoOutputTrack = videoTrack,
|
||||
audioOutputTrack = audioTrack,
|
||||
offsetUs = offsetUs,
|
||||
)
|
||||
offsetUs += durationUs + 1L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFile(source: File, target: File) {
|
||||
FileInputStream(source).use { input ->
|
||||
FileOutputStream(target).use { output -> input.copyTo(output) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun readTrackFormats(file: File): TrackFormats {
|
||||
val extractor = MediaExtractor()
|
||||
extractor.setDataSource(file.absolutePath)
|
||||
try {
|
||||
var video: MediaFormat? = null
|
||||
var audio: MediaFormat? = null
|
||||
for (index in 0 until extractor.trackCount) {
|
||||
val format = extractor.getTrackFormat(index)
|
||||
val mime = format.getString(MediaFormat.KEY_MIME).orEmpty()
|
||||
when {
|
||||
mime.startsWith("video/") && video == null -> video = format
|
||||
mime.startsWith("audio/") && audio == null -> audio = format
|
||||
}
|
||||
}
|
||||
return TrackFormats(video = video, audio = audio)
|
||||
} finally {
|
||||
extractor.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun copySegmentTracks(
|
||||
segment: File,
|
||||
muxer: MediaMuxer,
|
||||
videoOutputTrack: Int,
|
||||
audioOutputTrack: Int?,
|
||||
offsetUs: Long,
|
||||
): Long {
|
||||
var segmentDurationUs = 0L
|
||||
segmentDurationUs =
|
||||
maxOf(
|
||||
segmentDurationUs,
|
||||
copyTrack(segment, muxer, "video/", videoOutputTrack, offsetUs),
|
||||
)
|
||||
if (audioOutputTrack != null) {
|
||||
segmentDurationUs =
|
||||
maxOf(
|
||||
segmentDurationUs,
|
||||
copyTrack(segment, muxer, "audio/", audioOutputTrack, offsetUs),
|
||||
)
|
||||
}
|
||||
return segmentDurationUs
|
||||
}
|
||||
|
||||
private fun copyTrack(
|
||||
segment: File,
|
||||
muxer: MediaMuxer,
|
||||
mimePrefix: String,
|
||||
outputTrack: Int,
|
||||
offsetUs: Long,
|
||||
): Long {
|
||||
val extractor = MediaExtractor()
|
||||
extractor.setDataSource(segment.absolutePath)
|
||||
try {
|
||||
val inputTrack = findTrack(extractor, mimePrefix) ?: return 0L
|
||||
extractor.selectTrack(inputTrack)
|
||||
val bufferSize = trackBufferSize(extractor.getTrackFormat(inputTrack))
|
||||
val buffer = ByteBuffer.allocate(bufferSize)
|
||||
val info = MediaCodec.BufferInfo()
|
||||
var firstSampleTimeUs: Long? = null
|
||||
var lastSampleTimeUs = 0L
|
||||
|
||||
while (true) {
|
||||
buffer.clear()
|
||||
val sampleSize = extractor.readSampleData(buffer, 0)
|
||||
if (sampleSize < 0) break
|
||||
|
||||
val sampleTimeUs = extractor.sampleTime
|
||||
val baseTimeUs = firstSampleTimeUs ?: sampleTimeUs.also { firstSampleTimeUs = it }
|
||||
val normalizedTimeUs = (sampleTimeUs - baseTimeUs).coerceAtLeast(0L)
|
||||
info.set(
|
||||
0,
|
||||
sampleSize,
|
||||
offsetUs + normalizedTimeUs,
|
||||
extractor.sampleFlags,
|
||||
)
|
||||
muxer.writeSampleData(outputTrack, buffer, info)
|
||||
lastSampleTimeUs = normalizedTimeUs
|
||||
extractor.advance()
|
||||
}
|
||||
return lastSampleTimeUs
|
||||
} finally {
|
||||
extractor.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findTrack(extractor: MediaExtractor, mimePrefix: String): Int? {
|
||||
for (index in 0 until extractor.trackCount) {
|
||||
val mime = extractor.getTrackFormat(index).getString(MediaFormat.KEY_MIME).orEmpty()
|
||||
if (mime.startsWith(mimePrefix)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun trackBufferSize(format: MediaFormat): Int {
|
||||
return if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
|
||||
format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE).coerceAtLeast(DEFAULT_BUFFER_SIZE)
|
||||
} else {
|
||||
DEFAULT_BUFFER_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
private data class TrackFormats(
|
||||
val video: MediaFormat?,
|
||||
val audio: MediaFormat?,
|
||||
)
|
||||
|
||||
private const val DEFAULT_BUFFER_SIZE = 2 * 1024 * 1024
|
||||
}
|
||||
|
||||
private inline fun MediaMuxer.use(block: (MediaMuxer) -> Unit) {
|
||||
try {
|
||||
block(this)
|
||||
} finally {
|
||||
try {
|
||||
stop()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
release()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.dronex.rec.recording
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleService
|
||||
|
||||
object RecordingSession {
|
||||
private var cameraController: RecordingCameraController? = null
|
||||
|
||||
fun controller(context: Context): RecordingCameraController {
|
||||
return cameraController
|
||||
?: RecordingCameraController(context.applicationContext).also {
|
||||
cameraController = it
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
cameraController?.unbind()
|
||||
cameraController = null
|
||||
}
|
||||
|
||||
fun startForeground(context: Context) {
|
||||
RecordingForegroundService.start(context)
|
||||
}
|
||||
|
||||
fun stopForeground(context: Context) {
|
||||
RecordingForegroundService.stop(context)
|
||||
}
|
||||
|
||||
fun recordingLifecycleOwner(): LifecycleService? = RecordingForegroundService.instance
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.dronex.rec.recording
|
||||
|
||||
enum class RecordingState {
|
||||
IDLE,
|
||||
PREVIEWING,
|
||||
RECORDING,
|
||||
STOPPING,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
data class RecordingStatus(
|
||||
val state: RecordingState,
|
||||
val outputPath: String? = null,
|
||||
val elapsedMillis: Long = 0L,
|
||||
val message: String? = null,
|
||||
) {
|
||||
fun toMap(): Map<String, Any?> =
|
||||
mapOf(
|
||||
"state" to state.name.lowercase(),
|
||||
"outputPath" to outputPath,
|
||||
"elapsedMillis" to elapsedMillis,
|
||||
"message" to message,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user