移动测试开发 用模拟器实现视频流的音画分离
在 360 开测平台上, 对内的业务中, 需要对音视频进行检测, 音频的抽取成为一个难题
方案对比
在 Android 手机中,实现音频内录有以下几种方式:
- 硬件支持 (麦克风音频输出在转换为输入): 方案可行, 但是需要一定的成本, 而且不适合第三方 APK, 如果支持硬件不太好的话, 录制出来有电流噪声, 和环境噪声, 不易去除.
- root Android 手机, 伪装为系统应用: (未尝试) 360 开测平台上的真机不可能把手机 root, 风险太大.
- Android 9 以上系统手机: 只能无忧无虑的录制系统声音, 需要第三方的 APK 支持, 而且录制效果很差.
- 模拟器内录: 伪装系统 APK 在模拟器中录制, 应用崩溃, 模拟器不支持.
- PC 录制模拟器外放音量: 音频混淆, 不易拆分.
- 模拟器自己录制: 至今为止, 发现逍遥模拟器可以多个模拟器同时录制, 互不干扰
今天我们就说一下, 怎么使用逍遥模拟器来抽取手机中的音频文件 (包括第三方的 APK 和系统的 APK)
方案实现
1. 获取模拟器对应关系
在逍遥模拟器安装路径中, 可以看到 MemuHyperv VMs 文件夹, 打开可以看到当前我们所创建的所有模拟器. 在 MEmu_1.memu 或者 MEmu_1.memu-prev 文件中, 存储了模拟器的配置信息, 信息存储是按照 XML 的格式来存储的, 我们直接解析当前的 XML 文件.
def parse_file(filepath):
"""
解析 MEmu.memu xml 文件信息, 获取信息
:param filepath:
:return:
"""
infodict = {}
for root, dirs, files in os.walk(filepath):
for f in files:
if f.startswith("MEmu") and f.endswith(".memu"):
path = os.path.join(root, f)
dom = parse(path)
data = dom.documentElement
Machines = data.getElementsByTagName('Machine')
for Machine in Machines:
Machine_name = Machine.getAttribute('name')
Machine_index = getMachineIndex(Machine_name)
break
Forwardings = data.getElementsByTagName('Forwarding')
for host in Forwardings:
if host.getAttribute('name') == "ADB":
hostport = host.getAttribute('hostport')
break
infodict[Machine_index] = [Machine_name, hostport]
return infodict
我们在文件中, 分别获取 标签中的 name 属性, 标签中的 hostport 属性.
name 是指当前模拟器的名字, Forwarding 是指当前模拟器的 tid, tid 的值和 adb devices 命令获取的值是相同的, 我们可以根据这些信息, 来分别对应到各个模拟器上.
在逍遥模拟器的官方命令中, 有这么一条命令
memuc listvms --running # 就是获取我们当前正在运行的模拟器的一些信息
输出参数顺序: 模拟器索引, 标题(模拟器的页面标题, 和我们上述获取的不同), 顶层窗口的句柄, 是否进入Androi, 进程pid 信息, 模拟器磁盘占用的信息
最终, 我们合并后的集合:
{u'1': ['1', 'xxx - 1', '2950304', '1', '5128', u'MEmu_1', u'21513'],
u'3': ['3', 'xxx - 3', '19073560', '1', '16948', u'MEmu_3', u'21533'],
u'2': ['2', 'xxx - 2', '5573082', '1', '10924', u'MEmu_2', u'21523'],
u'5': ['5', 'xxx - 5', '10750466', '1', '10112', u'MEmu_5', u'21553'],
u'4': ['4', 'xxx - 4', '8063248', '1', '2908', u'MEmu_4', u'21543']}
2. 开始录屏操作
开始录制屏幕
win32gui.ShowWindow(hwnd, 1) # hwnd 句柄
win32gui.SetForegroundWindow(hwnd)
win32api.keybd_event(17, 0, 0, 0) # ctrl 键码是17
win32api.keybd_event(116, 0, 0, 0) # f5
win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(116, 0, win32con.KEYEVENTF_KEYUP, 0)
结束屏幕录制
win32gui.ShowWindow(hwnd, 1)
win32gui.SetForegroundWindow(hwnd)
win32api.keybd_event(17, 0, 0, 0) # ctrl 键码是17
win32api.keybd_event(117, 0, 0, 0) # f6
win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(117, 0, win32con.KEYEVENTF_KEYUP, 0)
就是简单的进行 ctrl + F5 和 ctrl + F6 的操作, 在操作的时候, 需要设置当前模拟器的焦点, 也就是我们需要把当前模拟器置顶操作, 操作的句柄就是在第一步的信息中.
坑: 在 windows 系统下, python 需要以管理员的权限运行, 或者给 python 赋予完全控制权限, 不然, 模拟器的窗口置顶操作会失败
3. 音画分离
我们在录屏结束后, 采用 ffmpeg 来进行音画分离
ffmpeg -i 视频路径 -ar 16000 -vn 音频输出路径
这样, 我们就获取到当前模拟器的音频文件了, 最后文件输出为 wav 文件, 通过这个文件, 就可以对音频文件进行音频质量检测.
4. 视频文件对应模拟器
这个是本文最大的坑. 且听我详细说一下.
比方说, 我们开启了 5 个模拟器, 上面一些图, 都是开 5 个模拟器获取到的信息, 5 个模拟器在同时工作的时候, 深坑就来了.
坑 1: 录制后的视频命名规范为 %Y%m%d%H%M%S, 最小区分度为秒, 这就可能会造成视频名字会重复, 文件覆盖, 造成最后的分离的音频缺失.
解决方案: 在操作模拟器的时候, 需要给 1 秒以上的间隔, 保证当前的视频文件不会重复.
坑 2: 没有视频文件和模拟器的对应关系
解决方案: 在每个模拟器开始录屏前, 获取当前时间, 并记录下来, 基本就能和模拟器对应起来
坑 3: 模拟器录制的视频文件的名字和我们自己定义的视频文件的名字有出入, 会有文件找不到的错误.
解决方案: 我们定义的时间和模拟器开始录制的时间稍微有些区别, 大部分都是 1 秒钟的差别, 我们采用如下方式来寻找文件, 可能还会有点缺陷, 需要在研究下.
if FileUtils.isExists(videopath):
filepath_no_ext = os.path.splitext(videopath)[0]
return filepath_no_ext + ".wav"
videopath = root_path + fileNameAddOne(taskdata[key][1]) + ".mp4"
if FileUtils.isExists(videopath):
updatetime(fileNameAddOne(taskdata[key][1]))
filepath_no_ext = os.path.splitext(videopath)[0]
return filepath_no_ext + ".wav"
videopath = root_path + fileNamesubOne(taskdata[key][1]) + ".mp4"
if FileUtils.isExists(videopath):
updatetime(fileNamesubOne(taskdata[key][1]))
filepath_no_ext = os.path.splitext(videopath)[0]
return filepath_no_ext + ".wav"