完成录制功能

This commit is contained in:
2026-06-03 16:04:52 +08:00
parent 9eb8d1cc37
commit fb61e28e2f
20 changed files with 1788 additions and 17 deletions

View File

@@ -39,6 +39,17 @@ android {
}
}
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")
}
flutter {
source = "../.."
}

View File

@@ -1,6 +1,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<application
android:label="flutter_template"
android:label="飞行极控"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -12,10 +26,6 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
@@ -25,17 +35,16 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<service
android:name=".recording.RecordingForegroundService"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>

View File

@@ -1,5 +1,47 @@
package com.example.flutter_template
import androidx.camera.view.PreviewView
import com.example.flutter_template.recording.RecordingPlatformHandler
import com.example.flutter_template.recording.RecordingPreviewFactory
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private var platformHandler: RecordingPlatformHandler? = null
var recordingPreviewView: PreviewView? = null
private set
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine
.platformViewsController
.registry
.registerViewFactory(
"recording-camera-preview",
RecordingPreviewFactory(this),
)
platformHandler =
RecordingPlatformHandler(
this,
flutterEngine.dartExecutor.binaryMessenger,
)
}
fun attachRecordingPreview(previewView: PreviewView) {
recordingPreviewView = previewView
}
fun detachRecordingPreview(previewView: PreviewView? = null) {
if (previewView == null || recordingPreviewView === previewView) {
recordingPreviewView = null
}
}
override fun onDestroy() {
platformHandler?.dispose()
platformHandler = null
super.onDestroy()
}
}

View File

@@ -0,0 +1,37 @@
package com.example.flutter_template.recording
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
object BatteryOptimizationHelper {
fun isIgnoringOptimizations(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true
val manager = context.getSystemService(PowerManager::class.java) ?: return true
return manager.isIgnoringBatteryOptimizations(context.packageName)
}
fun openSettings(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
val intent =
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
return
}
val fallback =
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(fallback)
}
}

View File

