Android USBCamera投屏 - 利用UVC协议将手机上的画面有线投屏到Android车机的屏幕上
1. 背景
一個需求 : 要將手機上的畫面和音頻 投屏 到 車機的Android屏幕上。
車機有一個支持OTG的USB-A口,由于設備有限,我們有一個USB-A轉HDMI轉接口,一跟HDMI線,一個USB-C的拓展塢 (包括HDMI口,兩個USB-A口,一個網口),我們將這幾根線接在一起,成功將手機和車機連在了一起。
接著,我們在網上找到了一個 jiangdongguo/AndroidUSBCamera ,我們使用Android Studio打開編譯安裝到車機,并將車機的Usb mode從Device mode切換為Host Mode,這個時候,打開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 modemUVCCamera.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() {@Overridepublic void onEncodeResult(byte[] data, int offset, int length, long timestamp, int type) {// type = 1,h264 video streamif (type == 1) {FileUtils.putFileStream(data, offset, length);}// type = 0,aac audio streamif(type == 0) {trackplayer.write(data, offset, length);//往track中寫數據}}@Overridepublic 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去請求權限。
當連接上的時候,會記錄下mCtrlBlock和mCtrlBlock
private final USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {@Overridepublic void onAttach(UsbDevice device) {Log.i(TAG, "onAttach: " + device);mUSBMonitor.requestPermission(device);}@Overridepublic void onDettach(UsbDevice device) {Log.i(TAG, "onDettach: " + device);}@Overridepublic 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
區別如下
| 支持格式 | MP3,AAC,WAV,OGG,MIDI等 | 已經解碼的PCM流,或WAV格式的音頻文件(大部分是PCM流) |
| 解碼器 | 在framework層創建對應音頻解碼器 | 不創建解碼器,所以只能播放無需解碼的WAV文件 |
| 聯系 | 在framework層還是會創建AudioTrack |
這里,有個采樣率的問題,得找到合適的采樣率,否則會有播放聲音不清晰、白噪音等情況。
結合在一起使用
將AndroidUSBCamera和android_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.本文代碼下載
本文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车机的屏幕上的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP中路径怎么用?
- 下一篇: android sina oauth2.