用 LSPosed 模拟录屏音频编码器卡顿:从思路到落地

用 LSPosed 模拟录屏音频编码器卡顿:从思路到落地

标签:Android / LSPosed / MediaCodec / 音画同步 / 缺陷复现

适用场景:录屏音画不同步、音频断续等缺陷,需要可控地「造」出编码器慢一拍的现象


预备

需要安装kernelSU 和LSPosed 安装kernelSU 和LSPosed

一、为什么要做这个

录屏链路里,视频和音频往往分开编码,最后再封装进同一个文件。

如果音频编码器偶尔变慢,就会出现:

  • 听感上断续、拖沓
  • 口型与画面不同步
  • 严重时录屏进程无响应

真实机器上这类问题偶发、难复现。最理想的做法是:在不改录屏业务代码的前提下,用 Hook 在音频编码收发缓冲时按概率注入随机延迟,把「编码器卡顿」变成可配置的实验条件。

本文介绍一套基于 LSPosed 模块 + 配置 App 的实现:MockEncoder(音频编码卡顿模拟)。

界面示意:

可配置项包括:

含义 示例
开启 / 关闭 是否注入延迟 已开启
触发概率 每次收发缓冲命中概率 40%
最小 / 最大延迟 命中后 sleep 的随机区间 69--317 ms

二、整体架构

bash 复制代码
┌─────────────────┐      Settings.Global       ┌──────────────────────┐
│  配置 App        │  ───────────────────────►  │  录屏进程             │
│  调参 / 开关     │                             │  LSPosed 注入模块     │
└─────────────────┘                             │  Hook MediaCodec     │
                                                │  概率 sleep          │
                                                └──────────────────────┘

分工很清晰:

  1. 配置 App:负责 UI 与参数持久化
  2. LSPosed 模块 :在录屏进程内 Hook MediaCodec
  3. 跨进程传参 :优先写 Settings.Global(任意进程可读),文件 / ContentProvider / XSharedPreferences 作兜底

作用域只勾选录屏应用(如 com.oplus.screenrecorder),避免误伤其他 App。


三、环境前提

  • 已 Root,Magisk + Zygisk
  • 已安装 LSPosed(Zygisk 版)
  • 模块在 LSPosed 中启用,作用域勾选录屏应用
  • 写入系统设置需一次性授权(见下文)
bash 复制代码
adb shell pm grant com.mockencoder.audio android.permission.WRITE_SECURE_SETTINGS

为什么要这个权限?

不是 Hook 需要它,而是「配置 App 把参数写给录屏进程」需要它。在较新的 ColorOS 上:

  • App 私有目录受 SELinux 限制,录屏进程读不到
  • ContentProvider 受应用可见性限制,可能报 Unknown authority
  • 传统 XSharedPreferences 路径也可能被挪走

Settings.Global写入 需权限,读取不需要。录屏侧 Hook 直接读同名键即可。


四、核心实现一:模块入口

目标进程加载时,框架回调入口,再安装 Hook。模块自身进程不注入。

kotlin 复制代码
/**
 * LSPosed 模块入口。
 * 目标 App(如录屏)进程启动时,框架会回调这里,随后在该进程内安装 Hook。
 */
class HookEntry : IXposedHookLoadPackage {
    override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
        // 不 Hook 配置 App 自身,避免无意义注入
        if (lpparam.packageName == MODULE_PACKAGE) {
            return
        }
        runCatching { AudioCodecHook.install(lpparam) }
            .onFailure { /* 打日志 */ }
    }
}

assets/xposed_init 指向该类全名;Manifest 里声明 xposedmodule 等 meta-data。


五、核心实现二:只卡音频,不卡视频

录屏会同时创建视频与音频编码器。必须在 configure 之后用 MIME 区分:

  • audio/mp4a-latm → 加入跟踪集合,后续注入延迟
  • video/avc → 不注入
kotlin 复制代码
/**
 * configure 之后根据 MediaFormat.KEY_MIME 判断是否为音频编码器。
 * audio/mp4a-latm 等加入跟踪集合;video/avc 等从集合移除。
 */
override fun afterHookedMethod(param: MethodHookParam) {
    val format = param.args.firstOrNull { it is MediaFormat } as? MediaFormat ?: return
    val mime = format.getString(MediaFormat.KEY_MIME)
    val isAudio = mime != null && mime.startsWith("audio/")
    if (isAudio) {
        audioCodecs.add(param.thisObject)   // 按对象身份跟踪
    } else {
        audioCodecs.remove(param.thisObject)
    }
}

IdentityHashMap 跟踪 MediaCodec 实例,避免错误地把视频编码也卡住。

queueInputBuffer / dequeueOutputBuffer 前,仅对集合内实例调用卡顿逻辑:

kotlin 复制代码
/** 送入编码输入缓冲前注入延迟,模拟「编码器处理慢」。 */
override fun beforeHookedMethod(param: MethodHookParam) {
    if (audioCodecs.contains(param.thisObject)) {
        LagSimulator.maybeStall("queueInput")
    }
}

成功日志示例:

ini 复制代码
hooks installed configure=3 queue=1 dequeue=1 release=1
codec configured mime=audio/mp4a-latm audio=true
codec configured mime=video/avc audio=false

六、核心实现三:按概率随机延迟

kotlin 复制代码
/**
 * 核心卡顿逻辑:
 * 1. 未开启 / 概率为 0 / 最大延迟为 0 → 直接返回
 * 2. 生成 [0,100) 随机数,小于设定概率则命中
 * 3. 在 [minDelay, maxDelay] 内随机 sleep
 */
