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

Kotlin实现拳击对练回合视频自动录制的时长与频率控制问题(2024年11月22日更新)

Kotlin实现拳击对练回合视频自动录制的时长与频率控制问题(2024年11月22日更新)

你好!我仔细看了你的拳击回合录制APP的需求、代码和当前遇到的问题,咱们一步步来拆解和解决这些问题:

一、核心需求回顾

你的APP需要根据用户输入的三个参数自动完成循环录制流程:

  • numberOfRounds:总录制回合数
  • lengthOfRound:每回合录制时长(分钟)
  • breakRounds:回合间休息时长(分钟)
    核心逻辑是:录制指定时长 → 休息指定时长 → 重复直到所有回合完成,目前你已经通过Handler.postDelayed实现了基础的循环逻辑,但卡在了视频文件生成和权限请求两个关键问题上。

二、当前问题的具体分析与修复方案

1. 视频不生成文件的核心原因与修复

这是Camera2 API录制视频的典型问题:你没有把MediaRecorder的Surface和相机的CaptureSession绑定,相机的视频数据根本没有输出到MediaRecorder,自然不会生成视频文件。

现在你的代码中,相机预览的画面输出到了TextureView的Surface,但录制时MediaRecorder的Surface没有被添加到CameraCaptureSession的目标列表里,相机只会把画面输出到预览Surface,不会给MediaRecorder传递数据。

修复步骤:

(1)重构startRecording方法,绑定MediaRecorder Surface到相机会话
private fun startRecording() {
    try {
        hideInputs()
        if (!::mediaRecorder.isInitialized) {
            mediaRecorder = MediaRecorder()
        }

        val outputFile = getOutputFile()
        val cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager
        val cameraId = cameraManager.cameraIdList.firstOrNull {
            cameraManager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
        } ?: throw IllegalStateException("No back-facing camera found")

        // 1. 初始化MediaRecorder
        mediaRecorder.apply {
            setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
            setVideoSource(MediaRecorder.VideoSource.SURFACE)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setOutputFile(outputFile.absolutePath)
            setVideoEncoder(MediaRecorder.VideoEncoder.H264)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setVideoEncodingBitRate(10000000)
            setVideoFrameRate(30)
            // 建议根据相机支持的尺寸设置,避免硬编码导致不兼容
            val characteristics = cameraManager.getCameraCharacteristics(cameraId)
            val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
            val optimalSize = map?.getOutputSizes(MediaRecorder::class.java)?.maxByOrNull { it.width * it.height }
            optimalSize?.let { setVideoSize(it.width, it.height) } ?: setVideoSize(1920, 1080)
            prepare()
        }

        // 2. 获取MediaRecorder的Surface和预览Surface
        val recorderSurface = mediaRecorder.surface
        val previewSurface = Surface(textureView.surfaceTexture)

        // 3. 重新创建相机CaptureSession,同时包含预览和录制的Surface
        cameraDevice.createCaptureSession(listOf(previewSurface, recorderSurface), object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) {
                cameraCaptureSession = session
                try {
                    // 4. 创建录制用的CaptureRequest(使用TEMPLATE_RECORD模板)
                    val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)
                    captureRequestBuilder.addTarget(previewSurface)
                    captureRequestBuilder.addTarget(recorderSurface)

                    // 启动MediaRecorder
                    mediaRecorder.start()
                    isRecording = true
                    Toast.makeText(this@MainActivity, "Recording started", Toast.LENGTH_SHORT).show()

                    // 开始重复请求,输出数据到两个Surface
                    session.setRepeatingRequest(captureRequestBuilder.build(), null, null)
                } catch (e: Exception) {
                    Log.e("MainActivity", "Failed to start recording session: ${e.message}")
                    Toast.makeText(this@MainActivity, "录制启动失败", Toast.LENGTH_SHORT).show()
                }
            }

            override fun onConfigureFailed(session: CameraCaptureSession) {
                Log.e("MainActivity", "Failed to configure recording session")
                Toast.makeText(this@MainActivity, "相机会话配置失败", Toast.LENGTH_SHORT).show()
            }
        }, null)

    } catch (e: Exception) {
        Log.e("MainActivity", "Error starting recording: ${e.message}")
        Toast.makeText(this, "录制初始化失败: ${e.message}", Toast.LENGTH_SHORT).show()
    }
}
(2)修复stopRecording方法,切换回预览模式

录制停止后,需要把相机会话切回预览模式,释放录制Surface:

