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

Android中将修剪后的MP4保存至外部存储时文件损坏

问题原因与解决方案

你的核心问题是:saveVideoToExternalStorage方法仅向MediaStore插入了媒体元数据,并未实际复制修剪后视频的文件内容。第三方播放器读取的是你插入的空媒体条目,自然无法播放;而ExoPlayer是直接播放修剪后的原始Uri,所以能正常工作。

修复步骤

1. 实现实际的文件内容复制

插入MediaStore条目后,需要通过ContentResolver获取输出流,将原修剪视频的内容写入到新的媒体文件中。

2. 适配Android版本存储特性

  • Android 10(API 29)及以上:无需WRITE_EXTERNAL_STORAGE权限,直接使用MediaStore操作
  • Android 9及以下:需要WRITE_EXTERNAL_STORAGE权限,同时注意文件路径的兼容性

修正后的完整代码

class MainActivity : AppCompatActivity() {

    private lateinit var trimmedUri: Uri
    private lateinit var playerView: PlayerView

    private val REQUEST_CODE_PERMISSIONS = 10
    private val REQUIRED_PERMISSIONS =
        mutableListOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE).toTypedArray()

    private var player: ExoPlayer? = null
    private var playWhenReady = true
    private var currentItem = 0
    private var playbackPosition = 0L

    private fun setUpPlayer(url: String) {
        player = ExoPlayer.Builder(this)
            .build()
            .also { exoPlayer ->
                playerView.player = exoPlayer
                val mediaItem = MediaItem.fromUri(url)
                exoPlayer.setMediaItem(mediaItem)
                exoPlayer.playWhenReady = playWhenReady
                exoPlayer.seekTo(currentItem, playbackPosition)
                exoPlayer.prepare()
            }
    }

    private val startForResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK &&
                result.data != null
            ) {
                trimmedUri = Uri.parse(TrimVideo.getTrimmedVideoPath(result.data))
                Log.i("TAG", "Trimmed path:: $trimmedUri")
                setUpPlayer(trimmedUri.toString())
                
                lifecycleScope.launch {
                    val savedUri = saveVideoToExternalStorage()
                    Toast.makeText(this@MainActivity, "保存后的Uri: $savedUri", Toast.LENGTH_LONG).show()
                }
            } else {
                Log.i("TAG", "修剪失败")
            }
        }

    private val singleVideoPickerLauncher =
        registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { videoUri ->
            TrimVideo.activity(videoUri.toString())
                .setHideSeekBar(true)
                .start(this, startForResult)
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        playerView = findViewById(R.id.playerview)

        findViewById<Button>(R.id.btn).setOnClickListener {
            // Android 9及以下先检查权限
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && !allPermissionsGranted()) {
                ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
            } else {
                singleVideoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                singleVideoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
            } else {
                Log.i("TAG", "权限被拒绝,无法选择视频")
            }
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }

    private suspend fun saveVideoToExternalStorage(): Uri? {
        return withContext(Dispatchers.IO) {
            try {
                val videoCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
                } else {
                    MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                }

                val originalFile = File(trimmedUri.path ?: "")
                val fileName = "${System.currentTimeMillis()}_${originalFile.name}"

                val contentValues = ContentValues().apply {
                    put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
                    put(MediaStore.Video.Media.TITLE, fileName)
                    put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
                    put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
                    // Android 9及以下需要设置DATA字段
                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                        val externalDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
                        val savePath = File(externalDir, fileName).absolutePath
                        put(MediaStore.Video.Media.DATA, savePath)
                    }
                }

                // 插入媒体条目
                val savedUri = contentResolver.insert(videoCollection, contentValues) ?: return@withContext null

                // 复制文件内容到新的Uri
                contentResolver.openOutputStream(savedUri)?.use { outputStream ->
                    contentResolver.openInputStream(trimmedUri)?.use { inputStream ->
                        inputStream.copyTo(outputStream)
                    }
                }

                // 通知媒体库扫描更新
                sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, savedUri))

                return@withContext savedUri
            } catch (e: Exception) {
                Log.e("SaveVideoError", "保存失败: ${e.message}", e)
                return@withContext null
            }
        }
    }
}

关键修改点说明

  • 变量重命名:将原uri改为trimmedUri,避免与保存后的Uri混淆
  • 实际内容复制:通过openInputStream读取原修剪视频,openOutputStream写入到新创建的MediaStore条目
  • 权限逻辑优化:将权限检查提前到视频选择前,避免流程混乱
  • 媒体库扫描:发送广播通知系统更新媒体库,确保第三方播放器能立即找到保存的视频
  • 异常捕获:添加try-catch处理文件读写过程中的可能错误

内容的提问来源于stack exchange,提问作者BraveEvidence

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 02:32:44