在疫情爆發的2020年,公司的Android項目要求支持外置攝像頭,即要求支持USB攝像頭;一臉懵逼的我從來沒聽過Android設備能支持USB攝像頭的,只知道Android大機器能接外置的攝像頭,但插口是接在Android機器上的前置和後置接口,也就是說可以通過Android自帶的Camera類或Camera2類中API直接調用的;然而項目要的是在包含前置和後置攝像頭之後,還要有USB攝像頭,懵逼的我立馬上網找這方面的資料,找到了UVCCamera和openCV,但下了openCV的demo之後並沒有任何關於USB攝像頭的玩意,轉戰UVCCamera;瞭解了UVCCamera之後,還要實現通過UVCCamera取流並給到webrtc
注:閱讀本篇需要簡單瞭解webrtc如何通過**Capturer類產生視頻流並轉化
一、UVCCamera下拉與編譯
1、UVCCamra下拉
UVCCamera的github地址爲:https://github.com/saki4510t/UVCCamera.git
2、UVCCamera編譯和demo測試
首先,下載了UVCCamera並且通過AS打開之後,你一定非常的迫不及待的想要看看USB攝像頭使用起來是怎麼樣的;但問題卻來了
2.1、一直在sync不好
原因:網絡問題
解決:將distributionUrl地址改成本機已有的版本,或者通過瀏覽器下載並放到系統android/.gradle文件夾中,具體文件夾地址可另外自行百度存放
2.2、Process 'command '.......\Android\sdk\ndk-bundle/ndk-build.cmd'' finished with non-zero exit value 2
原因:ndk版本不兼容,這項目感覺有點老,作者又不更新,無奈自己能力也沒那麼強,也不會改,那麼只能將ndk版本降低,降低到ndk r16b或者ndk r14b,下載地址爲:https://developer.android.google.cn/ndk/downloads/older_releases;無奈這個官網地址上已無法下載太舊的ndk了,那麼只能選擇從AS中獲取,好在AS最低有支持到ndk16的,要是以後AS也把ndk16給過濾掉,那我也暫時不知道怎麼辦了
下載好ndk之後,將UVCCamera的NDK版本改成16的這個:
2.3、無法出畫面
若出現所有的test demo都能夠打開,但無法出現畫面,Logcat日誌有:[2495*UVCCamera.cpp:172:connect]:could not open camera:err=-1。之類的,則將libuvccamera/src/main/jni/Application.mk中的
NDK_TOOLCHAIN_VERSION := 4.9
註釋打開
2.4、運行調試過程當中出現[5050*UVCPreview.cpp:507:prepare_preview]:could not negotiate with camera:err=-51
可能是uvcCamera.setPreviewSize的預覽格式不正確,更改預覽格式
二、VideoCapturer的子類
在使用webrtc視頻通話時,其中有一步是需要通過Camera1Capturer或Camera2Capturer來打開手機攝像頭,並調用startCapturer方法開始錄製並返回本地流數據,先來看下VideoCapturer和Camera1Capturer/Camera2Capturer的關係
可以看到Camera1Capturer和Camera2Capturer的曾祖父類是VideoCapturer類,該類是一個接口,定義了一些方法
可以看到,該類的方法依次爲初始化、開始錄像、暫停錄像、改變錄像規格(分辨率)、釋放和返回是否截屏的方法,那麼從這裏可以知道,子類Camera1Capturer(以下都只以Camera1Capturer做討論)也會繼承這些方法,那麼我們研究的重點在於初始化和開始錄像,接着我們看CameraVideoCapturer類做了什麼;CameraVideoCapturer這個類當中有兩個過時的方法(不討論),還有一個靜態內部類CameraStatistics,直接翻譯一下就是相機統計,通過源碼得知,該類只是開了個線程,通過surfaceTextureHelper對象每兩秒執行一次,判斷相機是否可用,並且記錄幀的數量。
2.1、CameraCapturer
看完CameraVideoCapturer之後,發現該類並沒有對VideoCapturer接口中的方法進行實現,那麼進而看子類CameraCapturer,首先是初始化方法initialze
該方法很簡單,只是對上層傳入的參數進行復制,但值得我們留意的是capturerObserver對象,根據webrtc視頻通話流程,我們知道有個VideoSource對象,在創建VideoCapturer時候需要將videoSource.capturerObserver對象傳進來;接着再看startCapture方法
可以看到startCapture方法當中調用了createSessionInternal方法,該方法又執行了createCameraSession方法,跟蹤發現,createCameraSession是一個抽象的方法,而抽象方法被實現的地方正是Camera1Capturer類。
2.2、Camera1Session相機會話
該類主要實現對相機的開啓和採集
通過源碼得知,實例化Camera1Session之後就立馬打開了相機,並且調用的是camera.startPreview方法,該方法是開啓預覽的方法,預覽的返回接口則執行了listenForTextureFrames方法
到這裏逐漸清晰,原來Camera開啓預覽,產生的幀數據是可以被採集到的,並且通過events.onFrameCaptured將幀數據傳遞出去;webrtc壓根不是通過錄像採集相機的流,而只是簡單的開啓預覽就行;在此可能有人就會問,events是什麼東西,別急,看Camera1Session的創建流程
而在前文我們看到的createSessionInternal方法中執行的createCameraSession方法傳入的第二個參數,到這裏我們可以知道,events應該是個接口或者是個抽象類,這樣才能把採集到的數據傳遞到上層
而在CameraCapturer類當中,執行createCameraSession方法時傳進的就是Events的一個實現對象,我們主要看onFrameCaptured實現
到此,我們知道最終的幀數據通過初始化initilaze方法傳入的capturerObserver對象回調給videoSource對象,接着就是webrtc更底層的操作,最終得到流並傳給對方或者本地顯示。
三、UsbCapturer
分析完原有webrtc如何採集數據之後,我們可以發現關鍵點;第一、採集數據是直接通過startPreview就可以,那麼UVCCamera也能對Usb攝像頭進行打開預覽關閉預覽等操作;第二、採集到的數據最終通過videoSource.capturerObserver對象傳出即可;
public class UsbCapturer implements VideoCapturer, USBMonitor.OnDeviceConnectListener, IFrameCallback {
private static final String TAG = "UsbCapturer";
private USBMonitor monitor;
private SurfaceViewRenderer svVideoRender;
private CapturerObserver capturerObserver;
private int mFps;
private UVCCamera mCamera;
private final Object mSync = new Object();
private boolean isRegister;
private USBMonitor.UsbControlBlock ctrlBlock;
public UsbCapturer(Context context, SurfaceViewRenderer svVideoRender) {
this.svVideoRender = svVideoRender;
monitor = new USBMonitor(context, this);
}
@Override
public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) {
this.capturerObserver = capturerObserver;
}
@Override
public void startCapture(int width, int height, int fps) {
this.mFps = fps;
if (!isRegister) {
isRegister = true;
monitor.register();
} else if (ctrlBlock != null) {
startPreview();
}
}
@Override
public void stopCapture() {
if (mCamera != null) {
mCamera.destroy();
mCamera = null;
}
}
@Override
public void changeCaptureFormat(int i, int i1, int i2) {
}
@Override
public void dispose() {
monitor.unregister();
monitor.destroy();
monitor = null;
}
@Override
public boolean isScreencast() {
return false;
}
@Override
public void onAttach(UsbDevice device) {
LogKt.Loges(TAG, "onAttach:");
monitor.requestPermission(device);
}
@Override
public void onDettach(UsbDevice device) {
LogKt.Loges(TAG, "onDettach:");
if (mCamera != null) {
mCamera.close();
}
}
@Override
public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) {
LogKt.Loges(TAG, "onConnect:");
UsbCapturer.this.ctrlBlock = ctrlBlock;
startPreview();
}
@Override
public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) {
LogKt.Loges(TAG, "onDisconnect:");
if (mCamera != null) {
mCamera.close();
}
}
@Override
public void onCancel(UsbDevice device) {
LogKt.Loges(TAG, "onCancel:");
}
private ReentrantLock imageArrayLock = new ReentrantLock();
@Override
public void onFrame(ByteBuffer frame) {
if (frame != null) {
imageArrayLock.lock();
byte[] imageArray = new byte[frame.remaining()];
frame.get(imageArray);
//關鍵
long imageTime = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime());
VideoFrame.Buffer mNV21Buffer = new NV21Buffer(imageArray
, UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT
, null);
VideoFrame mVideoFrame = new VideoFrame(mNV21Buffer, 0, imageTime);
capturerObserver.onFrameCaptured(mVideoFrame);
mVideoFrame.release();
imageArrayLock.unlock();
}
}
public USBMonitor getMonitor() {
return this.monitor;
}
private void startPreview() {
synchronized (mSync) {
if (mCamera != null) {
mCamera.destroy();
}
}
UVCCamera camera = new UVCCamera();
camera.setAutoFocus(true);
camera.setAutoWhiteBlance(true);
try {
camera.open(ctrlBlock);
// camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, UVCCamera.PIXEL_FORMAT_RAW);
camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, FpsType.FPS_15, mFps, UVCCamera.PIXEL_FORMAT_RAW, 1.0f);
} catch (Exception e) {
try {
// camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, UVCCamera.DEFAULT_PREVIEW_MODE);
camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, FpsType.FPS_15, mFps, UVCCamera.DEFAULT_PREVIEW_MODE, 1.0f);
} catch (Exception e1) {
camera.destroy();
camera = null;
}
}
if (camera != null) {
if (svVideoRender != null) {
camera.setPreviewDisplay(svVideoRender.getHolder().getSurface());
}
camera.setFrameCallback(UsbCapturer.this, UVCCamera.PIXEL_FORMAT_YUV420SP);
camera.startPreview();
}
synchronized (mSync) {
mCamera = camera;
}
}
public void setSvVideoRender(YQRTCSurfaceViewRenderer svVideoRender) {
this.svVideoRender = svVideoRender;
}
}
注:使用UsbCapturer需要傳入預覽View。
後序:其實一開始我分析完上述的流程之後,並不知道該如何着手設計UsbCapturer,直到在github上看到這個之後https://github.com/sutogan4ik/android-webrtc-usb-camera,才一下子恍然大悟,非常感謝這位大佬,雖然他的代碼在我項目中運行無效,但修改一下之後也就完美了,另外當中裏面使用的方法 capturerObserver.onByteBufferFrameCaptured在新版的webrtc中已經不存在了