暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

非ROOT安卓内录

贝贝猫技术分享 2019-10-19
482

引言

最近开发的远程控制功能需要增加音频采集的功能,而 Google 为了保护唱片协会的利益,不允许获取系统原始输出的音频。如果有 Root 权限的话,你自然可以轻易的做到这件事。但是我们的使用场景是不能获取 Root 权限的,所以我们借助了一些硬件的支持,最终达到了截获手机原始音频输出的效果。具体的实现方案也是经历了几个发展阶段,接下里我就按时间顺序介绍一下这部分的发展历程。

方案一:外接声卡

方案介绍

这个方案的基本思路如下图,通过音频线将手机的音频数据传入声卡,然后将声卡和服务器通过 USB 相连,最终从服务器上截获该声卡的音频数据。为了达到这个效果,必须将每款手机和与其相连的声卡建立绑定关系,这就需要每一个声卡都有一个唯一的序列号,这样当我们需要截获某一款手机的音频时,我们只需要从绑定关系表中查到与这款手机相连的声卡序列号,然后通过该序列号找到对应的声卡设备并进行录制。遗憾的是,在现有的产品中,我们没找到具有唯一序列号的 USB 声卡产品,我们只找到了HS-100B[1],它虽然没有唯一序列号,但是我们可以通过外接EEPROM[2]的方式,写入自定义内容作为序列号。所以,我们参考了HS-100B 的产品说明书[3],从中我们得知EEPROM[4]需要存储的内容如下图,其中画红线的部分可以用来定义声卡的序列号,我们之所以用 Product String 作为序列号的存储区域,主要是因为这部分内容可以通过 FFMPEG 的设备显示功能展示出来,我们只需要做一个字符串匹配就能定位需要截获的声卡设备。

EEPROM 写入数据方式

  1. 购买EZP2013 烧录器[5]

  2. 安装烧录软件[6]

  3. 选择 EEPROM 类型

  4. 写入数据[7]

显示序列号的方式

  • Mac: ffmpeg -f avfoundation -list_devices true -i ""

  • Linux: aplay -l (yum install alsa-utils alsa-lib)

方案总结

这个方案总的来说,实现起来是比较麻烦的,虽然他可以获取到多声道的音频数据,但是其工作量太大,既要烧入数据,又要焊接电路,而且还要维护手机与音频采集卡的映射关系。

方案二:音频输出转接音频输入

方案介绍

这个实现方案是受一款现有耳机产品[8]的启发,我们做了一个超级简易版。基本思路如下图,通过一个音频公头接线端子[9],将手机的音频输出接入到麦克风输入中,然后通过手机中的 APP 录制麦克风的输入从而达到内录的效果。我们参考了 Google 的3.5 毫米耳机规范[10],将音频公头接线端子[11]的左右声道连接一个电阻并连入地线,然后选取左声道连接一个电阻接入 MIC,从而达到截获左声道输出的效果。然后就是通过 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 接收端)虚拟为一个音频源,进行声音采集,这个方案目前还有一些问题,我后面会介绍。

      PipeLine

      Remote Device-SRC ---> SINK-Bluetooth-SRC ---> SINK-PulseAudioBlueToothModule-SRC ---> SINK-MyApp
      其中SRC
      代表数据源,SINK
      代表数据接收方。

      确立 AudioSink 角色

      这个方案的重点是如何让服务器上的蓝牙模块扮演 Audio Sink 角色,这就涉及到 Linux 上的BlueZ[12]模块。这里我们需要编辑/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[13]直接通过 BlueZ 构建一个 ALSA 设备。此外,我觉得还应该通过类似于tinyb[14]lbt4j[15]的库,来达到在 Java 中调度蓝牙模块的效果。

                        参考内容

                        [1]https://sspai.com/post/40962 [2]https://juejin.im/post/5be1802f6fb9a049e41229fa [3]https://jprvita.wordpress.com/2009/12/15/1-2-3-4-a2dp-stream/ [4]https://linuxtoy.org/archives/ubuntu-bluetooth-guide.html [5]https://wiki.gentoo.org/wiki/Bluetooth#BlueZ_5 [6]http://www.variwiki.com/index.php?title=BlueZ5_and_A2DP [7]https://wiki.gentoo.org/wiki/PulseAudio [8]http://www.lightofdawn.org/blog/?viewDetailed=00031 [9]http://www.lightofdawn.org/blog/?viewCat=Bluetooth [10]https://ops9.blogspot.com/2013/09/a2dp.html

                        引用链接

                        [1]

                        HS-100B: https://www.cmedia.com.tw/products/USB20_FULL_SPEED/HS100B

                        [2]

                        EEPROM: https://baike.baidu.com/item/EEPROM

                        [3]

                        HS-100B的产品说明书: https://www.beikejiedeliulangmao.top/data/audio/HS100-datasheet-v1-2.pdf

                        [4]

                        EEPROM: https://baike.baidu.com/item/EEPROM

                        [5]

                        EZP2013烧录器: https://search.jd.com/Search?keyword=EZP2013

                        [6]

                        烧录软件: https://www.beikejiedeliulangmao.top/data/audio/EZP2013V4.0.zip

                        [7]

                        数据: https://www.beikejiedeliulangmao.top/data/audio/data.bin

                        [8]

                        耳机产品: https://www.youtube.com/watch?v=tRbu8ow_dvk

                        [9]

                        音频公头接线端子: https://www.google.com/search?q=%E9%9F%B3%E9%A2%91%E5%85%AC%E5%A4%B4%E6%8E%A5%E7%BA%BF%E7%AB%AF%E5%AD%90

                        [10]

                        3.5毫米耳机规范: https://source.android.com/devices/accessories/headset/plug-headset-spec.html

                        [11]

                        音频公头接线端子: https://www.google.com/search?q=%E9%9F%B3%E9%A2%91%E5%85%AC%E5%A4%B4%E6%8E%A5%E7%BA%BF%E7%AB%AF%E5%AD%90

                        [12]

                        BlueZ: http://www.bluez.org/about/

                        [13]

                        bluez-alsa: https://github.com/Arkq/bluez-alsa

                        [14]

                        tinyb: https://github.com/intel-iot-devkit/tinyb

                        [15]

                        lbt4j: https://github.com/olir/lbt4j


                        文章转载自贝贝猫技术分享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                        评论