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

如何用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,提问作者Илья Акинин

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 04:17:02