草庐IT

Android USBCamera投屏 - 利用UVC协议将手机上的画面有线投屏到Android车机的屏幕上

氦客 2024-02-03 原文

1. 背景

一个需求 : 要将手机上的画面和音频 投屏 到 车机的Android屏幕上。

车机有一个支持OTG的USB-A口,由于设备有限,我们有一个USB-A转HDMI转接口,一跟HDMI线,一个USB-C的拓展坞 (包括HDMI口,两个USB-A口,一个网口),我们将这几根线接在一起,成功将手机和车机连在了一起。

接着,我们在网上找到了一个 jiangdongguo/AndroidUSBCamera ,我们使用Android Studio打开编译安装到车机,并将车机的Usb modeDevice mode切换为Host Mode,这个时候,AndroidUSBCamera会弹出打开USB摄像头的弹框,我们点击同意,就可以看到手机上的画面显示到车机上了。

使用yorkZJC/UvcCameraDemo这个库也可以成功

和这个相关的所有的项目,基本都是基于 saki4510t/UVCCamera 这个开源项目来改的

那么,我们有了以下两个疑问

  • 是如何读取到手机上的画面,显示到车机上的 ?
  • 为什么只有画面,没有声音 ?

2. 是如何读取到手机上的画面,显示到车机上的 ?

带着这个疑问,看了下AndroidUSBCamera的代码。

原来,现在所有主流操作系统都已提供UVC设备驱动,因此符合UVC规格的硬件设备在不需要安装任何的驱动程序下即可在主机中正常使用。使用UVC技术的包括摄像头、数码相机、类比影像转换器、电视棒及静态影像相机等设备。

UVC全称USB Video Class,即 USB视频类,是一种为USB视频捕获设备定义的协议标准。

是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,已成为USB org标准之一。

而项目中,对USB Camera (UVC设备)的使用和视频数据采集进行了高度封装。

//开始进行预览
private void startPreview() {
    mCameraHelper.startPreview(mUVCCameraView);
}

通过startPreview方法,会调用到handleStartPreview方法

public void handleStartPreview(final Object surface) {
    if (DEBUG) Log.v(TAG_THREAD, "handleStartPreview:");
    if ((mUVCCamera == null) || mIsPreviewing) return;
    try {
        mUVCCamera.setPreviewSize(mWidth, mHeight, 1, 31, mPreviewMode, mBandwidthFactor);
        // 获取USB Camera预览数据,使用NV21颜色会失真
        // 无论使用YUV还是MPEG,setFrameCallback的设置效果一致
        mUVCCamera.setFrameCallback(mIFrameCallback, UVCCamera.PIXEL_FORMAT_YUV420SP);
    } catch (final IllegalArgumentException e) {
        try {
            // fallback to YUV mode
            mUVCCamera.setPreviewSize(mWidth, mHeight, 1, 31, UVCCamera.DEFAULT_PREVIEW_MODE, mBandwidthFactor);
        } catch (final IllegalArgumentException e1) {
            callOnError(e1);
            return;
        }
    }
    if (surface instanceof SurfaceHolder) {
        mUVCCamera.setPreviewDisplay((SurfaceHolder) surface);
    }
    if (surface instanceof Surface) {
        mUVCCamera.setPreviewDisplay((Surface) surface);
    } else {
        mUVCCamera.setPreviewTexture((SurfaceTexture) surface);
    }
    mUVCCamera.startPreview();
    mUVCCamera.updateCameraParams();
    synchronized (mSync) {
        mIsPreviewing = true;
    }
    callOnStartPreview();
}

最终调用到nativeSetPreviewDisplay方法

static jint nativeSetPreviewDisplay(JNIEnv *env, jobject thiz,
	ID_TYPE id_camera, jobject jSurface) {

	jint result = JNI_ERR;
	ENTER();
	UVCCamera *camera = reinterpret_cast<UVCCamera *>(id_camera);
	if (LIKELY(camera)) {
		ANativeWindow *preview_window = jSurface ? ANativeWindow_fromSurface(env, jSurface) : NULL;
		result = camera->setPreviewDisplay(preview_window);
	}
	RETURN(result, jint);
}

