1. Android13 Camera2 多流输出适配:OutputConfiguration 与 Stream usecase 到底解决了什么问题
如果你在做 Android Camera2 开发,大概率遇到过这种场景:预览要一路流、拍照要一路流、录像还要一路流,三路流同时开的时候,要么帧率掉得厉害,要么某一路直接创建失败。Android 13 之前,我们能控制的只有 Surface 的尺寸和格式,至于「这路流到底是给预览用的还是给录像用的」,系统并不知道,只能靠 HAL 自己猜。猜错了,功耗和延迟就上去了。
Android 13 在 Camera2 里补上了这块拼图,核心是两个东西:OutputConfiguration和Stream usecase。OutputConfiguration 是 Android 10 就引入的,但 Android 13 给它加了 Mirror、Timestamp base、Dynamic range profile 这些新能力;Stream usecase 则是 Android 13 真正落地的一个「语义标签」机制,让你告诉底层「这路流是 PREVIEW、STILL_CAPTURE 还是 VIDEO_RECORD」。
打个比方:以前的 Surface 就像寄快递只写地址不写物品类型,快递员只能按默认方式处理;现在你可以标注「易碎」「冷藏」,物流系统就能提前分配对应的资源。Stream usecase 就是这个「物品类型标签」,它直接影响 ISP、Scaler 的资源分配策略。
这篇面向的是已经在用 Camera2、准备在 Android 13 设备上适配多流输出的开发者。我会给出可直接复制的 OutputConfiguration 配置骨架、Stream usecase 的设置方式,以及在真机上验证流组合是否生效的具体步骤。涉及的关键检索词包括 Android13 Camera2 OutputConfiguration 配置、Stream usecase 设置、SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS 查询等,都会在代码里体现。
需要先明确一点:Stream usecase 不是所有设备都支持。你得先查REQUEST_AVAILABLE_CAPABILITIES里有没有REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE,没有的话设了也白设,系统会忽略。这个判断逻辑我会在第三节的代码里写清楚。
另外,多流组合不是随便配的。Android 13 提供了SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS这个静态属性,它告诉你「哪些 usecase 组合是设备一定支持的」。你按它给的组合去配,成功率最高;自己乱配,可能创建 Session 时直接抛异常。这是本篇要重点讲的部分。
2. TaoToken 前置准备:用模型对话快速核对 Camera2 API 签名与常量
Camera2 的 API 签名和常量值经常记混,尤其是 Android 13 新增的这一批。我自己的做法是,在写代码前先用模型对话把关键 API 的签名和常量对照一遍,避免编译期才发现参数类型不对。
TaoToken 的模型对话入口在这里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。你可以直接问它「Android 13 OutputConfiguration setStreamUseCase 的参数类型是什么」「SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD 的常量值是多少」这类问题,它会给出对应的 API 说明。
为什么要在写 Camera2 代码前做这一步?因为 Android 13 的 Camera2 新增 API 有几个坑:
第一,setStreamUseCase(long)的参数是 long,不是 int。很多人习惯性写 int,编译不过。这个 long 值来自SCALER_AVAILABLE_STREAM_USE_CASES_*常量。
第二,setDynamicRangeProfile(long)也是 long,而且它和 output format 强绑定——只有ImageFormat.YCBCR_P010或ImageFormat.PRIVATE才能设 10bit HDR profile。你如果拿一个 YUV_420_888 的 Surface 去设 HLG10,运行时会报错。
第三,setMirrorMode(int)只影响 Buffer 的 Transform matrix,不会真的去翻转像素数据。这个语义如果理解错了,后面显示方向对不上会排查很久。
用模型对话把这些签名和约束先过一遍,比直接翻 AOSP 源码快得多。我试过把一段报错的堆栈贴进去问,它能定位到是哪个 setter 的参数类型或取值不对。
拿到确认后的 API 信息,再回到 Android Studio 里写代码,编译一次过的概率会高很多。这一步不涉及任何环境配置,就是纯查证,几分钟的事。
如果你后面要做的是长期编码或者 Agent 类的自动化任务,可以考虑 Coding Plan:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。但就本篇这个场景,模型对话足够用了。
3. 可复制配置:OutputConfiguration 与 Stream usecase 代码骨架
这一节给出完整的配置代码。核心思路是:先查设备支持哪些 usecase,再按 mandatory 组合去配 OutputConfiguration,最后创建 Session。
先看能力查询部分。这段代码判断设备是否支持 Stream usecase,并拿到支持的 usecase 列表:
// 查询设备是否支持 Stream usecase CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); int[] capabilities = characteristics.get( CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); boolean supportStreamUseCase = false; if (capabilities != null) { for (int cap : capabilities) { if (cap == CameraCharacteristics .REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE) { supportStreamUseCase = true; break; } } } // 拿到当前 Camera 支持的 stream usecase 列表 long[] availableUseCases = null; if (supportStreamUseCase) { availableUseCases = characteristics.get( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES); }接下来是查询 mandatory 组合。这个属性返回的是一个 long 数组,每两个一组表示一个组合,或者按文档定义的编码方式解析。实际使用时,最稳妥的做法是遍历它,找到包含你需要的 usecase 的组合:
// 查询设备一定支持的 usecase 组合 long[] mandatoryCombinations = characteristics.get( CameraCharacteristics.SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS);然后是核心的 OutputConfiguration 配置。假设我们要配三路流:预览、拍照、录像。每路流创建一个 OutputConfiguration,设置对应的 usecase:
// 预览流:尺寸 1920x1080,PRIVATE 格式 SurfaceTexture previewTexture = new SurfaceTexture(0); previewTexture.setDefaultBufferSize(1920, 1080); Surface previewSurface = new Surface(previewTexture); OutputConfiguration previewConfig = new OutputConfiguration(previewSurface); if (supportStreamUseCase) { previewConfig.setStreamUseCase( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW); } // 拍照流:尺寸 4032x3024,JPEG 格式 ImageReader stillReader = ImageReader.newInstance( 4032, 3024, ImageFormat.JPEG, 2); OutputConfiguration stillConfig = new OutputConfiguration( stillReader.getSurface()); if (supportStreamUseCase) { stillConfig.setStreamUseCase( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE); } // 录像流:尺寸 1920x1080,PRIVATE 格式 MediaRecorder recorder = new MediaRecorder(); // ... recorder 配置省略 ... OutputConfiguration recordConfig = new OutputConfiguration( recorder.getSurface()); if (supportStreamUseCase) { recordConfig.setStreamUseCase( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD); }如果你要做 10bit HDR 输出,需要额外设置 dynamic range profile,并且 format 必须是 YCBCR_P010 或 PRIVATE:
// 10bit HDR 输出流 ImageReader hdrReader = ImageReader.newInstance( 1920, 1080, ImageFormat.YCBCR_P010, 2); OutputConfiguration hdrConfig = new OutputConfiguration( hdrReader.getSurface()); if (supportStreamUseCase) { hdrConfig.setStreamUseCase( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD); } // 设置 HDR profile,需先确认设备支持 DynamicRangeProfiles profiles = characteristics.get( CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES); if (profiles != null && profiles.getSupportedProfiles() .contains(DynamicRangeProfiles.HLG10)) { hdrConfig.setDynamicRangeProfile(DynamicRangeProfiles.HLG10); }最后创建 Session。注意这里用的是SessionConfiguration,它接受 OutputConfiguration 列表:
List<OutputConfiguration> outputConfigs = new ArrayList<>(); outputConfigs.add(previewConfig); outputConfigs.add(stillConfig); outputConfigs.add(recordConfig); SessionConfiguration sessionConfig = new SessionConfiguration( SessionConfiguration.SESSION_REGULAR, outputConfigs, new HandlerExecutor(backgroundHandler), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // Session 创建成功,可以下发请求了 } @Override public void onConfigureFailed(CameraCaptureSession session) { // 配置失败,检查流组合是否被支持 } }); cameraDevice.createCaptureSession(sessionConfig);这段代码里,setStreamUseCase和setDynamicRangeProfile都做了能力判断,不支持就跳过,不会因为设了不支持的 usecase 而崩溃。这是适配多机型的关键。
4. 真机验证:确认流组合与输出配置是否生效
代码写完只是第一步,真正要确认的是「设备到底认不认你配的 usecase」。这一节给出真机验证的具体步骤。
第一步,打印设备支持的 usecase 列表。在onOpened回调里加日志:
long[] useCases = characteristics.get( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES); if (useCases != null) { for (long uc : useCases) { Log.d(TAG, "supported usecase: " + Long.toHexString(uc)); } }对照日志里的值,确认你用的PREVIEW、STILL_CAPTURE、VIDEO_RECORD是否在列表里。如果某个不在,说明这台设备不支持该 usecase,你设了也会被忽略。
第二步,验证 Session 是否创建成功。如果onConfigureFailed被调用,大概率是流组合不被支持。这时候去查SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS,看你的组合是否在 mandatory 列表里。不在的话,换一个 mandatory 支持的组合再试。
第三步,验证 usecase 是否真的生效。最直接的方法是抓CaptureResult,看CaptureResult里有没有对应的 usecase 回传。不过更实用的方法是看功耗和帧率:设置VIDEO_RECORDusecase 后,录像流的帧率应该更稳定,掉帧更少;设置PREVIEWusecase 后,预览延迟应该更低。
第四步,验证 Mirror 和 Timestamp base。Mirror 只影响 Transform matrix,你可以通过OutputConfiguration.getMirrorMode()确认设置是否被接受。Timestamp base 则可以通过对比不同流的 timestamp 来验证:
// 在 onCaptureCompleted 里打印 timestamp Log.d(TAG, "stream timestamp: " + result.get(CaptureResult.SENSOR_TIMESTAMP));如果设置了TIMESTAMP_BASE_SENSOR,那 timestamp 应该和 sensor 的时间基准一致;设置TIMESTAMP_BASE_REALTIME则和系统实时时钟对齐。
第五步,验证 10bit HDR。设置DynamicRangeProfile.HLG10后,检查输出 buffer 的 format 是否为YCBCR_P010。如果是,说明 HDR 流配置生效了。同时可以对比 HDR 和 SDR 流的画面亮度范围,HDR 流的高光细节应该更丰富。
实测下来,最容易出问题的是流组合。很多设备虽然支持单个 usecase,但不支持你想要的组合。所以第三步的 mandatory 组合查询一定要做,别跳过。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
这一节整理几个在配置过程中可能遇到的报错,以及对应的排查方向。
报错一:IllegalArgumentException: stream use case not supported
这个报错通常出现在setStreamUseCase时传了一个设备不支持的 usecase。排查方法:先打印SCALER_AVAILABLE_STREAM_USE_CASES,确认你用的常量在列表里。如果不在,就不要设,或者换一个支持的。
报错二:onConfigureFailed被调用,但没有明确异常信息
这是流组合不被支持。排查方法:查SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS,把你的组合和 mandatory 列表对比。如果组合不在列表里,尝试减少流数量,或者换用 mandatory 支持的组合。
报错三:local proxy failed或网络请求相关错误
如果你在查 API 文档或调用模型对话时遇到local proxy failed,先检查网络配置。TaoToken 的 API 入口是 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,确认请求地址没有拼错。如果是 401,说明 API Key 无效或过期,去 API Keys 页面重新生成:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。
报错四:reading choices相关错误
这个通常出现在解析模型返回结果时。如果你用模型对话查 API 签名,返回的 JSON 里choices字段解析失败,检查一下请求的 model 参数是否正确,以及返回内容是否被截断。
报错五:OAuth 相关错误
如果你用的是需要 OAuth 的接入方式,检查 token 是否过期。OAuth 流程的配置可以参考接入文档:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。
报错六:setDynamicRangeProfile抛异常
这个报错的原因是 output format 不是YCBCR_P010或PRIVATE。排查方法:检查创建 ImageReader 时用的 format,必须是这两个之一才能设 HDR profile。
报错七:Mirror 设置后画面方向不对
Mirror 只影响 Transform matrix,不会翻转像素。如果你在显示端没有正确处理 Transform matrix,画面方向就会不对。排查方法:检查显示端的 matrix 应用逻辑,确保它读取了 OutputConfiguration 的 mirror mode。
6. 语义一致 CTA:继续深入 Camera2 与 Android13 适配
Camera2 的适配工作,很多时候卡在「设备支持什么」和「我配了什么」之间的信息差上。Android 13 的 OutputConfiguration 和 Stream usecase 把一部分控制权交回给了开发者,但也要求开发者更清楚设备的能力边界。
如果你在配置过程中需要反复核对 API 签名、常量值、报错含义,用模型对话会省很多时间:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。把报错堆栈贴进去,它能帮你定位到具体的 setter 或参数。
需要生成 API Key 的话,入口在这里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。接入文档里有完整的请求示例和参数说明:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。
最后给一个实用建议:在真机验证时,先把SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS打印出来,按它给的组合去配,成功率最高。自己组合的流,即使单个 usecase 都支持,也可能因为资源冲突而创建失败。这个属性是 Android 13 给开发者的「安全组合清单」,别浪费它。