本文章参考自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全称为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,因为需求不同,开发功能的方向就不一样,待事而定,希望能够帮到你们!
关于
作者: 00000
邮箱:00000 [email protected]
博客主页:0000 https://www.jianshu.com/u/ec0cca2bb321
github: 00000 https://github.com/maiduoduo