Android设备接入外部USB摄像头:AndroidUSBCamera,UVCCamera开发总结

本文章参考自Android直播开发之旅(10):AndroidUSBCamera,UVCCamera开发通用库
参考自AndroidUSBCamera 使用步骤

之前也没有关于外接USB摄像头(UVCCamera)的需求,也就没有涉猎过相关。写这篇博客只是记录自己 在接到此种的需求了解到的东西

  先决条件:
    1. 设备必须满足USB-OTG功能支持
    2. usb摄像头为满足UVC制式协议的UVCCamera(关于UVC协议请阅读下方链接)
    3.对于android设备是不是只支持这一种摄像头,或者说只有遵循UVC协议的摄像头才可以接入
    4.UVCCamera属于免驱,热插拔,即插即用,即拔即停。Android底层Linux已经做好对此的兼容性设计及驱动支持。

不要着急,先看下什么是UVC

什么是UVC?

UVC协议

简而言之,言而总之,UVC是一种约束型协议
UVC全称为USB Video Class,即:USB视频类,是一种为USB视频捕获设备定义的协议标准。是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,已成为USB org标准之一。

  • 如今的主流操作系统(如Windows XP SP2 and later, Linux 2.4.6 and later, MacOS 10.5 and later)都已提供UVC设备驱动,因此符合UVC规格的硬件设备在不需要安装任何的驱动程序下即可在主机中正常使用。使用
  • UVC技术的包括摄像头、数码相机、类比影像转换器、电视棒及静态影像相机等设备。
  • 最新的UVC版本为UVC 1.5,由USB Implementers Forum定义包括基本协议及负载格式。
  • 网络摄像头是第一个支持UVC而且也是数量最多的UVC设备,操作系统只要是 Windows XP SP2 之后的版本都可以支持 UVC,当然 Vista 就更不用说了。Linux系统自2.4以后的内核都支持了大量的设备驱动,并可以支持 UVC设备。
  • 使用 UVC 的好处 USB 在 Video这块也成为一项标准了之后,硬件在各个程序之间彼此运行会更加顺利,而且 也省略了驱动程序安装这一环节。

怎么接入并使用

AndroidUSBCamera基于saki4510t/UVCCamera开发,该项目对USB Camera(UVC设备)的使用和视频数据采集进行了高度封装,能够帮助开发者通过几个简单的API实现USB Camera设备的检测、连接、预览和音视频数据采集,最重要的是手机无需root,只需支持otg功能即可驱动。主要功能包括:

  • (1)支持USB Camera设备检测,画面实时预览;
  • (2)支持本地录制mp4格式视频,支持实时获取音视频数据流;
  • (3)支持jpg格式图片抓拍;
  • (4)支持获取camera支持的分辨率,和分辨率切换;
  • (5)支持屏蔽声音,重启Camera;
  • (6)支持相机自动对焦;
  • (7)支持调整对比度和亮度
  • (8)支持480P、720P、1080P and higher
  • (9) 支持Android5.0,6.0,7.0,8.0,9.0
预览.png
1、git下载:https://github.com/jiangdongguo/AndroidUSBCamera
2、下载后解压

将模块 libusbcamera、libutils集成到自已的项目中,直接拷贝到项目根目录下,相关配置

  • settings.gradle 文件添加
  ':libusbcamera', ':libutils'
  • app build.gradle 文件
   implementation project(':libusbcamera')
  • project build.gradle 文件
allprojects {
    repositories {
        jcenter()
        google()
        maven { url 'https://jitpack.io' }
        maven { url 'https://raw.githubusercontent.com/saki4510t/libcommon/master/repository/' }
 
    }
}

AndroidManifest.xml 文件开启相关权限

    <uses-permission android:name="android.permission.RECORD_AUDIO"/>    
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />   
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />    
    <uses-feature android:name="android.hardware.usb.host"/>

项目ndk 要设置上 最后项目async

