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

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

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

1
2
3
4
5
  先决条件:
    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 文件添加
1
  ':libusbcamera', ':libutils'
  • app build.gradle 文件
1
   implementation project(':libusbcamera')
  • project build.gradle 文件
1
2
3
4
5
6
7
8
9
allprojects {
    repositories {
        jcenter()
        google()
        maven { url 'https://jitpack.io' }
        maven { url 'https://raw.githubusercontent.com/saki4510t/libcommon/master/repository/' }
 
    }
}

AndroidManifest.xml 文件开启相关权限

1
2
3
4
    <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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
  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

1
2
3
4
5
<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 [email protected]
博客主页:0000 https://www.jianshu.com/u/ec0cca2bb321
github: 00000 https://github.com/maiduoduo