@@ -0,0 +1,48 @@
package com.example.flutter_template.recording
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.core.app.NotificationManagerCompat
object DoNotDisturbHelper {
private var savedInterruptionFilter: Int? = null
fun hasAccess(context: Context): Boolean {
val manager = context.getSystemService(NotificationManager::class.java)
return manager?.isNotificationPolicyAccessGranted == true
}
fun openAccessSettings(context: Context) {
val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
fun enable(context: Context): Boolean {
val manager = context.getSystemService(NotificationManager::class.java) ?: return false
if (!manager.isNotificationPolicyAccessGranted) return false
if (savedInterruptionFilter == null) {
savedInterruptionFilter = manager.currentInterruptionFilter
}
manager.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE)
return true
}
fun disable(context: Context) {
val manager = context.getSystemService(NotificationManager::class.java) ?: return
if (!manager.isNotificationPolicyAccessGranted) return
val previous = savedInterruptionFilter ?: NotificationManager.INTERRUPTION_FILTER_ALL
manager.setInterruptionFilter(previous)
savedInterruptionFilter = null
}
fun areNotificationsEnabled(context: Context): Boolean {
return NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}

View File

@@ -0,0 +1,242 @@
package com.example.flutter_template.recording
import android.content.Context
import android.util.Log
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FileOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executor
class RecordingCameraController(
private val appContext: Context,
) {
private val mainExecutor: Executor = ContextCompat.getMainExecutor(appContext)
private var cameraProvider: ProcessCameraProvider? = null
private var preview: Preview? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var activeRecording: Recording? = null
private var boundLifecycleOwner: LifecycleOwner? = null
var status: RecordingStatus = RecordingStatus(RecordingState.IDLE)
private set
var statusListener: ((RecordingStatus) -> Unit)? = null
private var recordingStartedAt: Long = 0L
private var latestOutputPath: String? = null
fun bindPreview(
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
onReady: (Boolean) -> Unit,
) {
val future = ProcessCameraProvider.getInstance(appContext)
future.addListener(
{
try {
val provider = future.get()
cameraProvider = provider
boundLifecycleOwner = lifecycleOwner
preview =
Preview.Builder().build().also {
it.surfaceProvider = previewView.surfaceProvider
}
val recorder =
Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HD))
.build()
videoCapture = VideoCapture.withOutput(recorder)
provider.unbindAll()
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture,
)
updateStatus(RecordingStatus(RecordingState.PREVIEWING))
onReady(true)
} catch (error: Exception) {
Log.e(TAG, "bindPreview failed", error)
updateStatus(
RecordingStatus(
RecordingState.ERROR,
message = error.message,
),
)
onReady(false)
}
},
mainExecutor,
)
}
fun rebindForRecording(
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
onReady: (Boolean) -> Unit,
) {
val provider = cameraProvider
if (provider == null) {
bindPreview(lifecycleOwner, previewView, onReady)
return
}
try {
boundLifecycleOwner = lifecycleOwner
provider.unbindAll()
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture,
)
onReady(true)
} catch (error: Exception) {
Log.e(TAG, "rebindForRecording failed", error)
onReady(false)
}
}
fun startRecording(
withAudio: Boolean,
onStarted: (Boolean, String?) -> Unit,
) {
val capture = videoCapture
if (capture == null || boundLifecycleOwner == null) {
onStarted(false, "Camera not ready")
return
}
if (activeRecording != null) {
onStarted(false, "Already recording")
return
}
val outputFile = createOutputFile()
latestOutputPath = outputFile.absolutePath
val outputOptions = FileOutputOptions.Builder(outputFile).build()
val pending =
capture.output.prepareRecording(appContext, outputOptions).apply {
if (withAudio) {
val granted =
ContextCompat.checkSelfPermission(
appContext,
android.Manifest.permission.RECORD_AUDIO,
) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (granted) {
withAudioEnabled()
}
}
}
recordingStartedAt = System.currentTimeMillis()
updateStatus(
RecordingStatus(
RecordingState.RECORDING,
outputPath = latestOutputPath,
),
)
activeRecording =
pending.start(mainExecutor) { event ->
when (event) {
is VideoRecordEvent.Start -> Unit
is VideoRecordEvent.Finalize -> {
activeRecording = null
if (event.hasError()) {
updateStatus(
RecordingStatus(
RecordingState.ERROR,
message = event.cause?.message ?: "Recording failed",
),
)
} else {
updateStatus(
RecordingStatus(
RecordingState.PREVIEWING,
outputPath = latestOutputPath,
elapsedMillis = System.currentTimeMillis() - recordingStartedAt,
),
)
}
}
}
}
onStarted(true, latestOutputPath)
}
fun stopRecording(onStopped: (String?) -> Unit) {
val recording = activeRecording
if (recording == null) {
onStopped(latestOutputPath)
return
}
updateStatus(
RecordingStatus(
RecordingState.STOPPING,
outputPath = latestOutputPath,
),
)
recording.stop()
activeRecording = null
onStopped(latestOutputPath)
}
fun unbind() {
activeRecording?.stop()
activeRecording = null
cameraProvider?.unbindAll()
cameraProvider = null
preview = null
videoCapture = null
boundLifecycleOwner = null
updateStatus(RecordingStatus(RecordingState.IDLE))
}
fun elapsedMillis(): Long {
if (status.state != RecordingState.RECORDING) return 0L
return System.currentTimeMillis() - recordingStartedAt
}
private fun createOutputFile(): File {
val moviesDir = File(appContext.getExternalFilesDir(null), "recordings")
if (!moviesDir.exists()) {
moviesDir.mkdirs()
}
val timestamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
return File(moviesDir, "REC_$timestamp.mp4")
}
private fun updateStatus(next: RecordingStatus) {
status = next
statusListener?.invoke(next)
}
companion object {
private const val TAG = "RecordingCamera"
}
}

View File

