寫 WebRTC 的人都知道那句老話:Demo 跑通官方範例只要兩小時,上線除錯要花兩年。但在這塊算力被閹割 Snapdragon QM215 上,既要維持 7×24 活體人臉辨識,又要支援訪客即時視訊對講
一句話,相機不夠分
在低能門禁機的日常狀態下,相機是一條瘋狂運轉的高速輸送帶:RGB 與 ToF Sensor 以 30fps 採集畫面,C++ JNI 生成 3D 點雲,OpenGL ES 著色器繪製預覽,同時推入 AI 管線進行特徵比對。
系統本來跑得好好的,直到對講需求砸下來。
底層立刻引爆了致命衝突:Android 的 cameraserver 是排他性的,同一個 Camera ID 在同一瞬間只能被一個 Client 打開。AI 辨識霸佔相機不放,WebRTC 又急著搶相機,兩個互不相識的模組在底層硬碰硬。
最初的爛解法:sleep 5 秒
最初的工程師為了趕上線,採用了工程界最經典的「鴕鳥策略」:要對講就把 AI 相機徹底關掉;要回主畫面再把 WebRTC 相機徹底關掉。
於是在程式碼裡留下了這段令人哭笑不得的遺產:
// File: RecognitionFragment.java
DetectService.instance().stopDetection();
CameraProxy.getInstance().setCameraDeviceStateCallback(new ICameraInstance.CameraStateCallback() {
@Override
public void onClosed(int cameraId) {
if (cameraId == ICameraInstance.ALL) {
// 收到關閉通知後,在 UI 線程硬等 5 秒!
ThreadPoolManager.getInstance().runOnUiThreadDelay(() -> {
iSwitcher.switchTo(Const.MAIN_FRAGMENT, WebRTCFragment.class.getName());
}, 5000);
}
}
});
ThreadPoolManager.getInstance().execute(() -> {
CameraProxy.getInstance().close();
});
這段代碼是的「能跑就不要動」:
1. 去程 5 秒黑屏與 onClosed 的本質欺騙
訪客按鈴後螢幕黑屏整整 5 秒。為什麼不敢設 500ms?因為底層 CameraInstanceMK1.java 裡的 onClosed(ALL) 根本是個假訊號:
// File: CameraInstanceMK1.java
mRgbCamera.stopCameraSession();
mRgbCamera.close();
if (stateCallback != null) {
stateCallback.onClosed(ALL); // 剛發出關閉指令就立刻同步回呼!
}
onClosed 只代表「Java 層發出請求」,底層 Linux Kernel 設備節點根本還在收拾爛攤子。開發者不知道底層到底何時放手,只好展現老工程師的浪漫——「我不知道它何時關完,但我賭 5 秒後它肯定下班了」。
2. 回程 0 毫秒搶相機引發崩潰死鎖
更精彩的是掛斷通話時:
// File: WebRTCFragment.java
private void hangup() {
face3DWebRTC.hangup();
face3DWebRTC.release(); // 內部透過背景 CameraThread 非同步關相機
face3DWebRTC = null;
// 0 毫秒瞬間切回主畫面!
iSwitcher.switchTo(WebRTCFragment.class.getName(), Const.MAIN_FRAGMENT);
}
主線程 0ms 切回主畫面,RecognitionFragment.onResume() 秒呼 CameraProxy.open()。但 WebRTC 的背景執行緒甚至還沒向驅動說完再見,主畫面就一腳踹開大門進來搶相機。結果 cameraserver 當場噴出 CAMERA_IN_USE 異常,相機直接死鎖黑屏,整棟大樓的住戶站在門口跟你面面相覷,只能拔插頭重開機。
二、不可能,大家都這樣寫,肯定有優美的解法,那就看看androidx 怎麼處理
既然開關相機是一場豪賭,那我們老鳥最擅長的招數就來了:永遠不要關相機,假裝無事發生,在記憶體裡偷天換日動態轉發影格。

