戰(zhàn))
1. Android13 Camera2 多流輸出適配OutputConfiguration 與 Stream usecase 到底解決了什么問題如果你在做 Android Camera2 開發(fā)大概率遇到過這種場景預(yù)覽要一路流、拍照要一路流、錄像還要一路流三路流同時開的時候要么幀率掉得厲害要么某一路直接創(chuàng)建失敗。Android 13 之前我們能控制的只有 Surface 的尺寸和格式至于「這路流到底是給預(yù)覽用的還是給錄像用的」系統(tǒng)并不知道只能靠 HAL 自己猜。猜錯了功耗和延遲就上去了。Android 13 在 Camera2 里補(bǔ)上了這塊拼圖核心是兩個東西OutputConfiguration和Stream usecase。OutputConfiguration 是 Android 10 就引入的但 Android 13 給它加了 Mirror、Timestamp base、Dynamic range profile 這些新能力Stream usecase 則是 Android 13 真正落地的一個「語義標(biāo)簽」機(jī)制讓你告訴底層「這路流是 PREVIEW、STILL_CAPTURE 還是 VIDEO_RECORD」。打個比方以前的 Surface 就像寄快遞只寫地址不寫物品類型快遞員只能按默認(rèn)方式處理現(xiàn)在你可以標(biāo)注「易碎」「冷藏」物流系統(tǒng)就能提前分配對應(yīng)的資源。Stream usecase 就是這個「物品類型標(biāo)簽」它直接影響 ISP、Scaler 的資源分配策略。這篇面向的是已經(jīng)在用 Camera2、準(zhǔn)備在 Android 13 設(shè)備上適配多流輸出的開發(fā)者。我會給出可直接復(fù)制的 OutputConfiguration 配置骨架、Stream usecase 的設(shè)置方式以及在真機(jī)上驗(yàn)證流組合是否生效的具體步驟。涉及的關(guān)鍵檢索詞包括 Android13 Camera2 OutputConfiguration 配置、Stream usecase 設(shè)置、SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS 查詢等都會在代碼里體現(xiàn)。需要先明確一點(diǎn)Stream usecase 不是所有設(shè)備都支持。你得先查REQUEST_AVAILABLE_CAPABILITIES里有沒有REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE沒有的話設(shè)了也白設(shè)系統(tǒng)會忽略。這個判斷邏輯我會在第三節(jié)的代碼里寫清楚。另外多流組合不是隨便配的。Android 13 提供了SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS這個靜態(tài)屬性它告訴你「哪些 usecase 組合是設(shè)備一定支持的」。你按它給的組合去配成功率最高自己亂配可能創(chuàng)建 Session 時直接拋異常。這是本篇要重點(diǎn)講的部分。2. TaoToken 前置準(zhǔn)備用模型對話快速核對 Camera2 API 簽名與常量Camera2 的 API 簽名和常量值經(jīng)常記混尤其是 Android 13 新增的這一批。我自己的做法是在寫代碼前先用模型對話把關(guān)鍵 API 的簽名和常量對照一遍避免編譯期才發(fā)現(xiàn)參數(shù)類型不對。TaoToken 的模型對話入口在這里https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。你可以直接問它「Android 13 OutputConfiguration setStreamUseCase 的參數(shù)類型是什么」「SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD 的常量值是多少」這類問題它會給出對應(yīng)的 API 說明。為什么要在寫 Camera2 代碼前做這一步因?yàn)?Android 13 的 Camera2 新增 API 有幾個坑第一setStreamUseCase(long)的參數(shù)是 long不是 int。很多人習(xí)慣性寫 int編譯不過。這個 long 值來自SCALER_AVAILABLE_STREAM_USE_CASES_*常量。第二setDynamicRangeProfile(long)也是 long而且它和 output format 強(qiáng)綁定——只有ImageFormat.YCBCR_P010或ImageFormat.PRIVATE才能設(shè) 10bit HDR profile。你如果拿一個 YUV_420_888 的 Surface 去設(shè) HLG10運(yùn)行時會報(bào)錯。第三setMirrorMode(int)只影響 Buffer 的 Transform matrix不會真的去翻轉(zhuǎn)像素?cái)?shù)據(jù)。這個語義如果理解錯了后面顯示方向?qū)Σ簧蠒挪楹芫?。用模型對話把這些簽名和約束先過一遍比直接翻 AOSP 源碼快得多。我試過把一段報(bào)錯的堆棧貼進(jìn)去問它能定位到是哪個 setter 的參數(shù)類型或取值不對。拿到確認(rèn)后的 API 信息再回到 Android Studio 里寫代碼編譯一次過的概率會高很多。這一步不涉及任何環(huán)境配置就是純查證幾分鐘的事。如果你后面要做的是長期編碼或者 Agent 類的自動化任務(wù)可以考慮 Coding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。但就本篇這個場景模型對話足夠用了。3. 可復(fù)制配置OutputConfiguration 與 Stream usecase 代碼骨架這一節(jié)給出完整的配置代碼。核心思路是先查設(shè)備支持哪些 usecase再按 mandatory 組合去配 OutputConfiguration最后創(chuàng)建 Session。先看能力查詢部分。這段代碼判斷設(shè)備是否支持 Stream usecase并拿到支持的 usecase 列表// 查詢設(shè)備是否支持 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; } } } // 拿到當(dāng)前 Camera 支持的 stream usecase 列表 long[] availableUseCases null; if (supportStreamUseCase) { availableUseCases characteristics.get( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES); }接下來是查詢 mandatory 組合。這個屬性返回的是一個 long 數(shù)組每兩個一組表示一個組合或者按文檔定義的編碼方式解析。實(shí)際使用時最穩(wěn)妥的做法是遍歷它找到包含你需要的 usecase 的組合// 查詢設(shè)備一定支持的 usecase 組合 long[] mandatoryCombinations characteristics.get( CameraCharacteristics.SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS);然后是核心的 OutputConfiguration 配置。假設(shè)我們要配三路流預(yù)覽、拍照、錄像。每路流創(chuàng)建一個 OutputConfiguration設(shè)置對應(yīng)的 usecase// 預(yù)覽流尺寸 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 輸出需要額外設(shè)置 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); } // 設(shè)置 HDR profile需先確認(rèn)設(shè)備支持 DynamicRangeProfiles profiles characteristics.get( CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES); if (profiles ! null profiles.getSupportedProfiles() .contains(DynamicRangeProfiles.HLG10)) { hdrConfig.setDynamicRangeProfile(DynamicRangeProfiles.HLG10); }最后創(chuàng)建 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 創(chuàng)建成功可以下發(fā)請求了 } Override public void onConfigureFailed(CameraCaptureSession session) { // 配置失敗檢查流組合是否被支持 } }); cameraDevice.createCaptureSession(sessionConfig);這段代碼里setStreamUseCase和setDynamicRangeProfile都做了能力判斷不支持就跳過不會因?yàn)樵O(shè)了不支持的 usecase 而崩潰。這是適配多機(jī)型的關(guān)鍵。4. 真機(jī)驗(yàn)證確認(rèn)流組合與輸出配置是否生效代碼寫完只是第一步真正要確認(rèn)的是「設(shè)備到底認(rèn)不認(rèn)你配的 usecase」。這一節(jié)給出真機(jī)驗(yàn)證的具體步驟。第一步打印設(shè)備支持的 usecase 列表。在onOpened回調(diào)里加日志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)); } }對照日志里的值確認(rèn)你用的PREVIEW、STILL_CAPTURE、VIDEO_RECORD是否在列表里。如果某個不在說明這臺設(shè)備不支持該 usecase你設(shè)了也會被忽略。第二步驗(yàn)證 Session 是否創(chuàng)建成功。如果onConfigureFailed被調(diào)用大概率是流組合不被支持。這時候去查SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS看你的組合是否在 mandatory 列表里。不在的話換一個 mandatory 支持的組合再試。第三步驗(yàn)證 usecase 是否真的生效。最直接的方法是抓CaptureResult看CaptureResult里有沒有對應(yīng)的 usecase 回傳。不過更實(shí)用的方法是看功耗和幀率設(shè)置VIDEO_RECORDusecase 后錄像流的幀率應(yīng)該更穩(wěn)定掉幀更少設(shè)置PREVIEWusecase 后預(yù)覽延遲應(yīng)該更低。第四步驗(yàn)證 Mirror 和 Timestamp base。Mirror 只影響 Transform matrix你可以通過OutputConfiguration.getMirrorMode()確認(rèn)設(shè)置是否被接受。Timestamp base 則可以通過對比不同流的 timestamp 來驗(yàn)證// 在 onCaptureCompleted 里打印 timestamp Log.d(TAG, stream timestamp: result.get(CaptureResult.SENSOR_TIMESTAMP));如果設(shè)置了TIMESTAMP_BASE_SENSOR那 timestamp 應(yīng)該和 sensor 的時間基準(zhǔn)一致設(shè)置TIMESTAMP_BASE_REALTIME則和系統(tǒng)實(shí)時時鐘對齊。第五步驗(yàn)證 10bit HDR。設(shè)置DynamicRangeProfile.HLG10后檢查輸出 buffer 的 format 是否為YCBCR_P010。如果是說明 HDR 流配置生效了。同時可以對比 HDR 和 SDR 流的畫面亮度范圍HDR 流的高光細(xì)節(jié)應(yīng)該更豐富。實(shí)測下來最容易出問題的是流組合。很多設(shè)備雖然支持單個 usecase但不支持你想要的組合。所以第三步的 mandatory 組合查詢一定要做別跳過。5. 本篇常見報(bào)錯排查401、local proxy failed、reading choices、OAuth這一節(jié)整理幾個在配置過程中可能遇到的報(bào)錯以及對應(yīng)的排查方向。報(bào)錯一IllegalArgumentException: stream use case not supported這個報(bào)錯通常出現(xiàn)在setStreamUseCase時傳了一個設(shè)備不支持的 usecase。排查方法先打印SCALER_AVAILABLE_STREAM_USE_CASES確認(rèn)你用的常量在列表里。如果不在就不要設(shè)或者換一個支持的。報(bào)錯二onConfigureFailed被調(diào)用但沒有明確異常信息這是流組合不被支持。排查方法查SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS把你的組合和 mandatory 列表對比。如果組合不在列表里嘗試減少流數(shù)量或者換用 mandatory 支持的組合。報(bào)錯三local proxy failed或網(wǎng)絡(luò)請求相關(guān)錯誤如果你在查 API 文檔或調(diào)用模型對話時遇到local proxy failed先檢查網(wǎng)絡(luò)配置。TaoToken 的 API 入口是 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 確認(rèn)請求地址沒有拼錯。如果是 401說明 API Key 無效或過期去 API Keys 頁面重新生成https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。報(bào)錯四reading choices相關(guān)錯誤這個通常出現(xiàn)在解析模型返回結(jié)果時。如果你用模型對話查 API 簽名返回的 JSON 里choices字段解析失敗檢查一下請求的 model 參數(shù)是否正確以及返回內(nèi)容是否被截?cái)?。?bào)錯五OAuth 相關(guān)錯誤如果你用的是需要 OAuth 的接入方式檢查 token 是否過期。OAuth 流程的配置可以參考接入文檔https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。報(bào)錯六setDynamicRangeProfile拋異常這個報(bào)錯的原因是 output format 不是YCBCR_P010或PRIVATE。排查方法檢查創(chuàng)建 ImageReader 時用的 format必須是這兩個之一才能設(shè) HDR profile。報(bào)錯七M(jìn)irror 設(shè)置后畫面方向不對Mirror 只影響 Transform matrix不會翻轉(zhuǎn)像素。如果你在顯示端沒有正確處理 Transform matrix畫面方向就會不對。排查方法檢查顯示端的 matrix 應(yīng)用邏輯確保它讀取了 OutputConfiguration 的 mirror mode。6. 語義一致 CTA繼續(xù)深入 Camera2 與 Android13 適配Camera2 的適配工作很多時候卡在「設(shè)備支持什么」和「我配了什么」之間的信息差上。Android 13 的 OutputConfiguration 和 Stream usecase 把一部分控制權(quán)交回給了開發(fā)者但也要求開發(fā)者更清楚設(shè)備的能力邊界。如果你在配置過程中需要反復(fù)核對 API 簽名、常量值、報(bào)錯含義用模型對話會省很多時間https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。把報(bào)錯堆棧貼進(jìn)去它能幫你定位到具體的 setter 或參數(shù)。需要生成 API Key 的話入口在這里https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。接入文檔里有完整的請求示例和參數(shù)說明https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。最后給一個實(shí)用建議在真機(jī)驗(yàn)證時先把SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS打印出來按它給的組合去配成功率最高。自己組合的流即使單個 usecase 都支持也可能因?yàn)橘Y源沖突而創(chuàng)建失敗。這個屬性是 Android 13 給開發(fā)者的「安全組合清單」別浪費(fèi)它。