3.UsbCameraActivity
  public class UsbCameraActivity extends AppCompatActivity implements CameraViewInterface.Callback {
 
    private final String TAG = MainActivity.class.getSimpleName();
 
    public View mTextureView;
    private UVCCameraHelper mCameraHelper;
    private CameraViewInterface mUVCCameraView;
 
    private boolean isRequest = false;
    private boolean isPreview = false;
    private boolean isRecording = false;
    private UVCCameraHelper.OnMyDevConnectListener listener = new UVCCameraHelper.OnMyDevConnectListener() {
 
        @Override
        public void onAttachDev(UsbDevice device) {
            // request open permission
            Log.d(TAG, "camera: usb 设备 " + device.getProductName() + " 新连接");
            if (mCameraHelper == null || mCameraHelper.getUsbDeviceCount() == 0) {
                showShortMsg("未检测到USB摄像头设备");
                return;
            }
            List<UsbDevice> devList = mCameraHelper.getUsbDeviceList();
            /*
             * usb连接时,判断是不是这个摄像头,是就打开,实现了热插拔,插拔一次,
             * 设备的id就加一,所以地址就改变,机器重启id初始化,getProductName()获得的是摄像头 
             * 名称 
             * */
            if (!isRequest)
                for (int i = 0; i < devList.size(); i++) {
                    UsbDevice _device = devList.get(i);
                    //这里indexOf("xxxx")填入的是productName,productName根据你接入设备名称填入,最好是使用getProductName()方式去取值,打开摄像头
                    //我的外接USB摄像头的productName="UVC Camera",所以填入"UVC Camera"
                   // if (_device.getProductName().indexOf("camera") > -1) {
                      if (_device.getProductName().indexOf("UVC Camera") > -1) {
                        isRequest = true;
                        mCameraHelper.requestPermission(i);//打开对应usb摄像头
                    }
                }
        }
 
        @Override
        public void onDettachDev(UsbDevice device) {
            // close camera
            Log.d(TAG, "camera: usb 设备 " + device.getProductName() + " 已拔出");
            if (isRequest) {
                isRequest = false;
                mCameraHelper.closeCamera();
                showShortMsg(device.getProductName() + " 已拨出");
            }
        }
 
        @Override
        public void onConnectDev(UsbDevice device, boolean isConnected) {
            Log.d(TAG, "camera: usb 设备 " + device.getProductName() + " 连接失败");
            if (!isConnected) {
                showShortMsg("连接失败,请检查分辨率参数是否正确");
                isPreview = false;
            } else {
                isPreview = true;
                showShortMsg("usb 设备正在连接");
                // need to wait UVCCamera initialize over
                Log.d(TAG, "camera is connected");
            }
        }
 
        @Override
        public void onDisConnectDev(UsbDevice device) {
            Log.d(TAG, "camera: usb disconnecting");
            showShortMsg("usb设备连接断开");
        }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
        // step.1 initialize UVCCameraHelper
        mTextureView = findViewById(R.id.camera_view);
        mUVCCameraView = (CameraViewInterface) mTextureView;
        mUVCCameraView.setCallback(this);
        mCameraHelper = UVCCameraHelper.getInstance();
        mCameraHelper.setDefaultFrameFormat(UVCCameraHelper.FRAME_FORMAT_YUYV);
        /*
         * 初始化分辨率,一定是设备支持的分辨率,否则摄像不能正常使用
         * */
        mCameraHelper.setDefaultPreviewSize(640, 480);
        mCameraHelper.initUSBMonitor(this, mUVCCameraView, listener);
        mCameraHelper.setOnPreviewFrameListener(new AbstractUVCCameraHandler.OnPreViewResultListener() {
            int printNum = 0;
 
            @Override
            public void onPreviewResult(byte[] nv21Yuv) {
                printNum++;
                if (printNum == 300) {
                    printNum = 0;
                    Log.d(TAG, "onPreviewResult: " + nv21Yuv.length + "摄像头预览");
                }
 
            }
        });
    }
 
    //录像
    private void cameraRecording(Boolean isStartRecording, String Name) {
        isRecording = isStartRecording;
        if (mCameraHelper == null || !mCameraHelper.isCameraOpened() || !isPreview) {
            showShortMsg("摄像头异常,请重新更换插口并重启app");
            return;
        }
        String OrderRecordStr = prefs.getString(Config.ORDER_RECORDING, "");
        Log.d(TAG, "OrderRecorde1=" + OrderRecordStr);
        if (!mCameraHelper.isPushing() && isStartRecording) {
            //文件地址自已设置
            String videoPath = Config.VIDEO_DIRECTORY + "/ " + Name;
            OrderRecordStr = OrderRecordStr + "&" + Name;
            prefs.edit().putString(Config.ORDER_RECORDING, OrderRecordStr).apply();
            RecordParams params = new RecordParams();
            params.setRecordPath(videoPath);
            params.setRecordDuration(0);                        // auto divide saved,default 0 means not divided
            params.setVoiceClose(true);    // is close voice
            params.setSupportOverlay(true); // overlay only support armeabi-v7a & arm64-v8a
            mCameraHelper.startPusher(params, new AbstractUVCCameraHandler.OnEncodeResultListener() {
                @Override
                public void onEncodeResult(byte[] data, int offset, int length, long timestamp, int type) {
                    // type = 1,h264 video stream
                    if (type == 1) {
                        FileUtils.putFileStream(data, offset, length);
                    }
                    // type = 0,aac audio stream
                    if (type == 0) {
 
                    }
                }
 
                @Override
                public void onRecordResult(String videoPath) {
                    if (TextUtils.isEmpty(videoPath)) {
                        return;
                    }
                    new Handler(getMainLooper()).post(() -> Toast.makeText(MainActivity.this, "save videoPath:" + videoPath, Toast.LENGTH_SHORT).show());
                }
            });
            // if you only want to push stream,please call like this
            // mCameraHelper.startPusher(listener);
            showShortMsg("开始录制视频");
        } else if (mCameraHelper.isPushing() && !isStartRecording) {
            FileUtils.releaseFile();
            mCameraHelper.stopPusher();
            showShortMsg("停止录制视频");
            String[] OrderRecordArr = OrderRecordStr.split("&");
            if (OrderRecordArr.length > 5) {
                String order = OrderRecordArr[1];
                String filePath = Config.VIDEO_DIRECTORY + "/ " + order + ".mp4";
                deleteFile(filePath);
                String _OrderRecordStr = "";
                for (int i = 0; i < OrderRecordArr.length; i++) {
                    if (OrderRecordArr[i] != order && OrderRecordArr[i].length() > 0)
                        _OrderRecordStr = _OrderRecordStr + "&" + OrderRecordArr[i];
                }
                prefs.edit().putString(Config.ORDER_RECORDING, _OrderRecordStr).apply();
                Log.d(TAG, "OrderRecorde=" + prefs.getString(Config.ORDER_RECORDING, ""));
            }
        }
    }
 
    //删除文件
    public boolean deleteFile(String filePath) {
        File file = new File(filePath);
        if (file.isFile() && file.exists()) return file.delete();
        else if (file.isFile() && !file.exists()) return true;
        return false;
    }
    @Override
    public void onResume() {
        super.onResume();
        // 恢复Camera预览
        if (mUVCCameraView != null) mUVCCameraView.onResume();
    }
 
    @Override
    protected void onStart() {
        super.onStart();
        // step.2 register USB event broadcast
        if (mCameraHelper != null) {
            mCameraHelper.registerUSB();
        }
    }
 
    @Override
    protected void onStop() {
        super.onStop();
        // step.3 unregister USB event broadcast
        if (mCameraHelper != null) {
            mCameraHelper.unregisterUSB();
        }
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        
        if (mUVCCameraView != null) mUVCCameraView.onPause();
    }
 
    private void showShortMsg(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
 
    @Override
    public USBMonitor getUSBMonitor() {
        return mCameraHelper.getUSBMonitor();
    }
 
    @Override
    public void onDialogResult(boolean canceled) {
 
    }
 
    public boolean isCameraOpened() {
        return mCameraHelper.isCameraOpened();
    }
 
    @Override
    public void onSurfaceCreated(CameraViewInterface view, Surface surface) {
        isPreview = false;
        new Thread(new Runnable() {
            @Override
            public void run() {
                // wait for camera created
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG, "camera: surface start preview " + isPreview + "  " + isCameraOpened());
                if (!isPreview && isCameraOpened()) {
                    mCameraHelper.startPreview(mUVCCameraView);
                    isPreview = true;
                    Log.d(TAG, "camera: surface start preview");
                }
            }
        }).start();
    }
 
    @Override
    public void onSurfaceChanged(CameraViewInterface view, Surface surface, int width, int height) {
 
    }
 
    @Override
    public void onSurfaceDestroy(CameraViewInterface view, Surface surface) {
        if (isPreview && isCameraOpened()) {
            mCameraHelper.stopPreview();
            Log.d(TAG, "surface:" + "is destroy");
        }
        isPreview = false;
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        FileUtils.releaseFile();
        // step.4 release uvc camera resources
        if (mCameraHelper != null) {
            mCameraHelper.release();
            Log.d(TAG, "camera is release");
        }
        isPreview = false;
        isRequest = false;
    }
 
}

