如何用Kotlin实现Android前置摄像头后台录制(附问题代码)
Android Kotlin 前置摄像头后台录制问题修复方案
问题描述
我正在用Kotlin开发一款Android应用,需要实现前置摄像头的后台录制功能。网上没找到针对Kotlin的解决方案,自己写了代码但无法正常运行,求指导如何实现。
原代码
MainActivity.kt
class MainActivity : AppCompatActivity() { private lateinit var startButton: Button private lateinit var stopButton: Button private val PERMISSION_REQUEST_CODE=1001 private val REQUEST_CAMERA_AND_AUDIO_PERMISSION = 1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startButton = findViewById(R.id.start_button) stopButton = findViewById(R.id.stop_button) // 请求摄像头、录音和外部存储写入权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE) } startButton.setOnClickListener { val intent = Intent(this, VideoRecordService::class.java) startForegroundService(intent) } stopButton.setOnClickListener { val intent = Intent(this, VideoRecordService::class.java) stopService(intent) } } }
VideoRecordService.kt
class VideoRecordService : Service() { private var camera: Camera? = null private var mediaRecorder: MediaRecorder? = null private var outputFile: File? = null private val permissions = arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE ) override fun onBind(intent: Intent): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(1, createNotification()) startRecording() return START_STICKY } private fun createNotification(): Notification { val channelId = "VideoRecorder" val channelName = "Video Recorder" val importance = NotificationManager.IMPORTANCE_LOW val notificationChannel = NotificationChannel(channelId, channelName, importance) val notificationManager = getSystemService(NotificationManager::class.java) notificationManager?.createNotificationChannel(notificationChannel) val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) return NotificationCompat.Builder(this, channelId) .setContentTitle("Recording video") .setContentText("Touch to open app") .setSmallIcon(R.drawable.ic_launcher_background) .setContentIntent(pendingIntent) .build() } private fun startRecording() { camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT) outputFile = getOutputFile() if (!outputFile!!.exists()) { Log.e("VideoRecordService", "Output file does not exist") return } mediaRecorder = MediaRecorder().apply { setCamera(camera) setAudioSource(MediaRecorder.AudioSource.DEFAULT) setVideoSource(MediaRecorder.VideoSource.DEFAULT) setOutputFormat(MediaRecorder.OutputFormat.DEFAULT) setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) setOutputFile(outputFile?.path) setPreviewDisplay(null) try { prepare() start() } catch (e: IOException) { e.printStackTrace() } } } private fun getOutputFile(): File? { val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) Log.d("VideoRecordService", "Movies directory: $dir") if (!dir.exists()) { val created = dir.mkdirs() Log.d("VideoRecordService", "Movies directory created: $created") } val videoFile = "video_${System.currentTimeMillis()}.mp4" val outputFile = File(dir, videoFile) Log.d("VideoRecordService", "Output file: $outputFile") return outputFile } private fun stopRecording() { if (mediaRecorder == null) return try { mediaRecorder?.stop() mediaRecorder?.reset() } catch (e: IllegalStateException) { e.printStackTrace() } mediaRecorder?.release() camera?.lock() saveVideoToGallery() Toast.makeText(this, "Video saved to gallery", Toast.LENGTH_SHORT).show() } private fun saveVideoToGallery() { if (outputFile == null) return val values = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, outputFile?.name) put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()) put(MediaStore.Video.Media.DATA, outputFile?.path) } val contentResolver = applicationContext.contentResolver contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values) } override fun onDestroy() { super.onDestroy() if (mediaRecorder != null) { stopRecording() } camera?.release() camera = null } }
问题分析与修复方案
1. 权限请求逻辑错误
原代码用&&判断权限,只有当所有权限都未授予时才会发起请求,这会导致部分权限缺失时无法触发请求。需改为||,并根据Android版本适配存储权限:
2. 旧Camera API使用问题
原代码使用已废弃的android.hardware.Camera类,需补充解锁摄像头步骤,同时明确MediaRecorder的参数配置(避免使用DEFAULT)。
3. 输出文件判断错误
getOutputFile()创建的是新文件,outputFile!!.exists()会返回false导致录制终止,需删除该判断。
4. Android 10+存储适配
Environment.getExternalStoragePublicDirectory已废弃,需改用MediaStore写入文件,无需直接操作文件路径。
5. PendingIntent Flag适配
Android 12+要求指定FLAG_IMMUTABLE或FLAG_MUTABLE,否则会抛出异常。
6. 服务与资源释放优化
调整MediaRecorder和Camera的释放顺序,避免崩溃;添加媒体扫描广播确保视频能在图库中显示。
修复后的代码
MainActivity.kt
class MainActivity : AppCompatActivity() { private lateinit var startButton: Button private lateinit var stopButton: Button private val PERMISSION_REQUEST_CODE = 1001 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startButton = findViewById(R.id.start_button) stopButton = findViewById(R.id.stop_button) // 按需收集需要请求的权限 val neededPermissions = mutableListOf<String>() if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { neededPermissions.add(Manifest.permission.CAMERA) } if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { neededPermissions.add(Manifest.permission.RECORD_AUDIO) } // Android 9及以下需要存储权限 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { neededPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } if (neededPermissions.isNotEmpty()) { ActivityCompat.requestPermissions(this, neededPermissions.toTypedArray(), PERMISSION_REQUEST_CODE) } startButton.setOnClickListener { val intent = Intent(this, VideoRecordService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) } } stopButton.setOnClickListener { val intent = Intent(this, VideoRecordService::class.java) stopService(intent) } } }
VideoRecordService.kt
class VideoRecordService : Service() { private var camera: Camera? = null private var mediaRecorder: MediaRecorder? = null private var videoUri: Uri? = null // 适配Android 10+的Uri存储 override fun onBind(intent: Intent): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(1, createNotification()) startRecording() return START_STICKY } private fun createNotification(): Notification { val channelId = "VideoRecorder" val channelName = "视频录制" val importance = NotificationManager.IMPORTANCE_LOW val notificationChannel = NotificationChannel(channelId, channelName, importance) val notificationManager = getSystemService(NotificationManager::class.java) notificationManager?.createNotificationChannel(notificationChannel) val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE } else { 0 } val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, pendingIntentFlags) return NotificationCompat.Builder(this, channelId) .setContentTitle("正在录制视频") .setContentText("点击打开应用") .setSmallIcon(R.drawable.ic_launcher_foreground) // 建议使用透明背景图标 .setContentIntent(pendingIntent) .build() } private fun startRecording() { try { // 打开前置摄像头并解锁 camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT) camera?.unlock() mediaRecorder = MediaRecorder().apply { setCamera(camera) setAudioSource(MediaRecorder.AudioSource.MIC) setVideoSource(MediaRecorder.VideoSource.CAMERA) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setVideoEncoder(MediaRecorder.VideoEncoder.H264) setVideoSize(1280, 720) // 指定分辨率 setVideoFrameRate(30) // 指定帧率 setPreviewDisplay(SurfaceHolder().surface) // 空Surface用于后台录制 // 根据Android版本处理输出路径 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val values = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, "video_${System.currentTimeMillis()}.mp4") put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") put(MediaStore.Video.Media.RELATIVE_PATH, "${Environment.DIRECTORY_MOVIES}/MyRecorder") } videoUri = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values) videoUri?.let { setOutputFile(contentResolver.openFileDescriptor(it, "w")?.fileDescriptor) } } else { val dir = File("${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)}/MyRecorder") if (!dir.exists()) dir.mkdirs() val outputFile = File(dir, "video_${System.currentTimeMillis()}.mp4") setOutputFile(outputFile.path) } prepare() start() } } catch (e: Exception) { e.printStackTrace() stopSelf() // 录制失败时停止服务 } } private fun stopRecording() { mediaRecorder?.let { mr -> try { mr.stop() } catch (e: IllegalStateException) { e.printStackTrace() } mr.reset() mr.release() } camera?.let { cam -> cam.lock() cam.release() } mediaRecorder = null camera = null // 通知系统扫描新视频 videoUri?.let { sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, it)) } Toast.makeText(this, "视频已保存", Toast.LENGTH_SHORT).show() } override fun onDestroy() { super.onDestroy() stopRecording() } }
额外配置
在AndroidManifest.xml中添加权限和服务声明:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ...> <service android:name=".VideoRecordService" android:foregroundServiceType="camera|microphone" /> </application>
注意事项
- Android 12+必须指定
foregroundServiceType为camera|microphone,否则无法在后台使用摄像头和麦克风 - 部分厂商设备对后台摄像头录制有系统限制,需测试兼容性
- 推荐使用Jetpack CameraX替代旧Camera API,Kotlin开发更简洁且兼容性更好
内容的提问来源于stack exchange,提问作者Илья Акинин
相关产品推荐
相关产品推荐

