
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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。你可以直接问它「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 Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。但就本篇这个场景模型对话足够用了。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// 预览流尺寸 1920x1080PRIVATE 格式 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); } // 拍照流尺寸 4032x3024JPEG 格式 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); } // 录像流尺寸 1920x1080PRIVATE 格式 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 列表ListOutputConfiguration 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 确认请求地址没有拼错。如果是 401说明 API Key 无效或过期去 API Keys 页面重新生成https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。报错四reading choices相关错误这个通常出现在解析模型返回结果时。如果你用模型对话查 API 签名返回的 JSON 里choices字段解析失败检查一下请求的 model 参数是否正确以及返回内容是否被截断。报错五OAuth 相关错误如果你用的是需要 OAuth 的接入方式检查 token 是否过期。OAuth 流程的配置可以参考接入文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。报错六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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。把报错堆栈贴进去它能帮你定位到具体的 setter 或参数。需要生成 API Key 的话入口在这里https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。接入文档里有完整的请求示例和参数说明https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。最后给一个实用建议在真机验证时先把SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS打印出来按它给的组合去配成功率最高。自己组合的流即使单个 usecase 都支持也可能因为资源冲突而创建失败。这个属性是 Android 13 给开发者的「安全组合清单」别浪费它。