如何在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+必须声明前台服务类型 -->
额外注意事项
- 电池优化豁免:去设备设置给应用开启「忽略电池优化」,避免系统休眠时杀死服务。
- Android 13+权限:动态申请
POST_NOTIFICATIONS权限,否则前台服务通知无法显示,服务启动失败。 - 稳定定时任务:用WorkManager的
PeriodicWorkRequest实现每日定时,比手动计算延迟更可靠:
val dailyWorkRequest = PeriodicWorkRequestBuilder<RecordingWorker>(1, TimeUnit.DAYS) .setInitialDelay(delay, TimeUnit.MILLISECONDS) // 首次执行延迟 .build() workManager.enqueueUniquePeriodicWork("DailyRecording", ExistingPeriodicWorkPolicy.REPLACE, dailyWorkRequest)
内容的提问来源于stack exchange,提问作者MathieuDheer
相关产品推荐
相关产品推荐

