非 ROOT 安卓内录
引言
最近開發(fā)的遠程控制功能需要增加音頻采集的功能,而Google為了保護唱片協(xié)會的利益,不允許獲取系統(tǒng)原始輸出的音頻。如果有Root權(quán)限的話,你自然可以輕易的做到這件事。但是我們的使用場景是不能獲取Root權(quán)限的,所以我們借助了一些硬件的支持,最終達到了截獲手機原始音頻輸出的效果。具體的實現(xiàn)方案也是經(jīng)歷了幾個發(fā)展階段,接下里我就按時間順序介紹一下這部分的發(fā)展歷程。更多相關(guān)文章和其他文章均收錄于貝貝貓的文章目錄。
方案一:外接聲卡
方案介紹
這個方案的基本思路如下圖,通過音頻線將手機的音頻數(shù)據(jù)傳入聲卡,然后將聲卡和服務(wù)器通過USB相連,最終從服務(wù)器上截獲該聲卡的音頻數(shù)據(jù)。
為了達到這個效果,必須將每款手機和與其相連的聲卡建立綁定關(guān)系,這就需要每一個聲卡都有一個唯一的序列號,這樣當(dāng)我們需要截獲某一款手機的音頻時,我們只需要從綁定關(guān)系表中查到與這款手機相連的聲卡序列號,然后通過該序列號找到對應(yīng)的聲卡設(shè)備并進行錄制。遺憾的是,在現(xiàn)有的產(chǎn)品中,我們沒找到具有唯一序列號的USB聲卡產(chǎn)品,我們只找到了HS-100B,它雖然沒有唯一序列號,但是我們可以通過外接EEPROM的方式,寫入自定義內(nèi)容作為序列號。
所以,我們參考了HS-100B的產(chǎn)品說明書,從中我們得知EEPROM需要存儲的內(nèi)容如下圖,其中畫紅線的部分可以用來定義聲卡的序列號,我們之所以用Product String作為序列號的存儲區(qū)域,主要是因為這部分內(nèi)容可以通過FFMPEG的設(shè)備顯示功能展示出來,我們只需要做一個字符串匹配就能定位需要截獲的聲卡設(shè)備。
EEPROM寫入數(shù)據(jù)方式
顯示序列號的方式
- Mac: ffmpeg -f avfoundation -list_devices true -i “”
- Linux: aplay -l (yum install alsa-utils alsa-lib)
方案總結(jié)
這個方案總的來說,實現(xiàn)起來是比較麻煩的,雖然他可以獲取到多聲道的音頻數(shù)據(jù),但是其工作量太大,既要燒入數(shù)據(jù),又要焊接電路,而且還要維護手機與音頻采集卡的映射關(guān)系。
方案二:音頻輸出轉(zhuǎn)接音頻輸入
介紹
這個實現(xiàn)方案是受一款現(xiàn)有耳機產(chǎn)品的啟發(fā),我們做了一個超級簡易版。基本思路如下圖,通過一個音頻公頭接線端子,將手機的音頻輸出接入到麥克風(fēng)輸入中,然后通過手機中的APP錄制麥克風(fēng)的輸入從而達到內(nèi)錄的效果。
我們參考了Google的3.5毫米耳機規(guī)范,將音頻公頭接線端子的左右聲道連接一個電阻并連入地線,然后選取左聲道連接一個電阻接入MIC,從而達到截獲左聲道輸出的效果。
然后就是通過APP錄制音頻數(shù)據(jù)的部分了,首先我們需要構(gòu)造一個AudioRecord對象,其中需要的最小錄音緩存buffer大小可以通過getMinBufferSize方法得到。如果buffer容量過小,將導(dǎo)致對象構(gòu)造的失敗。
其中,音頻源我們選擇public static final int MIC = 1;,采樣率我使用了44100,因為我們這個方案只能截獲單聲道的數(shù)據(jù),所以聲道設(shè)置為CHANNEL_IN_MONO,采樣大小我選用了ENCODING_PCM_16BIT。設(shè)置完采集參數(shù)之后,就開始錄音并輸出PCM數(shù)據(jù)。
byte data[] = new byte[recordBufSize];FileOutputStream os = new FileOutputStream(filename);while (isRecording) {read = audioRecord.read(data, 0, recordBufSize);if (AudioRecord.ERROR_INVALID_OPERATION != read) {os.write(data);}}APP這部分,我覺得簡單的描述一下基本操作就夠了,剩下的就是通過Socket將音頻數(shù)據(jù)傳輸出去。
總結(jié)
這個方案相對于方案一來說就簡單了很多,接幾個電阻就能直接使用了,雖然目前還沒找到多聲道錄音的方式,但是已經(jīng)基本滿足我們的使用需要了。值得一提的是,這兩個方案都有一個共同的問題,就是需要手機有3.5mm耳機接口,而近來的安卓手機都在逐漸的移除3.5mm耳機接口。這時候你可能會說,可以通過一個轉(zhuǎn)接頭將耳機接口轉(zhuǎn)接到Type-C接口啊,可是因為我們的業(yè)務(wù)中需要通過USB來建立ADB連接,而且要用其給手機充電,所以Type-C接口會一直連接在服務(wù)器上。為了讓這類手機也能捕獲到音頻數(shù)據(jù),我們調(diào)研了第三種方案,通過藍牙傳輸音頻數(shù)據(jù)。
方案三:藍牙獲取音頻數(shù)據(jù)
相關(guān)知識
在介紹整個方案之前,我覺得有必要簡單描述一下藍牙傳輸音頻時使用到的A2DP協(xié)議,以及我們用到的音頻服務(wù)代理PulseAudio。
A2DP
A2DP全名是Advanced Audio Distribution Profile 藍牙音頻傳輸模型協(xié)定。 簡單地說它就是一個音頻傳輸協(xié)議,藍牙耳機都是通過該協(xié)議來接收手機上傳送過來的音頻數(shù)據(jù)并播放的。這里你可能會有疑問,一般來說,都是手機將音頻數(shù)據(jù)傳輸給藍牙耳機,或者PC將音頻數(shù)據(jù)傳輸給藍牙耳機,那么,到底是怎么讓手機將音頻數(shù)據(jù)傳輸給電腦呢?其實,A2DP協(xié)議中有一個角色的概念,通訊雙方在建立連接的時候會確立自己的角色,手機上自帶的藍牙模塊一般都是音頻數(shù)據(jù)源這個角色(Audio Source),而藍牙耳機默認(rèn)的角色是音頻接收端(Audio Sink),所以,要想讓手機通過藍牙發(fā)送音頻數(shù)據(jù)給服務(wù)器上的藍牙模塊,就需要修改服務(wù)器上的藍牙配置文件,讓它以音頻接收端(Audio Sink)的角色建立連接。
PulseAudio
PulseAudio 是在GNOME或KDE等桌面環(huán)境中廣泛使用的音頻服務(wù)。它在內(nèi)核音頻組件(比如ALSA和OSS)和音頻程序之間充當(dāng)代理的角色。在我們的場景中,主要用到了它的一個藍牙設(shè)備發(fā)現(xiàn)模塊,來自動地在藍牙連接建立完成之后通過A2DP協(xié)議虛擬出一塊聲音設(shè)備。
BlueZ
BlueZ是Linux官方藍牙協(xié)議棧。它是一個基于GNU General Public License (GPL)發(fā)布的開源項目,從Linux2.4.6開始便成為Linux 內(nèi)核的一部分。我們在Linux上操作Bluetooth實際上就是它提供的支持。
思路
這個方案的基本思路如下圖,手機扮演一個Audio Source的角色(A2DP發(fā)送端),服務(wù)器外接一個藍牙模塊扮演Audio Sink的角色(A2DP接收端),將手機與服務(wù)器藍牙模塊配對后,通過PulseAudio的藍牙模塊將服務(wù)器上外接的藍牙(A2DP接收端)虛擬為一個音頻源,進行聲音采集,這個方案目前還有一些問題,我后面會介紹。
PipeLine
Remote Device-SRC ---> SINK-Bluetooth-SRC ---> SINK-PulseAudioBlueToothModule-SRC ---> SINK-MyApp
其中SRC代表數(shù)據(jù)源,SINK代表數(shù)據(jù)接收方。
確立AudioSink角色
這個方案的重點是如何讓服務(wù)器上的藍牙模塊扮演Audio Sink角色,這就涉及到Linux上的BlueZ模塊。
這里我們需要編輯/etc/bluetooth/audio.conf,在[General]區(qū)段加入Enable=Source,并且關(guān)閉其作為Audio Source角色的能力,加入Disable=Socket,最終配置文件的內(nèi)容如下:
完成了藍牙音頻角色配置之后,重啟藍牙服務(wù)systemctl restart bluetooth。
設(shè)置PulseAudio
接著,我們還需要對PulseAudio進行一些設(shè)置,添加module-bluetooth-discover和module-bluetooth-policy模塊的支持,這個模塊默認(rèn)是加載的,如果沒有加載這個模塊的話,可以通過pactl load-module module-bluetooth-discover手動加載,或者修改PulseAudio的配置文件/etc/pulse/default.pa加入如下內(nèi)容。
### Automatically load driver modules for Bluetooth hardware.ifexists module-bluetooth-policy.soload-module module-bluetooth-policy.endif.ifexists module-bluetooth-discover.soload-module module-bluetooth-discover.endif連接藍牙
配置完BlueZ和PulseAudio之后,剩下的工作就是配對藍牙設(shè)備并建立連接了。首先我們需要確認(rèn)一下藍牙控制器是否工作正常。這里hci0是藍牙控制器的名字,第三行的UP表示其已經(jīng)啟動。如果該藍牙控制器未啟動,您可以通過hciconfig hci0 up來進行啟動。
root # hciconfig -ahci0: Type: BR/EDR Bus: USBBD Address: 00:02:72:2F:A9:33 ACL MTU: 1021:8 SCO MTU: 64:1UP RUNNING PSCANRX bytes:1166 acl:0 sco:0 events:43 errors:0TX bytes:960 acl:0 sco:0 commands:43 errors:0Features: 0xbf 0xfe 0xcf 0xfe 0xdb 0xff 0x7b 0x87Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3Link policy: RSWITCH SNIFFLink mode: SLAVE ACCEPTName: 'BlueZ 5.21'Class: 0x000104Service Classes: UnspecifiedDevice Class: Computer, Desktop workstationHCI Version: 4.0 (0x6) Revision: 0x1000LMP Version: 4.0 (0x6) Subversion: 0x220eManufacturer: Broadcom Corporation (15)當(dāng)然,您也可以通過/etc/bluetooth/main.conf設(shè)置藍牙模塊開機自動啟動。
[Policy]AutoEnable=true確認(rèn)完藍牙控制器的狀態(tài)之后,就是完整的藍牙配對過程如下:
啟動藍牙控制器
user $ bluetoothctl
列出所有藍牙控制器
[bluetooth]# list
顯示藍牙控制器的相關(guān)信息
[bluetooth]# show controller_mac_address
選擇要操作的藍牙控制器(可能插著多個藍牙模塊)
[bluetooth]# select controller_mac_address
供電
[bluetooth]# power on
開啟代理
設(shè)置藍牙控制器可以被發(fā)現(xiàn)并且可以配對(3分鐘有效)
[bluetooth]# discoverable on[bluetooth]# pairable on掃描設(shè)備
[bluetooth]# scan on
列出發(fā)現(xiàn)的設(shè)備
[bluetooth]# devices
配對設(shè)備
[bluetooth]# pair device_mac_address
如果有必要的話輸入PIN
[agent]PIN code: ####
允許鏈接權(quán)限
[agent]Authorize service service_uuid (yes/no): yes
設(shè)置信任設(shè)備
[bluetooth]# trust device_mac_address
連接設(shè)備
[bluetooth]# connect device_mac_address
顯示設(shè)備的相關(guān)信息
[bluetooth]# info device_mac_address
退出
[bluetooth]# quit
確認(rèn)結(jié)果
藍牙連接成功之后,PulseAudio會自動幫我們虛擬出聲音設(shè)備,我們可以通過pactl list cards來查看虛擬出來的聲卡設(shè)備。可以看到當(dāng)前的Profile是a2dp_source,如果您的聲卡profile不是a2dp_source的話可以通過pactl set-card-profile 10 a2dp_source來指定。
root # pactl list cards ... Card #2 Name: bluez_card.44_80_EB_26_0C_73 Driver: module-bluez5-device.c Owner Module: 23 Properties:device.description = "Nexus 6"device.string = "44:80:EB:26:0C:73"device.api = "bluez"device.class = "sound"device.bus = "bluetooth"device.form_factor = "phone"bluez.path = "/org/bluez/hci0/dev_44_80_EB_26_0C_73"bluez.class = "0x5a020c"bluez.alias = "Nexus 6"device.icon_name = "audio-card-bluetooth" Profiles:a2dp_source: High Fidelity Capture (A2DP Source) (sinks: 0, sources: 1, priority: 10, available: yes)headset_audio_gateway: Headset Audio Gateway (HSP/HFP) (sinks: 1, sources: 1, priority: 20, available: no)off: Off (sinks: 0, sources: 0, priority: 0, available: yes) Active Profile: a2dp_source Ports:phone-output: Phone (priority: 0, latency offset: 0 usec, not available)Part of profile(s): headset_audio_gatewayphone-input: Phone (priority: 0, latency offset: 0 usec, available)Part of profile(s): a2dp_source, headset_audio_gateway當(dāng)手機端有聲音播放時,我們可以通過pactl list sources來查看Audio Source。我們在APP中就是使用這個Audio Source作為音頻采集源。
root # pactl list sources ... Source #15 State: RUNNING Name: bluez_source.44_80_EB_26_0C_73.a2dp_source Description: Nexus 6 Driver: module-bluez5-device.c Sample Specification: s16le 2ch 44100Hz Channel Map: front-left,front-right Owner Module: 23 Mute: no Volume: front-left: 65536 / 100% / 0.00 dB, front-right: 65536 / 100% / 0.00 dBbalance 0.00 Base Volume: 65536 / 100% / 0.00 dB Monitor of Sink: n/a Latency: 25000 usec, configured 135294 usec Flags: HARDWARE DECIBEL_VOLUME LATENCY Properties:bluetooth.protocol = "a2dp_source"device.description = "Nexus 6"device.string = "44:80:EB:26:0C:73"device.api = "bluez"device.class = "sound"device.bus = "bluetooth"device.form_factor = "phone"bluez.path = "/org/bluez/hci0/dev_44_80_EB_26_0C_73"bluez.class = "0x5a020c"bluez.alias = "Nexus 6"device.icon_name = "audio-card-bluetooth" Ports:phone-input: Phone (priority: 0, available) Active Port: phone-input Formats:pcm使用技巧
此外在使用PulseAudio時,我還用到了update-source-proplist來給聲卡打標(biāo)記,使我可以通過字符串匹配找到指定設(shè)備連接的聲卡。
root # echo "update-source-proplist bluez_source.44_80_EB_26_0C_73.a2dp_source device.description=\"44_80_EB_26_0C_73\"" | pacmd root # pactl list sources ... Source #16 State: RUNNING Name: bluez_source.44_80_EB_26_0C_73.a2dp_source Description: 44_80_EB_26_0C_73 Driver: module-bluez5-device.c Sample Specification: s16le 2ch 44100Hz Channel Map: front-left,front-right Owner Module: 23 Mute: no Volume: front-left: 65536 / 100% / 0.00 dB, front-right: 65536 / 100% / 0.00 dBbalance 0.00 Base Volume: 65536 / 100% / 0.00 dB Monitor of Sink: n/a Latency: 25000 usec, configured 135294 usec Flags: HARDWARE DECIBEL_VOLUME LATENCY Properties:bluetooth.protocol = "a2dp_source"device.description = "44_80_EB_26_0C_73"device.string = "44:80:EB:26:0C:73"device.api = "bluez"device.class = "sound"device.bus = "bluetooth"device.form_factor = "phone"bluez.path = "/org/bluez/hci0/dev_44_80_EB_26_0C_73"bluez.class = "0x5a020c"bluez.alias = "Nexus 6"device.icon_name = "audio-card-bluetooth" Ports:phone-input: Phone (priority: 0, available) Active Port: phone-input Formats:pcm綜述
這個方案我只是達到了『能跑通』的程度,在測試的時候發(fā)現(xiàn)如果手機端沒有聲音時,該虛擬聲卡的Active Profile會變?yōu)锳ctive Profile: off,并且PulseAudio Source消失,這樣我們的APP中會丟失聲音采集設(shè)備,繼而切換到默認(rèn)聲音采集卡。此外這個方案也需要維護一個由聲卡到手機的映射關(guān)系,不過我覺得大部分情況下可以通過查看藍牙模塊已配對的手機的方式,快速得到這個對應(yīng)關(guān)系。
將來的工作
因為方案三的調(diào)查工作基本上都是在我業(yè)務(wù)之余,擠出時間進行的,后面因為一些原因中斷了更進一步的調(diào)查。不過,以我現(xiàn)在的理解來看的話,應(yīng)該可以實現(xiàn)一個基于PulseAudio的AudioDeviceModule,來解決當(dāng)手機端沒有聲音時PulseAudio Source消失的情況,或者參考bluez-alsa直接通過BlueZ構(gòu)建一個ALSA設(shè)備。此外,我覺得還應(yīng)該通過類似于tinyb和lbt4j的庫,來達到在Java中調(diào)度藍牙模塊的效果。
參考內(nèi)容
[1] 如何用 Android 手機完美錄屏?收下這份「錄屏 + 直播」全面指南
[2] Android音視頻之AudioRecord
[3] a2dp-stream
[4] ubuntu-bluetooth-guide
[5] BlueZ_5
[6] BlueZ5_and_A2DP
[7] PulseAudio
[8] Bluez Secret
[9] Bluetooth
[10] A2DP學(xué)習(xí)筆記
總結(jié)
以上是生活随笔為你收集整理的非 ROOT 安卓内录的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。