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> =
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.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<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() = 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) <= 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.Air
uvcChangedSub.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.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> =
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 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
}
}