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? = nulloverride 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> =DeviceFilter.getDeviceFilters(context, R.xml.device_filter_uvc)val devs: List<UsbDevice> = 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
<usb><usb-device class="239" subclass="2" /> <!-- all device of UVC -->
</usb>
如何判斷該連接對象是UVC對象,如果名字中包含USBCam或 interfaceClass = USB_CLASS_VIDEO
private fun findUsbCam(devs: List<UsbDevice>): UsbDevice? {for (dev in devs) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {val name = "" + dev.productName + dev.manufacturerNameBLLog.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 = camerapreviewStatus = 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.widthval height = mUVCCamera?.previewSize?.height ?: defPreviewSize.heightyuvCallback = callbackmUVCCamera?.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.simpleNameprivate val KEY_UVC_PREVIEW_SIZE = "KEY_UVC_PREVIEW_SIZE"// 默認支持:640*480, 1920*1080private var defPreviewSize = MySize.parseSize("1920*1080")!!enum class PreViewStatus {None,Visible,Air,}private lateinit var mUSBMonitor: USBMonitor@Volatileprivate var mUVCCamera: UVCCamera? = nullprivate val mSync = Object()private var previewStatus = PreViewStatus.None@Volatileprivate var yuvCallback: IMediaKit.OnYuvListener? = null// 狀態變更消息private var uvcChangedSub = PublishSubject.create<Boolean>()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() = uvcChangedSubfun 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.Visibleif (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 = nullif (previewStatus == PreViewStatus.Air) {mUVCCamera?.stopPreview()previewStatus = PreViewStatus.NoneuvcChangedSub.onNext(true)}}fun setYuvCallback(callback: IMediaKit.OnYuvListener): Boolean {if (mUVCCamera == null) {return false}if (previewStatus == PreViewStatus.None) {startPreviewWithAir()}val width = mUVCCamera?.previewSize?.width ?: defPreviewSize.widthval height = mUVCCamera?.previewSize?.height ?: defPreviewSize.heightyuvCallback = callbackmUVCCamera?.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) <= 25}private fun startPreviewWithAir() {if (!applyPreviewSize(mUVCCamera)) {BLLog.console(TAG, "UVC不支持此分辨率:$defPreviewSize")return}val surfaceTexture = SurfaceTexture(0)mUVCCamera?.setPreviewTexture(surfaceTexture)BLLog.i(TAG, "startPreviewWithAir")mUVCCamera?.startPreview()previewStatus = PreViewStatus.AiruvcChangedSub.onNext(true)}private fun findUsbCam(devs: List<UsbDevice>): UsbDevice? {for (dev in devs) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {val name = "" + dev.productName + dev.manufacturerNameBLLog.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> =DeviceFilter.getDeviceFilters(context, R.xml.device_filter_uvc)val devs: List<UsbDevice> = 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 modecamera.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? = nulloverride 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 = nullpreviewStatus = PreViewStatus.NoneuvcChangedSub.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 = camerapreviewStatus = 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 = nullpreviewStatus = PreViewStatus.NoneuvcChangedSub.onNext(true)}}override fun onCancel(device: UsbDevice?) {BLLog.i(TAG, "onCancel")}}fun setPreviewSize(size: MySize) {defPreviewSize = sizeSharedPreferenceHelper.saveCustom(KEY_UVC_PREVIEW_SIZE, size.toString())}private fun loadPreviewSize() {val str = SharedPreferenceHelper.loadCustom(KEY_UVC_PREVIEW_SIZE, "")defPreviewSize = MySize.parseSize(str) ?: defPreviewSize}
}