从ContentProvider提取:处理相册、文件管理器返回的URI
好,咱们继续聊MediaExtractor。前面几章我们都在处理本地文件路径,也就是那种直接传一个/sdcard/video.mp4进去就能用的场景。但实际开发中,你从相册选视频、从文件管理器选文件,拿到的可不是文件路径,而是一个URI。
这个URI长什么样?大概是content://media/external/video/media/12345这样。你直接把这种URI扔给MediaExtractor的setDataSource(String)方法?嗯,它会直接报错。为什么?因为MediaExtractor不认识ContentProvider的URI协议。
我个人习惯把这个问题分成两个层面来解决:一是怎么拿到正确的文件描述符,二是怎么处理那些“不老实”的URI——比如Google Photos返回的那种临时URI。
URI的两种常见来源
先看看我们平时会碰到哪些URI。我整理了一下,大概分两类:
| 来源 | URI示例 | 特点 |
|---|---|---|
| 系统相册 | content://media/external/images/media/123 |
持久化URI,可重复打开 |
| 文件管理器 | content://com.android.externalstorage.documents/document/primary%3AVideo%2Ftest.mp4 |
临时授权,用完即失效 |
| Google Photos | content://com.google.android.apps.photos.contentprovider/... |
需要特殊处理,可能不支持直接fd |
你看,同样是content://开头,但背后的行为完全不同。我在项目中遇到过最坑的一次,就是用户从Google Photos选了一个视频,我直接用openFileDescriptor去拿fd,结果返回null。后来才发现,Google Photos返回的URI根本不允许你直接读文件描述符,你得先把它拷贝到应用缓存目录。
核心方案:通过ContentResolver获取文件描述符
说白了,处理ContentProvider URI的核心就一句话:不要直接传URI给MediaExtractor,而是通过ContentResolver拿到一个文件描述符(FileDescriptor),再传给MediaExtractor。
代码怎么写?看下面:
// 假设你从onActivityResult拿到了uri
Uri videoUri = data.getData();
// 通过ContentResolver打开文件描述符
ContentResolver resolver = getContentResolver();
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(videoUri, "r");
if (pfd != null) {
FileDescriptor fd = pfd.getFileDescriptor();
// 把fd传给MediaExtractor
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(fd);
// 别忘了用完关闭
// extractor.release();
// pfd.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
这里有个细节要注意:openFileDescriptor的第二个参数是访问模式,对于读取视频来说,传"r"就够了。别传"rw",有些ContentProvider会拒绝写入权限。
关键点:MediaExtractor的setDataSource(FileDescriptor)方法不会帮你关闭fd。你必须在提取完成后手动关闭ParcelFileDescriptor。否则会出现文件句柄泄漏,尤其是在循环处理多个视频时。
避坑指南:临时URI的处理
我曾经在做一个视频剪辑App时,遇到一个诡异的问题:用户从文件管理器选了一个视频,第一次提取成功了,第二次再选同一个视频就崩溃了。查了半天才发现,文件管理器返回的URI是临时授权的,一旦你的Activity处理完这次请求,授权就失效了。
怎么判断一个URI是不是临时的?你可以检查一下Intent的flag:
// 检查是否有临时读取权限
if ((intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
// 这是临时授权,建议立即拷贝到本地
copyUriToCache(videoUri);
}
对于临时URI,我的建议是:拿到URI后立刻拷贝到应用缓存目录,然后再用本地文件路径去创建MediaExtractor。这样既避免了授权过期的问题,也提高了后续读取速度。
小技巧:拷贝URI内容到本地时,别忘了检查文件大小。有些相册App返回的URI可能指向一个超大文件(比如4K视频),直接拷贝到缓存目录可能会撑爆你的应用缓存。我一般会先通过ContentResolver查询文件大小,超过100MB就提示用户或者走流式处理。
获取URI的元数据
有时候你不仅需要视频的二进制数据,还需要知道它的文件名、大小、MIME类型等信息。这些信息也可以通过ContentResolver从URI中查询出来:
// 查询URI的元数据
String[] projection = {
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.SIZE,
MediaStore.Video.Media.MIME_TYPE,
MediaStore.Video.Media.DURATION
};
Cursor cursor = resolver.query(videoUri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(0);
long fileSize = cursor.getLong(1);
String mimeType = cursor.getString(2);
long duration = cursor.getLong(3);
// 这些信息可以用来做后续处理
cursor.close();
}
你想想看,有了这些元数据,你就可以在提取之前先判断一下:这个视频是不是太大了?格式支不支持?时长是不是太短了?提前过滤掉不合适的视频,能省不少事。
完整的处理流程
我把整个流程画了一张图,方便你理解:
这张图把整个决策流程讲得很清楚了。核心就是先判断URI的授权类型,再决定是直接打开fd还是先拷贝到本地。我个人更倾向于统一先拷贝到缓存目录,虽然多了一步IO操作,但胜在稳定可靠,不会出现授权过期的问题。
关于权限的提醒
注意:从Android 10(API 29)开始,系统对存储权限的管理更加严格。即使你声明了READ_EXTERNAL_STORAGE权限,也不一定能直接读取所有URI。正确的做法是使用ActivityResultContracts.OpenDocument或ActivityResultContracts.GetContent来获取URI,系统会自动帮你处理好权限问题。
嗯,这里还要提一句:有些国产ROM对ContentProvider的实现有差异。我在某款手机上遇到过openFileDescriptor返回的fd是只读的,但MediaExtractor却需要可读可写(虽然它实际上只读)。解决方案是在打开fd时指定"r"模式,如果不行就试试"rw"——虽然这听起来有点奇怪,但确实能解决问题。
好了,关于从ContentProvider提取视频的内容就讲到这里。你只要记住:URI不是文件路径,别直接传;用ContentResolver拿fd;临时URI先拷贝到本地。这三点做到位了,基本上不会出大问题。
公众号:蓝海资料掘金营,微信deep3321