ExoPlayer播放本地已存在文件时偶发FileNotFoundException问题
问题:ExoPlayer播放DownloadManager下载的视频时出现FileNotFoundException(文件实际存在)
环境与实现逻辑
- 使用
androidx.media3:media3-exoplayer:1.0.1 - 通过Android DownloadManager将视频下载到公共Download文件夹,文件名使用后端返回的UUID
- 播放时通过UUID匹配对应的视频URI,使用ExoPlayer播放
ViewModel下载触发代码
private fun downloadVideos() { viewModelScope.launch { mediaRepository.getMediaItems().forEach { item -> downloader.downloadFile(item.uuid, item.filePath) } } }
Downloader实现类
class AndroidDownloader( context: Context ): Downloader { private val downloadManager = context.getSystemService(DownloadManager::class.java) override fun downloadFile(title: String, url: String): Long { val request = DownloadManager.Request(url.toUri()) .setMimeType("video/mp4") .setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED or DownloadManager.Request.VISIBILITY_VISIBLE ) .setTitle(title) .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, title) return downloadManager.enqueue(request) } }
播放触发逻辑(ViewModel)
override fun playVideo(uuid: String) { videoIsPlaying = true currentUri = fileParser.getUri(uuid) currentUri?.let { viewModelScope.launch { videoPlayer.playVideo(it) } } }
FileParser(根据UUID获取URI)
class FileParser(private val context: Context, private val pref: DefaultSharedPref) { private val listOfUris = mutableListOf<Uri>() fun getUri(uuid: String) : Uri? { if (listOfUris.isEmpty()) { val manager: DownloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager pref.getVideoIds().forEach { val uri = manager.getUriForDownloadedFile(it.toLong()) uri.path?.let { listOfUris.add(uri) } } } listOfUris.forEach { val fileName = context.getFileName(it) if (fileName == uuid) return it } return null } } fun Context.getFileName(uri: Uri): String? = when(uri.scheme) { ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) else -> uri.path?.let(::File)?.name } private fun Context.getContentFileName(uri: Uri): String? = runCatching { contentResolver.query(uri, null, null, null, null)?.use { cursor -> cursor.moveToFirst() return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString) } }.getOrNull()
ExoPlayer播放实现
fun playVideo(uri: Uri) { player.setMediaItem(MediaItem.fromUri(uri)) player.prepare() player.play() }
Jetpack Compose UI代码
AnimatedVisibility(visible = videoIsPlaying) { AndroidView( factory = { context -> PlayerView(context).also { it.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL it.useController = false it.player = videoPlayer.player } }, update = { when (lifecycle) { Lifecycle.Event.ON_PAUSE -> { it.onPause() it.player?.pause() } Lifecycle.Event.ON_RESUME -> { it.onResume() } else -> Unit } }, modifier = Modifier .fillMaxHeight() .aspectRatio(16 / 9f) ) }
问题现象
视频可正常播放数小时/数分钟,但一段时间后抛出以下异常:
Playback error androidx.media3.exoplayer.ExoPlaybackException: Source error at androidx.media3.exoplayer.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:652) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:624) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.HandlerThread.run(HandlerThread.java:67) Caused by: androidx.media3.datasource.ContentDataSource$ContentDataSourceException: java.io.FileNotFoundException: open failed: ENOENT (No such file or directory) at androidx.media3.datasource.ContentDataSource.open(ContentDataSource.java:157) at androidx.media3.datasource.DefaultDataSource.open(DefaultDataSource.java:272) at androidx.media3.datasource.StatsDataSource.open(StatsDataSource.java:86) at androidx.media3.exoplayer.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1006) at androidx.media3.exoplayer.upstream.Loader$LoadTask.run(Loader.java:414) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.io.FileNotFoundException: open failed: ENOENT (No such file or directory) at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:151) at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:780) at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2029) at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1967) at androidx.media3.datasource.ContentDataSource.open(ContentDataSource.java:89) at androidx.media3.datasource.DefaultDataSource.open(DefaultDataSource.java:272) at androidx.media3.datasource.StatsDataSource.open(StatsDataSource.java:86) at androidx.media3.exoplayer.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1006) at androidx.media3.exoplayer.upstream.Loader$LoadTask.run(Loader.java:414) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012)
- 此时文件实际仍存在且文件名未修改
- 首次触发异常后视频无法再播放,仅能通过卸载重装应用或清理缓存临时解决,一段时间后问题复发
- 已尝试的无效方案:
- 使用
DefaultDataSource自定义数据源工厂 - 添加
seekTo(0)重启视频 - 添加文件访问权限
- 使用
问题分析与解决方案
核心原因
- DownloadManager的Content URI失效:
content://downloads/all_downloads/xxx这类URI依赖系统DownloadProvider的数据库记录,系统清理(媒体库扫描、Provider缓存失效、系统优化)会导致记录丢失,使URI无法访问实际文件。 - URI缓存机制缺陷:
listOfUris仅首次初始化,后续不会更新,即使原URI失效也持续使用。 - 公共目录访问稳定性差:公共Download文件夹的文件可能被其他应用/系统修改,Content URI访问依赖系统Provider状态,稳定性不如直接访问文件路径。
解决方案
方案1:改用FileProvider URI直接访问文件路径
这是最稳定的方案,避免依赖DownloadProvider的数据库状态:
- 修改Downloader,保存文件绝对路径到SharedPreferences:
class AndroidDownloader( context: Context, private val pref: DefaultSharedPref ): Downloader { private val downloadManager = context.getSystemService(DownloadManager::class.java) override fun downloadFile(title: String, url: String): Long { val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), title) val request = DownloadManager.Request(url.toUri()) .setMimeType("video/mp4") .setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED or DownloadManager.Request.VISIBILITY_VISIBLE ) .setTitle(title) .setDestinationUri(Uri.fromFile(file)) val downloadId = downloadManager.enqueue(request) pref.saveVideoPath(title, file.absolutePath) return downloadId } }
- 修改FileParser,通过路径生成FileProvider URI:
class FileParser(private val context: Context, private val pref: DefaultSharedPref) { fun getUri(uuid: String) : Uri? { val filePath = pref.getVideoPath(uuid) ?: return null val file = File(filePath) return if (file.exists()) { FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) } else null } }
需在Manifest中配置FileProvider:
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>并在
res/xml/file_paths.xml中添加:<paths> <external-path name="downloads" path="Download/" /> </paths>
方案2:修复URI缓存与有效性校验
每次获取URI时重新校验有效性,避免使用失效URI:
class FileParser(private val context: Context, private val pref: DefaultSharedPref) { fun getUri(uuid: String) : Uri? { val manager: DownloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager pref.getVideoIds().forEach { downloadId -> val uri = manager.getUriForDownloadedFile(downloadId.toLong()) ?: continue val fileName = context.getFileName(uri) if (fileName == uuid && isUriValid(context, uri)) { return uri } } return null } private fun isUriValid(context: Context, uri: Uri): Boolean { return runCatching { context.contentResolver.openInputStream(uri)?.use { true } ?: false }.getOrDefault(false) } }
方案3:监听DownloadProvider变化更新缓存
注册ContentObserver,当DownloadProvider数据变化时清空缓存:
class FileParser(private val context: Context, private val pref: DefaultSharedPref) { private val listOfUris = mutableListOf<Uri>() private val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean) { super.onChange(selfChange) listOfUris.clear() } } init { context.contentResolver.registerContentObserver( Uri.parse("content://downloads/all_downloads"), true, observer ) } fun destroy() { context.contentResolver.unregisterContentObserver(observer) } // 原getUri方法保持不变 }
总结
优先选择方案1,直接通过文件路径生成FileProvider URI,彻底规避DownloadProvider的稳定性问题,同时简化逻辑,减少系统依赖引发的异常。
内容的提问来源于stack exchange,提问作者Andrii
相关产品推荐
相关产品推荐