这时候,我们就可以预览到手机上的画面了。

3. 为什么只有画面,没有声音 ?

这个时候,我们可以发现,车机上只显示出了画面,没有声音播放的。

看了下AndroidUSBCamera里的代码,当点击了录像按钮,调用startPusher方法,回调里type==0,表示是aac audio stream的,但实际测试中,永远都只会收到type==1的情况,而收不到type==0的情况。

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) {
            trackplayer.write(data, offset, length);//往track中写数据
        }
    }

    @Override
    public void onRecordResult(String videoPath) {
        if(TextUtils.isEmpty(videoPath)) {
            return;
        }
        new Handler(getMainLooper()).post(() -> Toast.makeText(USBCameraActivity.this, "save videoPath:"+videoPath, Toast.LENGTH_SHORT).show());
    }
});

在Github 的issue上,我也看到了这个问题 : 可以支持USB的音频输入吗?

看上去大家也有同样的问题

这时候,找到的USB摄像头这个应用市场上的app,却是可以在投屏的同时,播放出声音的。

反编译了这个apk,可以看到它的so里面,有一个libUSBAudio.so,看上去就是用来处理音频的so

我们在网上查找了一下这个so,得知

libusb是一底层的API,可以跨平台实现。

基于libusb可以获取到usb mac的pcm流数据,从而可以读取到音频。

libusb库使用C语言编写,在Android中使用该库需要用到JNI技术。

github : libusb/libusb:用于访问 USB 设备的跨平台库

然后,我们找到了一个libusb的库 jim0608/android_usbaudio: 基于libusb,实现无驱动获取USBAudio

当然,这个库本身是有点问题的,但大体思路可以参考

由于libusb库使用到了JNI,所以我们需要先配置好NDK,其对应版本为21.0.6113669


代码中有一个OnDeviceConnectListener,当把视频线插到车机的USB-A口的时候,

onAttach方法就会被调用,这个时候会调用requestPermission去请求权限。

当连接上的时候,会记录下mCtrlBlockmCtrlBlock

private final USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {

        @Override
        public void onAttach(UsbDevice device) {
            Log.i(TAG, "onAttach: " + device);
            mUSBMonitor.requestPermission(device);
        }

        @Override
        public void onDettach(UsbDevice device) {
            Log.i(TAG, "onDettach: " + device);
        }
    
    	@Override
        public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) {
            Log.i(TAG, "onConnect: " + device);
            for (int interfaceIndex = 0; interfaceIndex < device.getInterfaceCount(); interfaceIndex++) {
                if (device.getInterface(interfaceIndex).getInterfaceClass() == USB_CLASS_AUDIO){
                    mCtrlBlock = ctrlBlock;
                    break;
                }
            }

            mAudioDevice = device;
        }

    	//...省略...
    };

接着,我们就可以去初始化音频了

 mCtrlBlock = mUSBMonitor.getDevice(mDevice);
 mUsbAudio.initAudio(mCtrlBlock);

然后再开始捕获

mUsbAudio.startCapture();

这个时候,我们在手机端播放音乐,车机的音响就会输出声音了。

播放声音其实是用过AudioTrack来播放的

在Android中,播放声音可以用MediaPlayer和AudioTrack

区别如下

MediaPlayerAudioTrack
支持格式MP3,AAC,WAV,OGG,MIDI等已经解码的PCM流,或WAV格式的音频文件(大部分是PCM流)
解码器在framework层创建对应音频解码器不创建解码器,所以只能播放无需解码的WAV文件
联系在framework层还是会创建AudioTrack

这里,有个采样率的问题,得找到合适的采样率,否则会有播放声音不清晰、白噪音等情况。

结合在一起使用

AndroidUSBCameraandroid_usbaudio结合起来,就可以实现既播放视频,同时播放出声音的效果了。

4. AudioTrack基础的使用

最后,介绍下AudioTrack基础的使用

AudioTrack 的构造方法

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) { ... }

streamType 音频流类型

