You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Android Kotlin的Worker中录制1小时视频?

解决Worker中Camera2录制视频的-22错误 & 更优实现方案

先直接说你代码里导致recorder.start()报-22错误(MEDIA_ERROR_INVALID_PARAMETER)的几个关键问题:

  • 相机资源没给MediaRecorder留着:你打开相机后立刻camera.close(),但MediaRecorder.VideoSource.DEFAULT本质依赖打开的相机,相机关了视频源根本拿不到。
  • Camera2和MediaRecorder绑定流程错了:用Camera2的话,MediaRecorder得通过CameraCaptureSession绑定到相机,不是直接用默认视频源就能干活的。
  • Worker线程提前结束:doWork()里刚发起相机操作就返回Result.success(),HandlerThread可能直接被销毁,相机操作半路中断。
  • 存储路径过时:Environment.getExternalStoragePublicDirectory()在Android 10之后已废弃,新系统上用这个可能存不了文件。

先修复当前Camera2+MediaRecorder的问题

下面是修正后的Worker代码,解决了上述问题,还加入了1小时自动停止逻辑:

package com.android.example.workerrecordingscheduler

import android.Manifest
import android.content.ContentValues
import android.content.ContentValues.TAG
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraCaptureSession
import android.media.MediaRecorder
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.provider.MediaStore
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.util.concurrent.CountDownLatch

class RecordingWorker(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    private lateinit var cameraDevice: CameraDevice
    private val waitLatch = CountDownLatch(1) // 等待录制完成再结束Worker

    override fun doWork(): Result {
        val cameraThread = HandlerThread("CameraRecordingThread")
        cameraThread.start()
        val cameraHandler = Handler(cameraThread.looper)

        val cameraManager = applicationContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        // 优先选后置相机(id为0),没有则用第一个可用相机
        val cameraId = cameraManager.cameraIdList.firstOrNull { it == "0" } ?: cameraManager.cameraIdList[0]

        // 检查权限,没权限直接返回失败
        if (ActivityCompat.checkSelfPermission(applicationContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(applicationContext, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            Log.e(TAG, "相机或录音权限未授予,无法录制")
            cameraThread.quitSafely()
            return Result.failure()
        }

        try {
            cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
                @RequiresApi(Build.VERSION_CODES.O)
                override fun onOpened(camera: CameraDevice) {
                    cameraDevice = camera
                    startCameraRecording(camera, cameraHandler)
                }

                override fun onDisconnected(camera: CameraDevice) {
                    camera.close()
                    waitLatch.countDown()
                }

                override fun onError(camera: CameraDevice, errorCode: Int) {
                    Log.e(TAG, "相机打开失败,错误码:$errorCode")
                    camera.close()
                    waitLatch.countDown()
                }
            }, cameraHandler)

            waitLatch.await() // 等待录制完成
            cameraThread.quitSafely()
            return Result.success()
        } catch (e: Exception) {
            Log.e(TAG, "录制失败", e)
            cameraThread.quitSafely()
            return Result.failure()
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun startCameraRecording(camera: CameraDevice, handler: Handler) {
        val recorder = MediaRecorder(applicationContext).apply {
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setVideoSource(MediaRecorder.VideoSource.SURFACE) // 用Surface与Camera2绑定
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setVideoEncoder(MediaRecorder.VideoEncoder.H264)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setVideoSize(1280, 720) // 固定分辨率避免兼容问题
            setVideoFrameRate(30)
            setOutputFile(getValidOutputPath()) // 适配新系统的存储路径
            prepare()
        }

        val recorderSurface = recorder.surface
        // 创建录制用的CaptureRequest
        val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
            addTarget(recorderSurface)
        }

        // 创建相机捕获会话,绑定MediaRecorder的Surface
        camera.createCaptureSession(listOf(recorderSurface), object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) {
                try {
                    session.setRepeatingRequest(captureRequest.build(), null, handler)
                    recorder.start()
                    Log.d(TAG, "录制开始")

                    // 1小时后自动停止录制
                    handler.postDelayed({
                        recorder.stop()
                        recorder.release()
                        session.stopRepeating()
                        session.close()
                        cameraDevice.close()
                        waitLatch.countDown()
                        Log.d(TAG, "1小时录制完成")
                    }, 3600 * 1000)
                } catch (e: Exception) {
                    Log.e(TAG, "启动录制失败", e)
                    cleanUpResources(recorder, session, camera)
                }
            }

            override fun onConfigureFailed(session: CameraCaptureSession) {
                Log.e(TAG, "相机会话配置失败")
                cleanUpResources(recorder, session, camera)
            }
        }, handler)
    }

    // 清理资源工具方法
    private fun cleanUpResources(recorder: MediaRecorder, session: CameraCaptureSession, camera: CameraDevice) {
        recorder.release()
        session.close()
        camera.close()
        waitLatch.countDown()
    }

    // 适配Android 10+的存储路径
    private fun getValidOutputPath(): String {
        val fileName = "myvideo_${System.currentTimeMillis()}.mp4"
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val contentValues = ContentValues().apply {
                put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
                put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/MyRecorder")
            }
            val uri = applicationContext.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
            applicationContext.contentResolver.openFileDescriptor(uri!!, "w")?.use {
                it.fileDescriptor.toString()
            } ?: ""
        } else {
            val saveDir = java.io.File(android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_MOVIES), "MyRecorder")
            if (!saveDir.exists()) saveDir.mkdirs()
            java.io.File(saveDir, fileName).absolutePath
        }
    }
}