4.activity_main.xml

<com.serenegiant.usb.widget.UVCCameraTextureView
    android:id="@+id/camera_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"/>

需要注意

(1)mCameraHelper.requestPermission(int index) 是打开usb设备,有的usb不是摄像头设备,需要对usb设备名称进行过滤,可控制需要打开特定的usb摄像头, 可以热插拔显示

(2)app关闭或后台运行 isPreview 需要重置为 false 不然再次进入app 预览无画面因为startPreview 未执行

(3)设备重启后第一次打开app, 预览画面可能没有,但实际是可以录制的,重新进入app就可以了

(4) 注:在使用Android Studio移植UVCCamera时,很多朋友可能会遇到"open(“/dev/bus/usb/001/002”, O_RDWR, 0),报错,Permission denied"问题,这是由于Android Studio使用的ndk版本所致,建议使用ndk-r14即可。解决方法:local.properties-->指定ndk.dir版本。(注:这里使用的是离线方式)

20180404163647386 (1).gif
20190924103544700 (1).gif

具体使用步骤和使用细节请参见本篇开头的两篇博客,再次放一下地址:

Android直播开发之旅(10):AndroidUSBCamera,UVCCamera开发通用库
AndroidUSBCamera 使用步骤

GitHub源码地址:https://github.com/jiangdongguo/AndroidUSBCamera(如果对您有用,欢迎star&fork以表支持~谢谢_!)

感谢

感谢相关开放出来解决方案的这些开发者们。
在使用中要结合自己项目需求去进行扩展和使用,不要一味依靠网络这些不同需求不同设定的demo,因为需求不同,开发功能的方向就不一样,待事而定,希望能够帮到你们!

关于

作者: 00000maiduoduo
邮箱:00000 maiduoduo0@163.com
博客主页:0000 https://www.jianshu.com/u/ec0cca2bb321
github: 00000 https://github.com/maiduoduo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343