Android获取SD卡视频音频文件:TaoToken统一Key接入与权限适配实战
1. Android 13 扫描 SD 卡视频音频文件为什么总失败如果你在 Android 13 及以上版本里用以前那套MediaStore.Video.Media.DATA去拿 SD 卡视频音频文件路径大概率会遇到两种情况要么查出来是空的要么DATA列直接返回 null。这不是你代码写错了而是分区存储Scoped Storage把直接访问文件路径这条路收紧了。我先把结论说清楚Android 13 想扫描 SD 卡上的视频音频文件正确姿势是走MediaStore查询拿到content://形式的 Uri再用ContentResolver.openFileDescriptor或openInputStream去读内容。DATA列只在极少数兼容场景下还能用不能作为主方案。这个能力适合谁适合做本地媒体管理器、相册/播放器、文件清理工具以及需要给媒体文件补元数据、打标签的开发者。核心检索词先摆出来Android 获取 SD 卡视频音频文件本质是「运行时权限 MediaStore 查询 Uri 读取」三件事。很多人卡在第一步权限因为 Android 13 把READ_EXTERNAL_STORAGE拆成了READ_MEDIA_VIDEO、READ_MEDIA_AUDIO、READ_MEDIA_IMAGES三个细分权限。你只申请旧的存储权限系统不会给你媒体读取能力查询结果自然为空。再往下MediaStore是 Android 系统封装好的多媒体数据库音频、视频、图像都在这套接口里。你不需要自己建库拿到ContentResolver就能查。查询方法签名是query(uri, projection, selection, selectionArgs, sortOrder)返回一个Cursor。参数含义分别是要查哪张表、要哪些列、where 条件、占位符实参、排序方式。这里有个容易踩的坑EXTERNAL_CONTENT_URI在 Android 10 之后代表的是「所有外部存储卷的聚合视图」不是特指某张物理 SD 卡。如果你要精确区分内置存储和可移除 SD 卡需要用MediaStore.getExternalVolumeNames(context)拿到卷名再拼MediaStore.Video.Media.getContentUri(volumeName)。这一步是很多教程没讲透的地方。还有一个现实问题扫描出来的文件你往往想进一步做元数据补全或内容标签生成比如识别视频主题、给音频分类。这类模型调用如果每个功能都单独配一套 Key维护成本很高。我在实际项目里会把模型能力统一走一个 API 通道用同一套 Key 和 Base URL 管理减少配置散落。下面会结合扫描链路把这条接入路径也讲清楚让你从扫描到展示、再到标签生成一次跑通。2. TaoToken 统一 Key 前置准备与权限声明在写查询代码之前先把两件事准备好一是 Android 侧的权限声明二是模型通道的 Key 和 Base URL。前者决定你能不能读到 SD 卡媒体后者决定你扫描完之后能不能顺手做元数据补全。先说权限。Android 13 的媒体权限要按类型申请视频和音频分开。你可以在AndroidManifest.xml里这样声明uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- Android 12 及以下的兜底 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 /注意maxSdkVersion32这个属性它让旧权限只在 Android 12 及以下生效避免在新系统上申请一个已经被弱化的权限。运行时申请时按系统版本分流val permissions if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { arrayOf( Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_AUDIO ) } else { arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) } ActivityCompat.requestPermissions(this, permissions, REQ_MEDIA)如果你只读不写不要申请WRITE_EXTERNAL_STORAGE它在 Android 11 基本没意义还会让用户觉得你要动他的文件。再说模型通道。扫描出来的媒体文件标题、时长、分辨率这些系统字段能拿到但「这个视频讲了什么」「这段音频属于什么风格」系统给不了需要模型补。我习惯把这类调用统一到一个入口Key 和 Base URL 只配一次。TaoToken 的 API 地址是https://taotoken.net/api控制台里可以创建 Key文档在接入文档页。模型 ID 按你实际要用的填比如做文本标签生成就选对应的对话模型。这里要强调一个配置纪律Base URL、Key、Model ID 这三件套必须成套出现缺一个都会报错。后面第五节排障会专门讲这个。前置准备清单项目值说明Base URLhttps://taotoken.net/api统一入口不加多余路径API Key控制台创建建议按项目分 KeyModel ID按需选择元数据补全用对话模型权限READ_MEDIA_VIDEO/AUDIOAndroid 13 必申注意Key 不要硬编码进 APK放local.properties或服务端下发避免被反编译提取。3. 可复制配置MediaStore 查询与统一 Key 接入这一节给你可以直接抄的代码。先讲扫描再讲模型接入的配置片段。3.1 查询 SD 卡视频文件先拿卷名再查。这样能区分内置存储和可移除 SD 卡val resolver context.contentResolver val volumeNames MediaStore.getExternalVolumeNames(context) volumeNames.forEach { volume - val collection MediaStore.Video.Media.getContentUri(volume) val projection arrayOf( MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.DURATION, MediaStore.Video.Media.SIZE, MediaStore.Video.Media.DATE_MODIFIED ) resolver.query( collection, projection, null, null, ${MediaStore.Video.Media.DATE_MODIFIED} DESC )?.use { cursor - val idCol cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID) val nameCol cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME) while (cursor.moveToNext()) { val id cursor.getLong(idCol) val name cursor.getString(nameCol) val uri ContentUris.withAppendedId(collection, id) Log.d(MediaScan, video: $name - $uri) } } }关键点_ID是必须查的列因为你要用它拼出content://Uri。DISPLAY_NAME是文件名DURATION是时长毫秒。不要查DATA它在 Android 13 上不可靠。3.2 查询 SD 卡音频文件音频同理换 Uri 和列名val audioCollection MediaStore.Audio.Media.getContentUri(volume) val audioProjection arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION ) resolver.query( audioCollection, audioProjection, ${MediaStore.Audio.Media.IS_MUSIC} ! 0, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER )?.use { cursor - val titleCol cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE) while (cursor.moveToNext()) { Log.d(MediaScan, audio: ${cursor.getString(titleCol)}) } }IS_MUSIC ! 0这个条件能过滤掉系统提示音只留音乐类音频。如果你要全部音频把这个 selection 去掉即可。3.3 统一 Key 接入配置片段扫描完拿到文件列表后调模型做标签生成。配置我建议放一个 JSON 文件路径app/src/main/assets/model_config.json{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: 你的模型ID, timeout_seconds: 30 }读取时用context.assets.open(model_config.json)。如果你用 Gradle 注入也可以放local.properties再通过BuildConfig读。三件套对应关系再强调一次Base URL 填https://taotoken.net/apiKey 填控制台创建的Model ID 填你要用的模型标识。这三个值在请求头Authorization: Bearer Key和请求体model字段里分别用到。提示如果你做的是长期编码或 Agent 类任务可以考虑 Coding Plan 这类按周期计费的方式比单次调用更省心只是偶尔做标签生成用 API Key 按量即可。4. 验证请求从扫描到展示的闭环代码写完得在真机上验证。我按顺序给你验证步骤每一步都有预期结果。第一步装到 Android 13 真机首次启动会弹媒体权限。授权后在 Logcat 里过滤MediaScan标签。预期能看到类似video: IMG_20240101_120000.mp4 - content://media/external_primary/video/media/10023 audio: 某首歌.mp3如果一条都没有先别怀疑代码去系统设置里确认权限真的给了再看是不是 SD 卡没挂载。第二步验证 Uri 能读。拿到 Uri 后用openInputStream读前几个字节确认不是空流resolver.openInputStream(uri)?.use { input - val buffer ByteArray(16) val read input.read(buffer) Log.d(MediaScan, read $read bytes from $uri) }预期read大于 0。如果抛FileNotFoundException说明 Uri 拼错了检查_ID列有没有取到。第三步验证模型通道。用扫描到的文件名做一次元数据补全请求比如让模型根据文件名和时长生成标签。请求体大致是{ model: 你的模型ID, messages: [ {role: user, content: 根据文件名 IMG_20240101_120000.mp4 和时长 125 秒生成 3 个内容标签} ] }请求头带Authorization: Bearer 你的Key地址https://taotoken.net/api加对话路径。预期返回一段 JSONchoices[0].message.content里有标签文本。这一步跑通说明扫描到展示再到标签生成的闭环成立了。第四步把结果渲染到列表。用RecyclerView展示文件名、时长、标签。缩略图可以用ContentResolver.loadThumbnail(uri, Size(200, 200), null)Android 10 原生支持不用自己解码。实测下来整个链路最容易出问题的是权限和 Uri 拼接模型调用反而稳定。验证时建议先只跑扫描确认列表有数据再接模型分阶段排障更快。5. 本篇常见错排查401、local proxy failed、reading choices这一节按真实报错来。你遇到哪个直接对号入座。报错一401 Unauthorized。这是 Key 问题。检查三件事Key 是不是复制时带了空格请求头是不是Authorization: Bearer sk-xxx格式Bearer后面有一个空格Key 是不是在控制台被禁用或删除了。如果 Key 没问题看 Base URL 是不是写成了https://taotoken.net/api/多了一个斜杠或者漏了/api。三件套里 Base URL 和 Key 任一错位都会 401。报错二local proxy failed。这个报错通常出现在你本地配了网络代理工具但代理没启动或端口不对。先关掉本地代理再试。如果你在公司网络检查是不是有防火墙拦了出站请求。这个错和 Key 无关是网络链路问题。报错三reading choices 相关解析失败。典型表现是返回体里没有choices字段或者你按choices[0]取的时候越界。原因一般是请求体 JSON 格式错了比如messages写成了字符串或者模型 ID 填错服务端返回了错误结构。先打印完整响应体确认结构再解析。用JSONObject解析时加 try-catch别直接强转。报错四查询返回空 Cursor。分两种。Android 13 上大概率是权限没给全只给了视频没给音频或者反过来。另一种是EXTERNAL_CONTENT_URI查不到可移除 SD 卡需要改用getExternalVolumeNames拿卷名。还有个小概率情况SD 卡刚插入媒体库还没扫描完等几秒再查。报错五DATA 列返回 null。这不是 bug是预期行为。Android 10 分区存储下DATA对第三方应用不可见。改用_ID拼 Uri或者用openFileDescriptor读。如果你确实需要文件路径做特殊处理考虑用MediaStore的RELATIVE_PATH加DISPLAY_NAME自己拼一个展示用路径但不要拿它去File构造。报错六OAuth 相关错误。如果你用的是需要 OAuth 的通道检查 token 是否过期。统一 Key 模式下一般用静态 Key不涉及 OAuth 刷新。如果混用了两种鉴权方式确认请求头没冲突。排障顺序建议先确认权限再确认 Uri最后确认模型通道。每步用 Logcat 打点别一次改多处。6. 语义一致 CTA把扫描和模型能力接起来扫描 SD 卡视频音频文件这件事本身不复杂难的是权限适配和后续能力扩展。你现在手里应该有一套能跑的代码权限声明、MediaStore 查询、Uri 读取、模型标签生成。如果你要接着做元数据补全和内容标签建议把模型调用统一到一个入口Key 和 Base URL 只配一次。API Key 在控制台创建接入细节看接入文档。想先验证模型返回效果可以直接在模型对话里试一条请求确认结构对了再写进代码。长期做编码或 Agent 类任务Coding Plan 这种周期方式比按量更可控。最后留个实用技巧扫描大容量 SD 卡时别在主线程查 Cursor用Dispatchers.IO包起来查询结果分页加载一次查太多会卡 UI。缩略图用系统loadThumbnail别自己写解码器。这两点能省你不少性能调优时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →