天天看點

非 ROOT 安卓内錄

引言

最近開發的遠端控制功能需要增加音頻采集的功能,而Google為了保護唱片協會的利益,不允許擷取系統原始輸出的音頻。如果有Root權限的話,你自然可以輕易的做到這件事。但是我們的使用場景是不能擷取Root權限的,是以我們借助了一些硬體的支援,最終達到了截獲手機原始音頻輸出的效果。具體的實作方案也是經曆了幾個發展階段,接下裡我就按時間順序介紹一下這部分的發展曆程。

方案一:外接聲霸卡

方案介紹

這個方案的基本思路如下圖,通過音頻線将手機的音頻資料傳入聲霸卡,然後将聲霸卡和伺服器通過USB相連,最終從伺服器上截獲該聲霸卡的音頻資料。

非 ROOT 安卓内錄

為了達到這個效果,必須将每款手機和與其相連的聲霸卡建立綁定關系,這就需要每一個聲霸卡都有一個唯一的序列号,這樣當我們需要截獲某一款手機的音頻時,我們隻需要從綁定關系表中查到與這款手機相連的聲霸卡序列号,然後通過該序列号找到對應的聲霸卡裝置并進行錄制。遺憾的是,在現有的産品中,我們沒找到具有唯一序列号的USB聲霸卡産品,我們隻找到了

HS-100B

,它雖然沒有唯一序列号,但是我們可以通過外接

EEPROM

的方式,寫入自定義内容作為序列号。

是以,我們參考了

HS-100B的産品說明書

,從中我們得知

需要存儲的内容如下圖,其中畫紅線的部分可以用來定義聲霸卡的序列号,我們之是以用Product String作為序列号的存儲區域,主要是因為這部分内容可以通過FFMPEG的裝置顯示功能展示出來,我們隻需要做一個字元串比對就能定位需要截獲的聲霸卡裝置。

非 ROOT 安卓内錄

EEPROM寫入資料方式

  1. 購買 EZP2013燒錄器
  2. 安裝 燒錄軟體
  3. 選擇EEPROM類型
    非 ROOT 安卓内錄
  4. 寫入 資料

顯示序列号的方式

  • Mac: ffmpeg -f avfoundation -list_devices true -i ""
  • Linux: aplay -l (yum install alsa-utils alsa-lib)

方案總結

這個方案總的來說,實作起來是比較麻煩的,雖然他可以擷取到多聲道的音頻資料,但是其工作量太大,既要燒入資料,又要焊接電路,而且還要維護手機與音頻采集卡的映射關系。

方案二:音頻輸出轉接音頻輸入

介紹

這個實作方案是受一款現有

耳機産品

的啟發,我們做了一個超級簡易版。基本思路如下圖,通過一個

音頻公頭接線端子

,将手機的音頻輸出接入到麥克風輸入中,然後通過手機中的APP錄制麥克風的輸入進而達到内錄的效果。

非 ROOT 安卓内錄

我們參考了Google的

3.5毫米耳機規範

,将

的左右聲道連接配接一個電阻并連入地線,然後選取左聲道連接配接一個電阻接入MIC,進而達到截獲左聲道輸出的效果。

非 ROOT 安卓内錄

然後就是通過APP錄制音頻資料的部分了,首先我們需要構造一個AudioRecord對象,其中需要的最小錄音緩存buffer大小可以通過getMinBufferSize方法得到。如果buffer容量過小,将導緻對象構造的失敗。

int recordBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, EncodingBitRate);
   AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, EncodingBitRate, recordBufSize);           

其中,音頻源我們選擇

public static final int MIC = 1;

,采樣率我使用了

44100

,因為我們這個方案隻能截獲單聲道的資料,是以聲道設定為

CHANNEL_IN_MONO

,采樣大小我選用了

ENCODING_PCM_16BIT

。設定完采集參數之後,就開始錄音并輸出PCM資料。

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将音頻資料傳輸出去。

總結

這個方案相對于方案一來說就簡單了很多,接幾個電阻就能直接使用了,雖然目前還沒找到多聲道錄音的方式,但是已經基本滿足我們的使用需要了。值得一提的是,這兩個方案都有一個共同的問題,就是需要手機有3.5mm耳機接口,而近來的安卓手機都在逐漸的移除3.5mm耳機接口。這時候你可能會說,可以通過一個轉接頭将耳機接口轉接到Type-C接口啊,可是因為我們的業務中需要通過USB來建立ADB連接配接,而且要用其給手機充電,是以Type-C接口會一直連接配接在伺服器上。為了讓這類手機也能捕獲到音頻資料,我們調研了第三種方案,通過藍牙傳輸音頻資料。