AudioManager.STREAM_MUSIC:用于音乐播放的音频流。
AudioManager.STREAM_SYSTEM:用于系统声音的音频流。
AudioManager.STREAM_RING:用于电话铃声的音频流。
AudioManager.STREAM_VOICE_CALL:用于电话通话的音频流。
AudioManager.STREAM_ALARM:用于警报的音频流。
AudioManager.STREAM_NOTIFICATION:用于通知的音频流。
AudioManager.STREAM_BLUETOOTH_SCO:用于连接到蓝牙电话时的手机音频流。
AudioManager.STREAM_SYSTEM_ENFORCED:在某些国家实施的系统声音的音频流。
AudioManager.STREAM_DTMFDTMF音调的音频流。
AudioManager.STREAM_TTS:文本到语音转换(TTS)的音频流。

sampleRateInHz 采样率

播放的音频每秒钟会有多少次采样,一般为44100,最好是通过代码动态获取采样率,其他常见的采样率还有

96000, 
88200, 
64000, 
48000, 
44100, 
32000, 
24000, 
22050, 
16000, 
12000, 
11025, 
8000, 
7350, 

如果发现播放后声音不清晰、白噪音等情况,可以调整这个采样率值

channelConfig 声道数

单声道AudioFormat.CHANNEL_IN_MONO,双声道AudioFormat.CHANNEL_IN_STEREO,建议选择单声道

audioFormat 数据位宽

只支持AudioFormat.ENCODING_PCM_8BIT(8bit)AudioFormat.ENCODING_PCM_16BIT(16bit)两种,后者支持所有Android手机

bufferSizeInBytes 音频缓冲区大小

建议使用AudioTrack.getMinBufferSize()这个方法获取

int bufSize = AudioTrack.getMinBufferSize(SAMPLE_RATE_HZ,channelConfig, AudioFormat.ENCODING_PCM_16BIT);

mode 播放模式

有两种播放模式:

  • MODE_STATIC : 一次性将所有数据都写入播放缓冲区中,简单高效,一般用于铃声,系统提醒音,内存比较小的。
  • MODE_STREAM : 需要按照一定的时间间隔,不断的写入音频数据,理论上它可以应用于任何音频播放的场景。

AudioTrack 播放示例

初始化

int bufSize = AudioTrack.getMinBufferSize(SAMPLE_RATE_HZ,AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);

track = new AudioTrack(AudioManager.STREAM_MUSIC,
                       SAMPLE_RATE_HZ,
                       channelConfig,
                       AudioFormat.ENCODING_PCM_16BIT,
                       bufSize,
                       AudioTrack.MODE_STREAM);
track.play();

写入数据

public void pcmData(byte[] data) {
    track.write(data, 0, data.length);
}

停止播放,销毁资源

if(audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED){
    audioTrack.stop();
    audioTrack.release();
}

5.本文代码下载

本文Demo下载地址 : Android UVC USBCamera投屏Demo

参考 :
一篇文章带你了解Android Usb摄像头
这可能是介绍Android UvcCamera最详细的文章了
Android音频系统AudioTrack使用方法详解
ffmpeg开发之旅(8):Android UVC Camera(USB摄像头)开发核心技术详解
Android音视频录制与播放功能简述
Android UCV 同时打开多路摄像头
Android从USB声卡录制高质量音频-----使用libusb读取USB声卡数据
基于libusb库、uac协议,获取Audio声音数据
Android/linux从usb声卡获取音频(使用libusb库)—监听“纯麦”(五)

