Android中CaptureVideo()回调始终返回false问题求助
解决方案:通过ContentObserver监听视频写入完成
针对Android 10及以上版本中ActivityResultContracts.CaptureVideo()回调提前触发(视频已保存但返回false)的问题,推荐通过注册ContentObserver监听目标Uri的内容变化来准确捕获视频写入完成的时机,替代依赖回调返回的result值。
具体实现步骤
- 定义ContentObserver子类
重写onChange方法,通过多次检查文件大小是否稳定来判定视频是否写入完成,确认后执行后续逻辑并注销观察者:
private class VideoCaptureObserver( private val contentResolver: ContentResolver, private val targetUri: Uri, private val onVideoSaved: () -> Unit ) : ContentObserver(Handler(Looper.getMainLooper())) { private var lastFileSize = 0L private var stableCount = 0 // 最大检查次数,避免无限等待 private val maxCheckCount = 5 override fun onChange(selfChange: Boolean, uri: Uri?) { if (uri != targetUri) return val currentSize = getFileSize() when { // 连续两次文件大小一致,且达到检查阈值,判定写入完成 currentSize == lastFileSize && stableCount >= 2 -> { contentResolver.unregisterContentObserver(this) onVideoSaved.invoke() } // 文件大小变化,重置计数 currentSize != lastFileSize -> { lastFileSize = currentSize stableCount = 0 postDelayedCheck() } // 文件大小稳定但未达阈值,继续检查 stableCount < maxCheckCount -> { stableCount++ postDelayedCheck() } // 超时处理 else -> { contentResolver.unregisterContentObserver(this) // 可添加超时错误逻辑 } } } private fun getFileSize(): Long { return try { contentResolver.openFileDescriptor(targetUri, "r")?.use { it.statSize } ?: 0L } catch (e: Exception) { 0L } } private fun postDelayedCheck() { Handler(Looper.getMainLooper()).postDelayed({ onChange(false, targetUri) }, 500) } }
- 注册观察者并启动拍摄
忽略原回调的result值,在拍摄流程结束后启动观察者监听:
private val getVideo = registerForActivityResult(ActivityResultContracts.CaptureVideo()) { // 仅作为拍摄流程结束的信号,开始监听文件写入状态 val observer = VideoCaptureObserver(contentResolver, targetUri) { // 视频写入完成后的业务逻辑 Toast.makeText(this, "视频保存完成", Toast.LENGTH_SHORT).show() } contentResolver.registerContentObserver(targetUri, true, observer) } // 启动视频拍摄的方法 fun startVideoCapture() { targetUri = createVideoUri() getVideo.launch(targetUri) } // 生成存储视频的Uri(基于MediaStore) private fun createVideoUri(): Uri { val values = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, "captured_${System.currentTimeMillis()}.mp4") put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES) } return contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)!! }
- 额外注意事项
- Android 10及以上无需申请
WRITE_EXTERNAL_STORAGE权限;Android 13+若使用Toast等通知,需申请POST_NOTIFICATIONS权限。 - 可根据需求调整检查间隔和最大检查次数,适配不同设备的视频写入速度。
替代方案:自定义ActivityResultContract
若偏好统一的合约调用方式,可自定义合约在后台线程等待文件写入完成(需配合协程避免阻塞主线程):
class CaptureVideoWithCompletion : ActivityResultContract<Uri, Boolean>() { override fun createIntent(context: Context, input: Uri): Intent { return Intent(MediaStore.ACTION_VIDEO_CAPTURE).putExtra(MediaStore.EXTRA_OUTPUT, input) } override suspend fun parseResult(resultCode: Int, intent: Intent?): Boolean { if (resultCode != Activity.RESULT_OK) return false val uri = intent?.getParcelableExtra<Uri>(MediaStore.EXTRA_OUTPUT) ?: return false var lastSize = 0L var stableCount = 0 repeat(5) { val currentSize = getFileSize(uri, context.contentResolver) if (currentSize == lastSize) stableCount++ else stableCount = 0 lastSize = currentSize delay(500) if (stableCount >=2) return@repeat } return stableCount >=2 } private fun getFileSize(uri: Uri, resolver: ContentResolver): Long { return try { resolver.openFileDescriptor(uri, "r")?.use { it.statSize } ?:0L } catch (e: Exception) {0L} } }
使用时替换原合约即可:
private val getVideo = registerForActivityResult(CaptureVideoWithCompletion()) { success -> if (success) { // 视频已写入完成 } }
内容的提问来源于stack exchange,提问作者Ali Doran
相关产品推荐
相关产品推荐

