Android webrtc使用USB攝像頭

在疫情爆發的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中已經不存在了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章