private fun stopRecording() {
    try {
        if (isRecording) {
            mediaRecorder.stop()
            mediaRecorder.reset()
            isRecording = false
            Toast.makeText(this, "Recording stopped", Toast.LENGTH_SHORT).show()

            // 切换回预览会话
            val previewSurface = Surface(textureView.surfaceTexture)
            cameraDevice.createCaptureSession(listOf(previewSurface), object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    cameraCaptureSession = session
                    val previewRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
                        addTarget(previewSurface)
                    }
                    session.setRepeatingRequest(previewRequest.build(), null, null)
                }

                override fun onConfigureFailed(session: CameraCaptureSession) {
                    Log.e("MainActivity", "Failed to configure preview session")
                }
            }, null)
        }
        showInputs()
    } catch (e: Exception) {
        Log.e("MainActivity", "Error stopping recording: ${e.message}")
        Toast.makeText(this, "停止录制失败: ${e.message}", Toast.LENGTH_SHORT).show()
    }
}

2. 权限请求循环问题的修复

现在的权限请求逻辑中,如果用户拒绝权限并勾选“不再询问”,代码会继续重复请求权限,导致无限循环。我们需要判断用户是否拒绝了权限且不再提示,此时应该引导用户去设置页开启权限。

修复onRequestPermissionsResult方法:

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    if (requestCode == 102) {
        val deniedPermissions = mutableListOf<String>()
        val hasPermanentDenied = permissions.any { perm ->
            grantResults[permissions.indexOf(perm)] != PackageManager.PERMISSION_GRANTED && 
            !shouldShowRequestPermissionRationale(perm)
        }

        for (i in permissions.indices) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                deniedPermissions.add(permissions[i])
            }
        }

        if (deniedPermissions.isEmpty()) {
            Log.d("MainActivity", "All permissions granted!")
            initializeApp()
        } else {
            if (hasPermanentDenied) {
                // 有用户勾选了不再询问的权限,直接引导到设置
                showPermissionExplanation(deniedPermissions)
            } else {
                permissionAttempts++
                if (permissionAttempts < 2) { // 最多请求2次,避免循环
                    Toast.makeText(
                        this,
                        "需要以下权限才能使用功能: $deniedPermissions,请允许",
                        Toast.LENGTH_LONG
                    ).show()
                    requestPermissions()
                } else {
                    showPermissionExplanation(deniedPermissions)
                }
            }
        }
    }
}

另外,注意Android 10+的Scoped Storage规则:你用的getExternalFilesDir(Environment.DIRECTORY_MOVIES)是APP私有目录,不需要WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE权限,可以把这两个权限从REQUIRED_PERMISSIONS中移除,减少权限请求的数量,也避免不必要的权限问题。

3. 其他优化建议

  • 用Coroutines替代Handler实现循环:Handler的嵌套回调容易导致内存泄漏,用协程的delay函数更简洁易维护,比如:
private fun startRecordingLoop() = lifecycleScope.launch {
    hideInputs()
    for (round in 1..numberOfRounds) {
        Toast.makeText(this@MainActivity, "Recording round $round", Toast.LENGTH_SHORT).show()
        startRecording()
        delay(lengthOfRound * 60 * 1000L)
        stopRecording()
        if (round < numberOfRounds) {
            Toast.makeText(this@MainActivity, "Break round $round", Toast.LENGTH_SHORT).show()
            delay(breakRounds * 60 * 1000L)
        }
    }
    Toast.makeText(this@MainActivity, "All rounds completed", Toast.LENGTH_SHORT).show()
    showInputs()
}

(需要在build.gradle中添加androidx.lifecycle:lifecycle-runtime-ktx依赖)

  • 相机资源的释放:在Activity的onDestroy方法中,确保关闭相机设备和释放MediaRecorder:
override fun onDestroy() {
    super.onDestroy()
    if (::cameraDevice.isInitialized) {
        cameraDevice.close()
    }
    if (::mediaRecorder.isInitialized) {
        mediaRecorder.release()
    }
}

三、总结

  1. 先修复权限请求循环问题,简化权限列表,优化权限请求逻辑;
  2. 重点修复MediaRecorder与Camera2 Session的绑定问题,这是视频不生成的核心原因;
  3. 可以用协程替代Handler让循环逻辑更清晰;
  4. 增加更明确的错误提示,方便调试和用户感知。

按照这些修改后,你的APP应该能正常生成录制的视频文件,权限请求也不会陷入循环了!

备注:内容来源于stack exchange,提问作者FranciscoRincon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 10:28:05