方案三:藍牙擷取音頻資料

相關知識

在介紹整個方案之前,我覺得有必要簡單描述一下藍牙傳輸音頻時使用到的A2DP協定,以及我們用到的音頻服務代理PulseAudio。

A2DP

A2DP全名是Advanced Audio Distribution Profile 藍牙音頻傳輸模型協定。 簡單地說它就是一個音頻傳輸協定,藍牙耳機都是通過該協定來接收手機上傳送過來的音頻資料并播放的。這裡你可能會有疑問,一般來說,都是手機将音頻資料傳輸給藍牙耳機,或者PC将音頻資料傳輸給藍牙耳機,那麼,到底是怎麼讓手機将音頻資料傳輸給電腦呢?其實,A2DP協定中有一個角色的概念,通訊雙方在建立連接配接的時候會确立自己的角色,手機上自帶的藍牙子產品一般都是音頻資料源這個角色(Audio Source),而藍牙耳機預設的角色是音頻接收端(Audio Sink),是以,要想讓手機通過藍牙發送音頻資料給伺服器上的藍牙子產品,就需要修改伺服器上的藍牙配置檔案,讓它以音頻接收端(Audio Sink)的角色建立連接配接。

PulseAudio

PulseAudio 是在GNOME或KDE等桌面環境中廣泛使用的音頻服務。它在核心音頻元件(比如ALSA和OSS)和音頻程式之間充當代理的角色。在我們的場景中,主要用到了它的一個藍牙裝置發現子產品,來自動地在藍牙連接配接建立完成之後通過A2DP協定虛拟出一塊聲音裝置。

BlueZ

BlueZ是Linux官方藍牙協定棧。它是一個基于GNU General Public License (GPL)釋出的開源項目,從Linux2.4.6開始便成為Linux 核心的一部分。我們在Linux上操作Bluetooth實際上就是它提供的支援。

思路

這個方案的基本思路如下圖,手機扮演一個Audio Source的角色(A2DP發送端),伺服器外接一個藍牙子產品扮演Audio Sink的角色(A2DP接收端),将手機與伺服器藍牙子產品配對後,通過PulseAudio的藍牙子產品将伺服器上外接的藍牙(A2DP接收端)虛拟為一個音頻源,進行聲音采集,這個方案目前還有一些問題,我後面會介紹。

非 ROOT 安卓内錄

PipeLine

Remote Device-SRC ---> SINK-Bluetooth-SRC ---> SINK-PulseAudioBlueToothModule-SRC ---> SINK-MyApp

其中

SRC

代表資料源,

SINK

代表資料接收方。

确立AudioSink角色

這個方案的重點是如何讓伺服器上的藍牙子產品扮演Audio Sink角色,這就涉及到Linux上的

子產品。

這裡我們需要編輯

/etc/bluetooth/audio.conf

,在

[General]

區段加入

Enable=Source

,并且關閉其作為Audio Source角色的能力,加入

Disable=Socket

,最終配置檔案的内容如下:

[General]
   Enable=Source
   Disable=Socket           

完成了藍牙音頻角色配置之後,重新開機藍牙服務

systemctl restart bluetooth

設定PulseAudio

接着,我們還需要對PulseAudio進行一些設定,添加

module-bluetooth-discover

module-bluetooth-policy

子產品的支援,這個子產品預設是加載的,如果沒有加載這個子產品的話,可以通過

pactl load-module module-bluetooth-discover

手動加載,或者修改PulseAudio的配置檔案

/etc/pulse/default.pa

加入如下内容。

### Automatically load driver modules for Bluetooth hardware
   .ifexists module-bluetooth-policy.so
   load-module module-bluetooth-policy
   .endif

   .ifexists module-bluetooth-discover.so
   load-module module-bluetooth-discover
   .endif           

連接配接藍牙

配置完BlueZ和PulseAudio之後,剩下的工作就是配對藍牙裝置并建立連接配接了。首先我們需要确認一下藍牙控制器是否工作正常。這裡

hci0

是藍牙控制器的名字,第三行的UP表示其已經啟動。如果該藍牙控制器未啟動,您可以通過

hciconfig hci0 up

來進行啟動。