有关Android USBCamera投屏 - 利用UVC协议将手机上的画面有线投屏到Android车机的屏幕上的更多相关文章

  1. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  2. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  3. ruby - HTTP POST 上的 SSL 错误(未知协议(protocol)) - 2

    尝试通过SSL连接到ImgurAPI时出现错误。这是代码和错误:API_URI=URI.parse('https://api.imgur.com')API_PUBLIC_KEY='Client-ID--'ENDPOINTS={:image=>'/3/image',:gallery=>'/3/gallery'}#Public:Uploadanimage##args-Theimagepathfortheimagetoupload#defupload(image_path)http=Net::HTTP.new(API_URI.host)http.use_ssl=truehttp.verify

  4. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  5. 网络实验之RIPV2协议(一) - 2

    一、RIPV2协议简介  RIP(RoutingInformationProtocol)路由协议是一种相对古老,在小型以及同介质网络中得到了广泛应用的一种路由协议。RIP采用距离向量算法,是一种距离向量协议。RIP-1是有类别路由协议(ClassfulRoutingProtocol),它只支持以广播方式发布协议报文。RIP-1的协议报文无法携带掩码信息,它只能识别A、B、C类这样的自然网段的路由,因此RIP-1不支持非连续子网(DiscontiguousSubnet)。RIP-2是一种无类别路由协议(ClasslessRoutingProtocol),支持路由标记,在路由策略中可根据路由标记对

  6. iOS快捷指令:执行Python脚本(利用iSH Shell) - 2

    文章目录前言核心逻辑配置iSH安装Python创建Python脚本配置启动文件测试效果快捷指令前言iOS快捷指令所能做的操作极为有限。假如快捷指令能运行Python程序,那么可操作空间就瞬间变大了。iSH是一款免费的iOS软件,它模拟了一个类似Linux的命令行解释器。我们将在iSH中运行Python程序,然后在快捷指令中获取Python程序的输出。核心逻辑我们用一个“获取当前日期”的Python程序作为演示(其实快捷指令中本身存在“获取当前日期”的操作,因而此需求可以不用Python,这里仅仅为了演示方便),核心代码如下。>>>importtime>>>time.strftime('%Y-%

  7. ruby-on-rails - 共享主机上的 Rails 应用程序? - 2

    有没有一种方法可以在安装了Ruby的共享主机上部署本地制作的Rails应用程序? 最佳答案 部署Capistrano和(Mongrel或Passenger...最好是后者...或者,如果你必须,FastCGI尽管FastCGI在Rails中非常慢)服务。如果您有一个共享主机,您将不得不提供您的Rails版本和所有的gem,因为您可能无权安装gem。即使您这样做了,您也不希望Rails选择旧版本的东西。 关于ruby-on-rails-共享主机上的Rails应用程序?,我们在StackOv

  8. ruby - Sidekiq 可以利用多个 CPU 内核吗? - 2

    我是Sidekiq的新手,将它与AmazonEC2实例上的Ruby结合使用,以使用ImageMagick处理图像来完成一些工作。在运行它时,我意识到每个工作人员都在同一个核心上运行。我使用EC2c3.2xlarge机器,它们有8个内核。它显示CPU使用率为15%,但一个内核使用了100%,而其他内核使用了0%。Sidekiq可以为不同的worker使用不同的CPU内核吗?如果可以,这种低效率是由ImageMagic造成的吗?我怎样才能让它使用其他内核? 最佳答案 如果您想使用MRI使用多个内核,则需要启动多个Sidekiq进程;为您

  9. Android Studio开发之使用内容组件Content获取通讯信息讲解及实战(附源码 包括添加手机联系人和发短信) - 2

    运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid

  10. PLUS模型和InVEST模型生态系统服务多情景模拟预测、ArcGIS空间数据处理、空间分析与制图、土地利用时空变化 - 2

    查看原文>>>基于”PLUS模型+“生态系统服务多情景模拟预测实践技术应用目录第一章、理论基础与软件讲解第二章、数据获取与制备第三章、土地利用格局模拟第四章、生态系统服务评估第五章、时空变化及驱动机制分析第六章、论文撰写技巧及案例分析基于ArcGISPro、Python、USLE、INVEST模型等多技术融合的生态系统服务构建生态安全格局基于生态系统服务(InVEST模型)的人类活动、重大工程生态成效评估、论文写作等具体应用基于ArcGISPro、R、INVEST等多技术融合下生态系统服务权衡与协同动态分析实践应用    本文从数据、方法、实践三方面对生态系统服务多情景预测进行讲解。内容涵盖多

随机推荐