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

点击上方蓝字关注我们!

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

01

对比方案

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

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

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

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

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

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

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

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

02

方案实现

no.1

获取模拟器对应关系

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

在逍遥模拟器的官⽅命令中, 有这么⼀条命令

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']}

no.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 赋予完全控制权限, 不然, 模拟器的窗⼝置顶操作会失败

no.3

音画分离

ffmpeg -i 视频路径 -ar 16000 -vn 音频输出路径

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

no.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"