1. 消除 Data Race:帶有釋放保護的物件池
WebRTC MediaCodec 硬體編碼是非同步的。如果不做物件池,相機 33ms 吐一幀,WebRTC 40ms 才編完,下一幀會直接在編碼器嘴巴裡塞進新數據,遠端警衛看到的就不只是訪客,而是二維維度打擊般的現代藝術綠屏。透過 3 槽位物件池搭配 releaseCallback 實現精準管理:
public class SafeWebRtcFramePool {
private static final int POOL_SIZE = 3;
private final byte[][] buffers = new byte[POOL_SIZE][];
private final boolean[] inUse = new boolean[POOL_SIZE];
public synchronized byte[] acquire(int size) {
for (int i = 0; i < POOL_SIZE; i++) {
if (!inUse[i]) {
inUse[i] = true;
if (buffers[i] == null || buffers[i].length != size) {
buffers[i] = new byte[size];
}
return buffers[i];
}
}
return null; // 滿載時安全拋棄本幀,絕不踩踏
}
public synchronized void release(byte[] buffer) {
for (int i = 0; i < POOL_SIZE; i++) {
if (buffers[i] == buffer) {
inUse[i] = false;
break;
}
}
}
}
2. 被動式 WebRTC 外部採集器
public class ExternalVideoCapturer implements VideoCapturer {
private CapturerObserver capturerObserver;
private volatile boolean isCapturing = false;
private final SafeWebRtcFramePool framePool = new SafeWebRtcFramePool();
// ... 省略 initialize, startCapture, stopCapture 等 WebRTC 標準樣板代碼 ...
public void feedRawFrame(byte[] nv21Data, int width, int height, int rotation) {
if (!isCapturing || capturerObserver == null || nv21Data == null) return;
byte[] safeBuffer = framePool.acquire(nv21Data.length);
if (safeBuffer == null) return;
System.arraycopy(nv21Data, 0, safeBuffer, 0, nv21Data.length);
long timestampNs = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime());
// safeBuffer 綁定 releaseCallback:編碼完成後自動歸還物件池
NV21Buffer buffer = new NV21Buffer(safeBuffer, width, height, () -> {
framePool.release(safeBuffer);
});
VideoFrame videoFrame = new VideoFrame(buffer, rotation, timestampNs);
capturerObserver.onFrameCaptured(videoFrame);
videoFrame.release();
}
}
3. 分發器路由與 ToF 發熱控制
public class CameraFrameDispatcher {
public enum Mode { AI_DETECTION, WEBRTC_INTERCOM }
private static final CameraFrameDispatcher INSTANCE = new CameraFrameDispatcher();
public static CameraFrameDispatcher getInstance() { return INSTANCE; }
private volatile Mode mode = Mode.AI_DETECTION;
private volatile ExternalVideoCapturer webrtcCapturer;
private CameraFrameDispatcher() {}
public synchronized void switchToWebRTC(ExternalVideoCapturer capturer) {
this.webrtcCapturer = capturer;
this.mode = Mode.WEBRTC_INTERCOM;
CameraInstanceMK1.getInstance().closeTofOnly(); // 關閉 ToF 抑制發熱
}
public synchronized void switchToAI() {
this.mode = Mode.AI_DETECTION;
this.webrtcCapturer = null;
CameraInstanceMK1.getInstance().openTofOnly(); // 恢復 ToF 供辨識
}
public Mode getMode() { return mode; }
public void dispatch(byte[] yuvData, int width, int height, int rotation) {
if (mode == Mode.WEBRTC_INTERCOM && webrtcCapturer != null && yuvData != null) {
webrtcCapturer.feedRawFrame(yuvData, width, height, rotation);
}
}
}
4. AI 管線短路與 BufferQueue 阻塞防護
在 AI 容器頂端短路,杜絕通話時的 CPU 浪費;離開畫面時掛載空轉監聽器,防止 Android GraphicBuffer 阻塞死鎖:
// File: CameraFrameContainerMK1.java (短路保護)
producer.setFrameAvailableCallback((byte[] color, byte[] raw, byte[] depth) -> {
CameraFrameDispatcher dispatcher = CameraFrameDispatcher.getInstance();
if (dispatcher.getMode() == CameraFrameDispatcher.Mode.WEBRTC_INTERCOM) {
if (color != null) {
dispatcher.dispatch(color, mCamera.getCroppedFrameWidth(),
mCamera.getCroppedFrameHeight(), mCamera.getColorRotate());
}
return; // 跳過所有 ToF 點雲解析與 AI 比對
}
tofRawToPclIrIrz(raw);
// ...
});
// File: CameraInstanceMK1.java (防止 BufferQueue 假死)
public void destroyViewOnly() {
mView = null;
if (mSurfaceTexture != null) {
// 維持空轉監聽:持續消耗 GraphicBuffer,確保 Camera HAL 管線暢通不卡死
mSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
try { surfaceTexture.updateTexImage(); } catch (Exception ignored) {}
});
}
}
三、寫完覺得哪裡怪怪的 Frame Dispatcher 代價
架構圖在會議室投影幕上看起來都很風光,但回到座位只有自己知道背後的血淚:
1. 優點
- 0 毫秒切換:相機從不關閉,進出對講皆為 0ms,消滅
CAMERA_IN_USE併發死鎖;
2. 代價
- CPU 記憶體拷貝開銷:相機 NV21 數據每次需經
System.arraycopy(約 460KB)灌入物件池,單次耗時 0.3ms,在閹割版晶片上多佔用微量頻寬;
沒有銀彈,只有妥協
業界現代標準(如 Android Jetpack CameraX 或 WebRTC 原生 rtc::VideoBroadcaster)本質上皆為單一相機源、多消費者分發體系。
軟體工程裡沒有銀彈,只有權衡:
- 5 秒睡眠重啟 雖然丟人,卻用最低成本換來了模組隔離與快速交付;
- 影格分發器 雖然優雅且徹底解決了延遲與死鎖,卻高度考驗工程師對 Android 底層 GraphicBuffer、EGL 與非同步記憶體管理的駕馭力。
參考文獻與源碼追蹤
- Android Open Source Project:
frameworks/av/services/camera/libcameraservice/ - Google WebRTC Source Code:
webrtc/src/media/base/videobroadcaster.h - Android Jetpack CameraX Architecture Guide: Multi-use-case Binding Guidelines
- Linux Kernel V4L2 Subsystem: Single-opener vs Multi-streamer Drivers Specifications

發佈留言