用模拟器实现视频流的音画分离

  • 2021-03-12
  • 本文字数:2666 字

    阅读完需:约 9 分钟

在 360 开测平台上, 对内的业务中, 需要对⾳视频进⾏检测,  ⾳频的抽取成为⼀个难题。

对比方案

在 Android ⼿机中,实现⾳频内录有以下⼏种⽅式:

1. 硬件⽀持 (⻨克⻛⾳频输出在转换为输⼊):  ⽅案可⾏, 但是需要⼀定的成本, ⽽且不适合第三⽅ APK。

2. root Android ⼿机, 伪装为系统应⽤:   (未尝试) 360 开测平台上的真机不可能把⼿机 root, ⻛险太⼤。

3. Android 9 以上系统⼿机: 只能⽆忧⽆虑的录制系统声⾳, 需要第三⽅的 APK⽀持, ⽽且录制效果很差。

4. 模拟器内录: 伪装系统 APK 在模拟器中录制, 应⽤崩溃, 模拟器不⽀持。

5. PC 录制模拟器外放⾳量: ⾳频混淆, 不易拆分。

6. 模拟器⾃⼰录制:  ⾄今为⽌, 发现逍遥模拟器可以多个模拟器同时录制, 互不⼲扰。

今天我们就说⼀下, 怎么使⽤逍遥模拟器来抽取⼿机中的⾳频⽂件(包括第三⽅的 APK 和系统的 APK)。

方案实现

获取模拟器对应关系

在逍遥模拟器安装路径中,  可以看到 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']}
复制代码

开始录屏操作

win32gui.ShowWindow(hwnd, 1) # hwnd 句柄win32gui.SetForegroundWindow(hwnd)win32api.keybd_event(17, 0, 0, 0)  # ctrl 键码是17win32api.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 键码是17win32api.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 赋予完全控制权限, 不然, 模拟器的窗⼝置顶操作会失败。

音画分离

我们在录屏结束后, 采⽤ffmpeg 来进⾏⾳画分离

ffmpeg -i 视频路径 -ar 16000 -vn 音频输出路径
复制代码

图片

这样, 我们就获取到当前模拟器的⾳频⽂件了, 最后⽂件输出为 wav ⽂件, 通过这个⽂件, 就可以对⾳频⽂件进⾏⾳频质量检测

视频文件对应模拟器

这个是本⽂最⼤的坑,且听我详细说⼀下:

⽐⽅说, 我们开启了 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"
复制代码

本文转载自:360 技术(ID:qihoo_tech)

原文链接:用模拟器实现视频流的音画分离