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
相关产品推荐
相关产品推荐