@@ -0,0 +1,182 @@
package com.example.flutter_template.recording
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.content.ContextCompat
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService
import com.example.flutter_template.MainActivity
class RecordingForegroundService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
instance = this
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
when (intent?.action) {
ACTION_START -> {
acquireWakeLock()
val notification = buildNotification("正在录制")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
foregroundServiceTypes(),
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
isRunning = true
}
ACTION_STOP -> {
releaseWakeLock()
stopForeground(STOP_FOREGROUND_REMOVE)
isRunning = false
stopSelf()
}
}
return START_STICKY
}
override fun onDestroy() {
releaseWakeLock()
isRunning = false
if (instance === this) {
instance = null
}
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
private fun acquireWakeLock() {
if (wakeLock?.isHeld == true) return
val manager = getSystemService(PowerManager::class.java) ?: return
wakeLock =
manager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
setReferenceCounted(false)
acquire(4 * 60 * 60 * 1000L)
}
}
private fun releaseWakeLock() {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
wakeLock = null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channel =
NotificationChannel(
CHANNEL_ID,
"录制服务",
NotificationManager.IMPORTANCE_LOW,
).apply {
description = "保持相机录制在后台与息屏时继续运行"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
}
private fun foregroundServiceTypes(): Int {
var types = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
if (hasRecordAudioPermission()) {
types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
return types
}
private fun hasRecordAudioPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.RECORD_AUDIO,
) == PackageManager.PERMISSION_GRANTED
}
private fun buildNotification(content: String): Notification {
val launchIntent =
Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent =
PendingIntent.getActivity(
this,
0,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("录制进行中")
.setContentText(content)
.setSmallIcon(android.R.drawable.presence_video_online)
.setOngoing(true)
.setContentIntent(pendingIntent)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
companion object {
const val CHANNEL_ID = "recording_foreground"
const val NOTIFICATION_ID = 1001
const val ACTION_START = "com.example.flutter_template.recording.START"
const val ACTION_STOP = "com.example.flutter_template.recording.STOP"
private const val WAKE_LOCK_TAG = "record_tool:recording_wake_lock"
@Volatile
var isRunning: Boolean = false
@Volatile
var instance: RecordingForegroundService? = null
fun start(context: Context) {
val intent =
Intent(context, RecordingForegroundService::class.java).apply {
action = ACTION_START
}
ContextCompatStart.startForegroundService(context, intent)
}
fun stop(context: Context) {
val intent =
Intent(context, RecordingForegroundService::class.java).apply {
action = ACTION_STOP
}
context.startService(intent)
}
}
}
private object ContextCompatStart {
fun startForegroundService(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}

View File

@@ -0,0 +1,228 @@
package com.example.flutter_template.recording
import android.app.Activity
import android.os.Build
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.example.flutter_template.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 {
private val methodChannel =
MethodChannel(messenger, "com.example.flutter_template/recording")
private val eventChannel =
EventChannel(messenger, "com.example.flutter_template/recording_events")
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
startRecording(withAudio, enableDnd, result)
}
"stopRecording" -> stopRecording(result)
"disposePreview" -> {
controller.unbind()
result.success(null)
}
"hasNotificationPolicyAccess" ->
result.success(DoNotDisturbHelper.hasAccess(activity))
"openNotificationPolicySettings" -> {
DoNotDisturbHelper.openAccessSettings(activity)
result.success(null)
}
"enableDoNotDisturb" ->
result.success(DoNotDisturbHelper.enable(activity))
"disableDoNotDisturb" -> {
DoNotDisturbHelper.disable(activity)
result.success(null)
}
"isIgnoringBatteryOptimizations" ->
result.success(BatteryOptimizationHelper.isIgnoringOptimizations(activity))
"openBatteryOptimizationSettings" -> {
BatteryOptimizationHelper.openSettings(activity)
result.success(null)
}
"setImmersiveMode" -> {
val enabled = call.argument<Boolean>("enabled") ?: false
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,
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) { 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)
mainHandler.post {
result.success(
mapOf(
"outputPath" to path,
"status" to controller.status.toMap(),
),
)
}
}
}
private fun setImmersiveMode(enabled: Boolean) {
val window = activity.window
WindowCompat.setDecorFitsSystemWindows(window, !enabled)
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
if (enabled) {
insetsController.hide(WindowInsetsCompat.Type.systemBars())
insetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} else {
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()
}
}

View File

@@ -0,0 +1,37 @@
package com.example.flutter_template.recording
import android.content.Context
import android.view.View
import androidx.camera.view.PreviewView
import com.example.flutter_template.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)
}
}

View File

@@ -0,0 +1,30 @@
package com.example.flutter_template.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
}

View File

@@ -0,0 +1,24 @@
package com.example.flutter_template.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,
)
}

View File

@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false