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的音频输入吗?

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

android uvc 与soc协议介绍 安卓手机uvc协议_投屏

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

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

android uvc 与soc协议介绍 安卓手机uvc协议_android_02

我们在网上查找了一下这个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

android uvc 与soc协议介绍 安卓手机uvc协议_usbaudio_03


android uvc 与soc协议介绍 安卓手机uvc协议_usbaudio_04

代码中有一个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

区别如下

MediaPlayer

AudioTrack

支持格式

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_DTMF:DTMF音调的音频流。
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.本文代码下载