root # hciconfig -a
   hci0:   Type: BR/EDR  Bus: USB
           BD Address: 00:02:72:2F:A9:33  ACL MTU: 1021:8  SCO MTU: 64:1
           UP RUNNING PSCAN
           RX bytes:1166 acl:0 sco:0 events:43 errors:0
           TX bytes:960 acl:0 sco:0 commands:43 errors:0
           Features: 0xbf 0xfe 0xcf 0xfe 0xdb 0xff 0x7b 0x87
           Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
           Link policy: RSWITCH SNIFF
           Link mode: SLAVE ACCEPT
           Name: 'BlueZ 5.21'
           Class: 0x000104
           Service Classes: Unspecified
           Device Class: Computer, Desktop workstation
           HCI Version: 4.0 (0x6)  Revision: 0x1000
           LMP Version: 4.0 (0x6)  Subversion: 0x220e
           Manufacturer: Broadcom Corporation (15)           

當然,您也可以通過

/etc/bluetooth/main.conf

設定藍牙子產品開機自動啟動。

[Policy]
   AutoEnable=true           

确認完藍牙控制器的狀态之後,就是完整的藍牙配對過程如下:

啟動藍牙控制器

user $ bluetoothctl

列出所有藍牙控制器

[bluetooth]# list

顯示藍牙控制器的相關資訊

[bluetooth]# show controller_mac_address

選擇要操作的藍牙控制器(可能插着多個藍牙子產品)

[bluetooth]# select controller_mac_address

供電

[bluetooth]# power on

開啟代理

[bluetooth]# agent on
   [bluetooth]# default-agent           

設定藍牙控制器可以被發現并且可以配對(3分鐘有效)

[bluetooth]# discoverable on
   [bluetooth]# pairable on           

掃描裝置

[bluetooth]# scan on

列出發現的裝置

[bluetooth]# devices

配對裝置

[bluetooth]# pair device_mac_address

如果有必要的話輸入PIN

[agent]PIN code: ####

允許連結權限

[agent]Authorize service service_uuid (yes/no): yes

設定信任裝置

[bluetooth]# trust device_mac_address

連接配接裝置

[bluetooth]# connect device_mac_address

顯示裝置的相關資訊

[bluetooth]# info device_mac_address

退出

[bluetooth]# quit

确認結果

藍牙連接配接成功之後,PulseAudio會自動幫我們虛拟出聲音裝置,我們可以通過

pactl list cards

來檢視虛拟出來的聲霸卡裝置。可以看到目前的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_gateway
 phone-input: Phone (priority: 0, latency offset: 0 usec, available)
  Part of profile(s): a2dp_source, headset_audio_gateway           

當手機端有聲音播放時,我們可以通過

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 dB
  balance 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

來給聲霸卡打标記,使我可以通過字元串比對找到指定裝置連接配接的聲霸卡。

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 dB
    balance 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           

綜述

這個方案我隻是達到了『能跑通』的程度,在測試的時候發現如果手機端沒有聲音時,該虛拟聲霸卡的Active Profile會變為

Active Profile: off

,并且PulseAudio Source消失,這樣我們的APP中會丢失聲音采集裝置,繼而切換到預設聲音采集卡。此外這個方案也需要維護一個由聲霸卡到手機的映射關系,不過我覺得大部分情況下可以通過檢視藍牙子產品已配對的手機的方式,快速得到這個對應關系。

将來的工作

因為方案三的調查工作基本上都是在我業務之餘,擠出時間進行的,後面因為一些原因中斷了更進一步的調查。不過,以我現在的了解來看的話,應該可以實作一個基于PulseAudio的AudioDeviceModule,來解決當手機端沒有聲音時PulseAudio Source消失的情況,或者參考

bluez-alsa

直接通過BlueZ建構一個ALSA裝置。此外,我覺得還應該通過類似于

tinyb lbt4j

的庫,來達到在Java中排程藍牙子產品的效果。

文章說明

更多有價值的文章均收錄于

貝貝貓的文章目錄
非 ROOT 安卓内錄

版權聲明: 本部落格所有文章除特别聲明外,均采用 BY-NC-SA 許可協定。轉載請注明出處!

創作聲明: 本文基于下列所有參考内容進行創作,其中可能涉及複制、修改或者轉換,圖檔均來自網絡,如有侵權請聯系我,我會第一時間進行删除。

參考内容

[1]

如何用 Android 手機完美錄屏?收下這份「錄屏 + 直播」全面指南

[2]

Android音視訊之AudioRecord

[3]

a2dp-stream

[4]

ubuntu-bluetooth-guide

[5]

BlueZ_5

[6]

BlueZ5_and_A2DP

[7]

[8]

Bluez Secret

[9]

Bluetooth

[10]

A2DP學習筆記