fun maybeStall(tag: String) {
    val config = currentConfig()
    if (!config.enabled || config.probabilityPercent <= 0 || config.maxDelayMs <= 0) {
        return
    }
    val hit = ThreadLocalRandom.current().nextInt(100) < config.probabilityPercent
    if (!hit) return

    val min = config.minDelayMs
    val max = max(config.maxDelayMs, min)
    val delay = if (max == min) min else ThreadLocalRandom.current().nextInt(min, max + 1)
    if (delay > 0) {
        Thread.sleep(delay.toLong())
        // 日志:stall injected at queueInput delay=123ms
    }
}

注意:Thread.sleep 堵的是编码线程 。概率过高、延迟过大时,录屏可能像卡死------建议先用 20% + 100--300ms 验证,再加压。

配置约每 1.5 秒刷新一次,避免每次编码都读设置。


七、核心实现四:配置如何传到录屏进程

保存时三路冗余:

kotlin 复制代码
/** 本地保存 + 导出文件 + 写入 Settings.Global,三路冗余提高跨进程可读性。 */
fun save(context: Context, config: MockConfig): Boolean {
    // 1. SharedPreferences:App 自己读
    prefs(context).edit()....commit()
    // 2. 导出文件:部分机型可能被 SELinux 挡住
    exportToFile(context, c, now)
    // 3. Settings.Global:跨进程最稳(需 WRITE_SECURE_SETTINGS)
    writeSettings(context, c, now)
}

录屏侧读取优先级:Settings.Global → 文件 → ContentProvider → XSharedPreferences。

界面底部状态区应显示类似:

matlab 复制代码
磁盘开关 已开启
概率 40%
延迟 69-317ms
写入时间 19:43:45
共享文件已就绪
系统设置(global) 已就绪

看到「系统设置(global) 已就绪」,说明录屏进程有机会读到真实参数。


八、踩坑记录(强烈建议看)

1. 编译期 Xposed API 占位声明返回类型写错

内网若拉不到官方 api:82,常会自写 stub。若把:

  • hookAllMethods 写成 void
  • 而运行时实际是 Set<Unhook>

Android 按「参数 + 返回类型」匹配方法,会直接 NoSuchMethodError,表现为挂接全部失败(configure=0 queue=0)。

stub 的方法签名必须和运行时 LSPosed 一致。

2. 以为保存成功,其实开关一直是关的

早期 UI 开关开/关外观几乎一样,磁盘上 enabled=false,模块永远不注入。

后来改成「开启模拟 / 关闭模拟」大按钮,并在底部回显磁盘真实状态。

4. Hook 成功 ≠ 生效

可能看到:

  • hooks installed ...
  • codec configured mime=audio/...

但若日志是:

ini 复制代码
config[prefs] enabled=false probability=10%

说明配置没传过去,延迟逻辑被短路。目标应是:

ini 复制代码
config[settings] enabled=true probability=40% delay=69-317ms
stall injected at queueInput delay=xxxms

九、使用步骤(精简)

  1. 安装配置 App(即 LSPosed 模块 APK)
  2. LSPosed 启用模块,勾选录屏包名,强制停止录屏
  3. adb shell pm grant ... WRITE_SECURE_SETTINGS
  4. 打开 App → 点「开启模拟」→ 调好概率/延迟 → 确认底部「系统设置(global) 已就绪」
  5. 重新开始录屏,观察音画;用日志确认 stall injected

查看模块日志(LSPosed 模块日志页,或过滤):

bash 复制代码
adb logcat -s MockEncoder
# 或查看 /data/adb/lspd/log/modules_*.log

十、小结

目标 做法
不改录屏源码造故障 LSPosed Hook MediaCodec
只影响音频 configure 后按 MIME 过滤
可调强度 概率 + 随机延迟区间
跨进程配置 优先 Settings.Global
可观测 关键路径打日志,UI 回显磁盘状态

这套方案适合做音画同步 / 音频断续类问题的可控复现。参数要循序渐进:先确认链路通,再加大概率和延迟,否则很容易把录屏直接「卡死」当成模块没生效。


附录:相关声明

  • 仅建议在自有测试设备、合规 Root 环境下使用
  • 勿用于非授权应用或破坏他人设备稳定性
  • 文中包名、截图以 ColorOS 录屏为例,其他 ROM 请自行改作用域

(完)

相关推荐
启雀AI17 小时前
培训平台移动端离线学习方案设计与实现:视频缓存、断点续传与进度同步的工程实践
android·学习·缓存·音视频·企业lms
我命由我1234518 小时前
RxJava - 冷数据流与热数据流
android·java·java-ee·android studio·rxjava·android-studio·android runtime
2601_9665635219 小时前
愤怒的小鸟单机版 去广告 安卓+PC端离线纯净版 益智耐玩小游戏 不需联网 老旧手机都可以玩 怀旧手机小游戏
android·智能手机
hunterandroid20 小时前
[Android 从零到一] Retrofit 请求取消与生命周期绑定:从 Call.cancel 到协程可取消设计
android
爱跑马的程序员20 小时前
安卓专有的通信子系统-Binder IPC
android·binder·ipc·安卓间通信机制
ue星空20 小时前
【安卓逆向】为什么用Frida?
android
小驰行动派21 小时前
Camx架构全景图:从V4L2到Pipeline的完整拆解
android·camera·android camera
hunterandroid1 天前
[Android 从零到一] Custom View 触摸反馈与手势冲突解决
android
coderSong25681 天前
Android | 四大组件之 BroadcastReceiver(广播接收器)
android
爱笑鱼1 天前
Android 系统启动机制(二):init.rc 不是普通脚本,service、action 和 property 怎样驱动启动?
android