行业知识
Android在后台读取UVC摄像头的帧数据流并推送
2026-11-09 00:07:26
来源:互联网转载
Android在后台读取UVC摄像头的帧数据流并推送
-
添加UvcCamera依赖库
使用原版的 saki4510t/UVCCamera 在预览过程中断开可能会闪退,这里使用的是
jiangdongguo/AndroidUSBCamera 中修改的版本,下载到本地即可。
https://github.com/jiangdongguo/AndroidUSBCamera
-
监听UVC连接回调
mUSBMonitor = USBMonitor(context, mOnDeviceConnectListener) mUSBMonitor.register() public interface OnDeviceConnectListener { void onAttach(UsbDevice device); void onDetach(UsbDevice device); void onConnect(UsbDevice device, UsbControlBlock ctrlBlock, boolean createNew); void onDisconnect(UsbDevice device, UsbControlBlock ctrlBlock); void onCancel(UsbDevice device); } - 检测到UVC后连接该设备
USB连接上会回调,onAttach, 本地判断连接上的USB设备是否UVC,如果是的话可以尝试调用连接该对象。调用mUSBMonitor.requestPermission(cam)就会请求权限并且连接该对象。连接成功后会回调 onConnect。
var connectJob: Disposable? = null override fun onAttach(device: UsbDevice?) { BLLog.i(TAG, "onAttach") BLLog.toast("USB_DEVICE_ATTACHED") connectJob?.dispose() connectJob = CommonUtils.runDelayed(1000) { autoConnectUvcDevice() } } private fun autoConnectUvcDevice() { val context = BLSession.getApplicationContext() val filter: List = DeviceFilter.getDeviceFilters(context, R.xml.device_filter_uvc) val devs: List = mUSBMonitor?.getDeviceList(filter[0]) ?: listOf() val cam = findUsbCam(devs) BLLog.log2File( TAG, "autoConnect 共有USB数量:${devs.size}, Uvc: ${cam?.productName}" ) if (cam == null) { BLLog.i(TAG, "未连接USB摄像头") } else { mUSBMonitor.requestPermission(cam) } } device_filter_uvc.xml
如何判断该连接对象是UVC对象,如果名字中包含USBCam或 interfaceClass = USB_CLASS_VIDEO
private fun findUsbCam(devs: List): UsbDevice? { for (dev in devs) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val name = "" + dev.productName + dev.manufacturerName BLLog.i(TAG, "findUsbCam name:$name") if (name.contains("USBCam")) { return dev } for (i in 0 until dev.interfaceCount) { val inter = dev.getInterface(i) BLLog.i(TAG, "getInterface($i):$inter") if (inter.interfaceClass == UsbConstants.USB_CLASS_VIDEO) { return dev } } } } return null } - 在onConnect中保存UvcCamera对象
override fun onConnect( device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?, createNew: Boolean ) { BLLog.i(TAG, "onConnect ${device?.productName}") synchronized(mSync) { try { // 保存最新的Uvc对象 val camera = UVCCamera(); camera.open(ctrlBlock) BLLog.log2File( TAG, "supportedSize:" + camera.supportedSize + "thread:" + Thread.currentThread().name ) if (applyPreviewSize(camera)) { mUVCCamera?.destroy() mUVCCamera = camera previewStatus = PreViewStatus.None } else { BLLog.console(TAG, "UVC不支持此分辨率:$defPreviewSize") if (mUVCCamera == null) { mUVCCamera = camera } } uvcChangedSub.onNext(true) } catch (e:Exception){ BLLog.log2File(TAG, "onConnect Recv exception: $e") } } } - 预览并获取YUC视频帧
如果需要预览到UI中显示,需要创建SurfaceView或者TextureView.
mUVCCamera?.setPreviewDisplay(previewSurface) mUVCCamera?.startPreview()
如果不需要预览到UI中显示,可以new一个SurfaceTexture对象传进去即可;必须要调用预览才能获取到YUV数据。
val surfaceTexture = SurfaceTexture(0) mUVCCamera?.setPreviewTexture(surfaceTexture) BLLog.i(TAG, "startPreviewWithAir") mUVCCamera?.startPreview()
预览后获取YUV帧流:
val width = mUVCCamera?.previewSize?.width ?: defPreviewSize.width val height = mUVCCamera?.previewSize?.height ?: defPreviewSize.height yuvCallback = callback mUVCCamera?.setFrameCallback({ buffer: ByteBuffer ->// BLLog.i(TAG, "onFrame ${width}*${height}") // yuv格式 if (acceptFrame()) { val format = MediaFormat.createVideoFormat("", width, height) val data = ByteArray(buffer.remaining()) buffer.get(data) val frame = YuvFrameData(format, data, width, height) yuvCallback?.onGetYuvData(frame) } }, UVCCamera.PIXEL_FORMAT_NV21) 获取到的YUV帧可以使用其他推流SDK进行推流即可,比如使用阿里云推流SDK推流。
这完成可以在后台进行推流,不需要UI上展示,节省设备的性能。
- 连接类参考:
// Uvc设备连接器object UvcConnector : BaseBussModel(ModelType.Shared) { private val TAG = UvcConnector::class.java.simpleName private val KEY_UVC_PREVIEW_SIZE = "KEY_UVC_PREVIEW_SIZE" // 默认支持:640*480, 1920*1080 private var defPreviewSize = MySize.parseSize("1920*1080")!! enum class PreViewStatus { None, Visible, Air, } private lateinit var mUSBMonitor: USBMonitor @Volatile private var mUVCCamera: UVCCamera? = null private val mSync = Object() private var previewStatus = PreViewStatus.None @Volatile private var yuvCallback: IMediaKit.OnYuvListener? = null // 状态变更消息 private var uvcChangedSub = PublishSubject.create() override fun onStartUp() { super.onStartUp() BLLog.i(TAG, "onStartUp") val context = BLSession.getApplicationContext() mUSBMonitor = USBMonitor(context, mOnDeviceConnectListener) mUSBMonitor.register() CommonUtils.runAsync(::loadPreviewSize) } override fun onShutdown() { BLLog.i(TAG, "onShutdown") mUSBMonitor.unregister() mUSBMonitor.destroy() super.onShutdown() } fun hasUvcDevice(): Boolean { return mUVCCamera != null } fun previewStatus(): PreViewStatus { return previewStatus } fun getSubject() = uvcChangedSub fun getNowSize(): MySize? { return mUVCCamera?.previewSize?.let { MySize(it.width, it.height) } } fun getExpSize(): MySize { return defPreviewSize } fun startPreview(previewSurface: Surface): CallResult { BLLog.i(TAG, "startPreview") if (!hasUvcDevice()) { return CallResult(false, "未连接设备") } if (previewStatus == PreViewStatus.Air) { mUVCCamera?.stopPreview() } if (!applyPreviewSize(mUVCCamera)) { BLLog.console(TAG, "UVC不支持此分辨率:$defPreviewSize") return CallResult(false, "UVC不支持此分辨率:$defPreviewSize") } mUVCCamera?.setPreviewDisplay(previewSurface) mUVCCamera?.startPreview() previewStatus = PreViewStatus.Visible if (yuvCallback != null) { setYuvCallback(yuvCallback!!) } uvcChangedSub.onNext(true) return CallResult(true, "成功") } fun stopPreview() { BLLog.i(TAG, "stopPreview") if (previewStatus != PreViewStatus.Visible) { return } mUVCCamera?.stopPreview() previewStatus = PreViewStatus.None // 需要接收数据 if (yuvCallback != null) { startPreviewWithAir() setYuvCallback(yuvCallback!!) } uvcChangedSub.onNext(true) } fun clearYuvCallback() { yuvCallback = null if (previewStatus == PreViewStatus.Air) { mUVCCamera?.stopPreview() previewStatus = PreViewStatus.None uvcChangedSub.onNext(true) } } fun setYuvCallback(callback: IMediaKit.OnYuvListener): Boolean { if (mUVCCamera == null) { return false } if (previewStatus == PreViewStatus.None) { startPreviewWithAir() } val width = mUVCCamera?.previewSize?.width ?: defPreviewSize.width val height = mUVCCamera?.previewSize?.height ?: defPreviewSize.height yuvCallback = callback mUVCCamera?.setFrameCallback({ buffer: ByteBuffer ->// BLLog.i(TAG, "onFrame ${width}*${height}") // yuv格式 if (acceptFrame()) { val format = MediaFormat.createVideoFormat("", width, height) val data = ByteArray(buffer.remaining()) buffer.get(data) val frame = YuvFrameData(format, data, width, height) yuvCallback?.onGetYuvData(frame) } }, UVCCamera.PIXEL_FORMAT_NV21) return true } private fun acceptFrame(): Boolean { return Random.nextInt(30) if (!applyPreviewSize(mUVCCamera)) { BLLog.console(TAG, "UVC不支持此分辨率:$defPreviewSize") return } val surfaceTexture = SurfaceTexture(0) mUVCCamera?.setPreviewTexture(surfaceTexture) BLLog.i(TAG, "startPreviewWithAir") mUVCCamera?.startPreview() previewStatus = PreViewStatus.Air uvcChangedSub.onNext(true) } private fun findUsbCam(devs: List for (dev in devs) { if (Build.VERSION.SDK_INT = Build.VERSION_CODES.LOLLIPOP) { val name = "" + dev.productName + dev.manufacturerName BLLog.i(TAG, "findUsbCam name:$name") if (name.contains("USBCam")) { return dev } for (i in 0 until dev.interfaceCount) { val inter = dev.getInterface(i) BLLog.i(TAG, "getInterface($i):$inter") if (inter.interfaceClass == UsbConstants.USB_CLASS_VIDEO) { return dev } } } } return null } private fun autoConnectUvcDevice() { val context = BLSession.getApplicationContext() val filter: List = DeviceFilter.getDeviceFilters(context, R.xml.device_filter_uvc) val devs: List = mUSBMonitor?.getDeviceList(filter[0]) ?: listOf() val cam = findUsbCam(devs) BLLog.log2File( TAG, "autoConnect 共有USB数量:${devs.size}, Uvc: ${cam?.productName}" ) if (cam == null) { BLLog.i(TAG, "未连接USB摄像头") } else { mUSBMonitor.requestPermission(cam) } } private fun applyPreviewSize(camera: UVCCamera?):Boolean { if (camera == null){ return false } try { camera.setPreviewSize( defPreviewSize.width, defPreviewSize.height, UVCCamera.FRAME_FORMAT_MJPEG ) } catch (e: IllegalArgumentException) { BLLog.log2File(TAG, "setPreviewSize1 $defPreviewSize: $e") try { // fallback to YUV mode camera.setPreviewSize( defPreviewSize.width, defPreviewSize.height, UVCCamera.DEFAULT_PREVIEW_MODE ) } catch (e1: IllegalArgumentException) { BLLog.log2File(TAG, "setPreviewSize2 $defPreviewSize: $e1") return false } } return true } private val mOnDeviceConnectListener: USBMonitor.OnDeviceConnectListener = object : USBMonitor.OnDeviceConnectListener { var connectJob: Disposable? = null override fun onAttach(device: UsbDevice?) { BLLog.i(TAG, "onAttach") BLLog.toast("USB_DEVICE_ATTACHED") connectJob?.dispose() connectJob = CommonUtils.runDelayed(1000) { autoConnectUvcDevice() } } override fun onDetach(device: UsbDevice?) { BLLog.i(TAG, "onDetach") BLLog.toast("USB_DEVICE_DETACHED") synchronized(mSync) { if (mUVCCamera != null) { mUVCCamera?.destroy() mUVCCamera = null previewStatus = PreViewStatus.None uvcChangedSub.onNext(true) } } } override fun onConnect( device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?, createNew: Boolean ) { BLLog.i(TAG, "onConnect ${device?.productName}") synchronized(mSync) { try { // 保存最新的Uvc对象 val camera = UVCCamera(); camera.open(ctrlBlock) BLLog.log2File( TAG, "supportedSize:" + camera.supportedSize + "thread:" + Thread.currentThread().name ) if (applyPreviewSize(camera)) { mUVCCamera?.destroy() mUVCCamera = camera previewStatus = PreViewStatus.None } else { BLLog.console(TAG, "UVC不支持此分辨率:$defPreviewSize") if (mUVCCamera == null) { mUVCCamera = camera } } uvcChangedSub.onNext(true) } catch (e:Exception){ BLLog.log2File(TAG, "onConnect Recv exception: $e") } } } override fun onDisconnect(device: UsbDevice?, ctrlBlock: USBMonitor.UsbControlBlock?) { BLLog.i(TAG, "onDisconnect ${device?.productName}") synchronized(mSync) { mUVCCamera?.destroy() mUVCCamera = null previewStatus = PreViewStatus.None uvcChangedSub.onNext(true) } } override fun onCancel(device: UsbDevice?) { BLLog.i(TAG, "onCancel") } } fun setPreviewSize(size: MySize) { defPreviewSize = size SharedPreferenceHelper.saveCustom(KEY_UVC_PREVIEW_SIZE, size.toString()) } private fun loadPreviewSize() { val str = SharedPreferenceHelper.loadCustom(KEY_UVC_PREVIEW_SIZE, "") defPreviewSize = MySize.parseSize(str) ?: defPreviewSize }}