同时给Manifest添加相机特性声明,让系统识别应用需要相机:

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />

更靠谱的实现方案:ForegroundService + WorkManager

你需要远程设备每日录制1小时,Worker在设备休眠时可能被系统杀死,ForegroundService(前台服务)优先级更高,不易被销毁,搭配WorkManager触发定时任务才是正确姿势,还能使用更简洁的CameraX替代复杂的Camera2。

步骤1:编写录制用的ForegroundService

package com.android.example.workerrecordingscheduler

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.camera.core.CameraSelector
import androidx.camera.core.ProcessCameraProvider
import androidx.camera.video.Recorder
import androidx.camera.video.VideoCapture
import android.app.Service
import android.os.IBinder

class RecordingForegroundService : Service() {
    private val TAG = "RecordingService"
    private lateinit var videoCapture: VideoCapture<Recorder>

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 启动前台服务,必须显示通知(Android 8+要求)
        startForeground(1001, createRecordingNotification())
        initCameraX()
        return START_STICKY
    }

    private fun initCameraX() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            val recorder = Recorder.Builder()
                .setQualitySelector(Recorder.QualitySelector.HIGHEST_QUALITY)
                .build()

            videoCapture = VideoCapture.withOutput(recorder)

            try {
                cameraProvider.unbindAll()
                // 绑定CameraX到进程生命周期,无需Activity的Lifecycle
                cameraProvider.bindToLifecycle(ProcessLifecycleOwner.get(), cameraSelector, videoCapture)
                startCameraXRecording()
            } catch (e: Exception) {
                Log.e(TAG, "CameraX绑定失败", e)
                stopSelf()
            }
        }, ContextCompat.getMainExecutor(this))
    }

    private fun startCameraXRecording() {
        val outputOptions = getVideoOutputOptions()
        videoCapture.startRecording(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : androidx.camera.video.VideoRecordEvent.Listener {
                override fun onVideoRecordEvent(event: androidx.camera.video.VideoRecordEvent) {
                    when(event) {
                        is androidx.camera.video.VideoRecordEvent.Start -> Log.d(TAG, "CameraX录制开始")
                        is androidx.camera.video.VideoRecordEvent.Finalize -> {
                            Log.d(TAG, "CameraX录制结束")
                            stopForeground(STOP_FOREGROUND_REMOVE)
                            stopSelf()
                        }
                    }
                }
            }
        )

        // 1小时后停止录制
        Handler(Looper.getMainLooper()).postDelayed({
            videoCapture.stopRecording()
        }, 3600 * 1000)
    }

    // 创建前台服务通知
    private fun createRecordingNotification(): Notification {
        val channelId = "recording_channel"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, "视频录制通知", NotificationManager.IMPORTANCE_LOW)
            getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
        }
        return NotificationCompat.Builder(this, channelId)
            .setContentTitle("正在录制视频")
            .setSmallIcon(R.mipmap.ic_launcher)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .build()
    }

    // 获取CameraX输出文件配置
    private fun getVideoOutputOptions(): VideoCapture.OutputFileOptions {
        val fileName = "myvideo_${System.currentTimeMillis()}.mp4"
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val contentValues = ContentValues().apply {
                put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
                put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/MyRecorder")
            }
            VideoCapture.OutputFileOptions.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues).build()
        } else {
            val saveDir = java.io.File(android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_MOVIES), "MyRecorder")
            if (!saveDir.exists()) saveDir.mkdirs()
            VideoCapture.OutputFileOptions.Builder(java.io.File(saveDir, fileName)).build()
        }
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

步骤2:修改Worker启动前台服务

class RecordingWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    override fun doWork(): Result {
        val intent = Intent(applicationContext, RecordingForegroundService::class.java)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            applicationContext.startForegroundService(intent)
        } else {
            applicationContext.startService(intent)
        }
        return Result.success()
    }
}

步骤3:Manifest注册Service

<service
    android:name=".RecordingForegroundService"
    android:foregroundServiceType="camera|microphone" /> <!-- Android 12+必须声明前台服务类型 -->

额外注意事项

  1. 电池优化豁免:去设备设置给应用开启「忽略电池优化」,避免系统休眠时杀死服务。
  2. Android 13+权限:动态申请POST_NOTIFICATIONS权限,否则前台服务通知无法显示,服务启动失败。
  3. 稳定定时任务:用WorkManager的PeriodicWorkRequest实现每日定时,比手动计算延迟更可靠:
val dailyWorkRequest = PeriodicWorkRequestBuilder<RecordingWorker>(1, TimeUnit.DAYS)
    .setInitialDelay(delay, TimeUnit.MILLISECONDS) // 首次执行延迟
    .build()
workManager.enqueueUniquePeriodicWork("DailyRecording", ExistingPeriodicWorkPolicy.REPLACE, dailyWorkRequest)

内容的提问来源于stack exchange,提问作者MathieuDheer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 19:12:02