深入浅出 Android CTS 视频测试:从环境搭建、用例设计到底层通信原理

深入浅出 Android CTS 视频测试:从环境搭建、用例设计到底层通信原理

作者 : Antigravity Pair Programmer

适用场景: Android 音视频开发、MediaCodec/MediaPlayer 兼容性测试、CTS 驱动适配与测试自动化


摘要

Android Compatibility Test Suite (CTS) 是 Google 验证设备兼容性的核心套件。在音视频领域,CTS 视频测试覆盖了底层硬件解码器 MediaCodec、解复用器 MediaExtractor 以及上层播放器 MediaPlayer 的正确性与性能极限。

本文将从 环境准备4 大核心视频 CTS 测试 Case 设计五大金刚 API 汇总自动化脚本打造 以及 底层信号传递与结果回传原理 五个维度,全方位拆解如何设计并实际跑通 Android CTS 视频测试。


一、 CTS 视频测试环境准备

1. 基础依赖环境

要运行 Android CTS 视频测试,需要准备以下软硬件环境:

  • 硬件/设备:开启 USB 调试的真实 Android 手机/车载终端/电视盒子(建议 Android 10+ / API 29+),或开启 HAXM/KVM 硬件渲染加速的 Android Emulator 模拟器。
  • 开发工具链
    • JDK 17 (Java 运行环境)
    • Android SDK & ADB (platform-tools/adb.exe)
    • Gradle 8.5+ (项目构建系统)

2. 双层测试架构设计

官方 cts-tradefed 套件重达 5GB+ 且初始化极其笨重。在实际开发调试中,我们可以采用 Direct Instrumentation 双层架构

测试模式 触发条件 运行载体 核心价值
真机/模拟器模式 (androidTest) 连接 ADB 设备 真实 Android 操作系统 (mediaserver / 硬解芯片) 硬核验证芯片厂商 MediaCodec 兼容性
宿主机 Mock 模式 (test) 无 ADB 设备 电脑本地 OpenJDK 17 10秒内极速验证 CTS 测试框架控制流与断言算法

二、 4 大核心 CTS 视频测试 Case 设计与关键代码

我们针对 Android 音视频的核心组件(能力矩阵、解码 PTS、Surface 渲染、播放器状态机),设计了 4 个具代表性的 CTS 测试 Case。

Case 1: 解码器能力矩阵与分辨率极限测试 (VideoDecoderCapabilitiesTest)

  • 测试目标 :验证系统 MediaCodecListvideo/avc (H.264)、video/hevc (H.265)、video/vp9 解码器的注册情况以及 1080p/720p 尺寸支持。
  • 关键代码
java 复制代码
@RunWith(AndroidJUnit4.class)
public class VideoDecoderCapabilitiesTest {
    @Test
    public void testAvcDecoderCapabilities() {
        verifyCodecCapabilities(MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080);
    }

    private void verifyCodecCapabilities(String mimeType, int reqWidth, int reqHeight) {
        MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
        MediaCodecInfo[] codecInfos = codecList.getCodecInfos();

        List<MediaCodecInfo> decoders = new ArrayList<>();
        for (MediaCodecInfo info : codecInfos) {
            if (info.isEncoder()) continue;
            for (String type : info.getSupportedTypes()) {
                if (type.equalsIgnoreCase(mimeType)) {
                    decoders.add(info);
                    break;
                }
            }
        }

        // CTS 规则 1:系统必须提供至少一个对应的解码器
        Assert.assertFalse("CTS Failure: Missing decoder for " + mimeType, decoders.isEmpty());

        for (MediaCodecInfo codecInfo : decoders) {
            MediaCodecInfo.CodecCapabilities caps = codecInfo.getCapabilitiesForType(mimeType);
            MediaCodecInfo.VideoCapabilities videoCaps = caps.getVideoCapabilities();

            // CTS 规则 2:验证解码器必须支持 1080p/720p 标清/高清分辨率
            boolean isSizeSupported = videoCaps.isSizeSupported(reqWidth, reqHeight);
            Assert.assertTrue("CTS Violation: Codec " + codecInfo.getName() + " unsupported " + reqWidth + "x" + reqHeight, isSizeSupported);
        }
    }
}

Case 2: MediaCodec 解码时间戳 (PTS) 与 EOS 信号测试 (VideoFrameDecodingTest)

  • 测试目标 :驱动底层 MediaCodecqueueInputBufferdequeueOutputBuffer 队列,校验输出帧的 Presentation Timestamp (PTS) 是否严格单调递增,且在流结束时收到 BUFFER_FLAG_END_OF_STREAM
  • 关键代码
java 复制代码
@Test
public void testDecoderPtsMonotonicityAndEos() throws Exception {
    String mimeType = MediaFormat.MIMETYPE_VIDEO_AVC;
    MediaFormat format = MediaFormat.createVideoFormat(mimeType, 640, 360);
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);

    MediaCodec decoder = MediaCodec.createDecoderByType(mimeType);
    decoder.configure(format, null, null, 0);
    decoder.start();

    long lastOutputPtsUs = -1;
    boolean outputEosReceived = false;

    while (!outputEosReceived) {
        // 1. 向 InputBuffer 入队包含 NALU 头的视频帧
        int inputBufIndex = decoder.dequeueInputBuffer(10000);
        if (inputBufIndex >= 0) {
            ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufIndex);
            // ... 填充 dummy 帧字节数据与 pts ...
            decoder.queueInputBuffer(inputBufIndex, 0, sampleSize, ptsUs, flags);
        }

        // 2. 从 OutputBuffer 出队解码后的像素帧
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);

        if (outputBufIndex >= 0) {
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                outputEosReceived = true; // 收到 EOS 标记
            }
            if (bufferInfo.size > 0) {
                // CTS 断言:PTS 必须严格单调递增
                Assert.assertTrue("PTS must be monotonically increasing!", bufferInfo.presentationTimeUs >= lastOutputPtsUs);
                lastOutputPtsUs = bufferInfo.presentationTimeUs;
            }
            decoder.releaseOutputBuffer(outputBufIndex, false);
        }
    }
    decoder.release();
}

Case 3: Surface 硬件加速渲染测试 (VideoSurfaceRenderingTest)

  • 测试目标 :将 MediaCodecSurface / SurfaceTexture 绑定,验证解码直投 Surface 时的硬件加速唤醒逻辑 (OnFrameAvailableListener)。
  • 关键代码
java 复制代码
@Test
public void testDecoderSurfaceRendering() throws Exception {
    SurfaceTexture surfaceTexture = new SurfaceTexture(10);
    Surface surface = new Surface(surfaceTexture);

    final CountDownLatch frameLatch = new CountDownLatch(1);
    surfaceTexture.setOnFrameAvailableListener(st -> frameLatch.countDown());

    MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1280, 720);
    MediaCodec decoder = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);

    // 绑定 Surface 配置
    decoder.configure(format, surface, null, 0);
    decoder.start();

    // 出队渲染至 Surface
    int outIdx = decoder.dequeueOutputBuffer(info, 5000);
    if (outIdx >= 0) {
        decoder.releaseOutputBuffer(outIdx, true); // render = true
    }

    boolean rendered = frameLatch.await(1, TimeUnit.SECONDS);
    Assert.assertTrue("Surface rendering callback must trigger", rendered);
}

Case 4: MediaPlayer 7 阶段状态机与精确 Seek 测试 (MediaPlayerCtsTest)

  • 测试目标 :校验 MediaPlayerIdle -> Initialized -> Prepared -> Started -> Paused -> Reset 状态迁移的正确性与高版本精确 Seek (SEEK_CLOSEST_SYNC)。
  • 关键代码
java 复制代码
@Test
public void testMediaPlayerStateMachineAndSeek() throws Exception {
    MediaPlayer player = new MediaPlayer();
    Assert.assertFalse(player.isPlaying()); // 初始状态为 Idle

    player.reset();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        // CTS 规则:验证 SEEK_CLOSEST_SYNC 精确寻帧 API
        player.seekTo(1000, MediaPlayer.SEEK_CLOSEST_SYNC);
    }
    player.release();
}

三、 Android 音视频 / CTS 五大金刚 API 汇总与示例代码

在 Android 音视频及 CTS 测试开发中,绝大多数测试套件都是围绕以下 5 个核心 API 展开的:

组件 / API 接口 CTS 测试里的核心作用
MediaCodecList 查能力 :查询设备到底注册了哪些硬解/软解芯片(如 video/avc, video/hevc),最大支持多少路并发实例。
MediaCodec 控解码 :驱动底层输入/输出 Buffer 队列(queueInputBuffer / dequeueOutputBuffer),校验解码 PTS 时间戳与 EOS。
MediaExtractor 拆解流:把 MP4 / WebM 文件解复用,提取出 H.264 / AAC 原始 NALU 字节流喂给 Codec。
Surface / SurfaceTexture 控渲染:绑定 GPU 显存,测试解码帧直投 Surface 时的色彩空间格式与丢帧率。
MediaPlayer 控状态机 :校验上层播放器在 Initialized -> Prepared -> Started -> SeekTo 状态迁移时的准确性。

1. MediaCodecList (查能力)

java 复制代码
// 查能力:查询当前设备 H.264 解码器及最大并发实例数
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo info : codecList.getCodecInfos()) {
    if (!info.isEncoder() && Arrays.asList(info.getSupportedTypes()).contains("video/avc")) {
        MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType("video/avc");
        int maxInstances = caps.getMaxSupportedInstances(); // 获取最大并发硬解实例数
        boolean supports1080p = caps.getVideoCapabilities().isSizeSupported(1920, 1080);
        Log.i("CTS", "Codec: " + info.getName() + ", 1080p: " + supports1080p + ", MaxInstances: " + maxInstances);
    }
}

2. MediaCodec (控解码)

java 复制代码
// 控解码:驱动输入/输出 Buffer 队列
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();

// 1. 入队
int inputBufIndex = codec.dequeueInputBuffer(timeoutUs);
if (inputBufIndex >= 0) {
    ByteBuffer inputBuf = codec.getInputBuffer(inputBufIndex);
    inputBuf.put(naluData);
    codec.queueInputBuffer(inputBufIndex, 0, naluData.length, ptsUs, 0);
}

// 2. 出队
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outputBufIndex = codec.dequeueOutputBuffer(info, timeoutUs);
if (outputBufIndex >= 0) {
    codec.releaseOutputBuffer(outputBufIndex, true /* 渲染到 Surface */);
}

3. MediaExtractor (拆解流)

java 复制代码
// 拆解流:解复用 MP4 提取音视频轨原始 NALU 数据
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(videoFilePath);

int videoTrackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (mime.startsWith("video/")) {
        videoTrackIndex = i;
        extractor.selectTrack(i); // 选择视频轨
        break;
    }
}
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
int sampleSize = extractor.readSampleData(buffer, 0); // 读取一帧 NALU
long pts = extractor.getSampleTime();                 // 获取当前帧 PTS
extractor.advance();                                   // 推进到下一帧

4. Surface / SurfaceTexture (控渲染)

java 复制代码
// 控渲染:结合 SurfaceTexture 进行 OpenGL 纹理/显存离屏渲染
SurfaceTexture surfaceTexture = new SurfaceTexture(texId);
surfaceTexture.setDefaultBufferSize(1920, 1080);
Surface surface = new Surface(surfaceTexture);

// 监听新帧到达事件
surfaceTexture.setOnFrameAvailableListener(st -> {
    st.updateTexImage(); // 唤醒更新 OpenGL 纹理
});

// 将 Surface 传给 MediaCodec 或 MediaPlayer 绑定
codec.configure(format, surface, null, 0);

5. MediaPlayer (控状态机)

java 复制代码
// 控状态机:校验 MediaPlayer 生命状态迁移与精准 Seek
MediaPlayer player = new MediaPlayer();
player.setDataSource(context, videoUri);
player.setOnPreparedListener(mp -> {
    mp.start(); // 进入 Started 状态
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        mp.seekTo(2000, MediaPlayer.SEEK_CLOSEST_SYNC); // 精确 SEEK
    }
});
player.prepareAsync(); // 异步 Prepare

四、 自动化驱动与环境兼容性踩坑

在 Windows 环境运行 Android 构建与 CTS 测试时,易遇到两类环境坑,可在 gradle.properties 与 Python 脚本中完美解决:

1. 中文路径警告阻断 (overridePathCheck)

Windows 系统中如果工程路径包含中文(如 agent-shizhan--工程落地),Android Gradle Plugin 会抛出 non-ASCII characters 异常。

  • 解决方案 :在 gradle.properties 中添加:

    properties 复制代码
    android.overridePathCheck=true
    org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

2. 终端编码问题 (UnicodeEncodeError)

Python 脚本在 Windows GBK 控制台下打印特殊符号(如 Unicode 勾号 \u2714)会崩溃。

  • 解决方案 :在终端打印日志中使用标号 [OK] 替代不兼容字符,确保跨平台稳定运行。

五、 底层原理剖析:测试信号如何跨设备跨进程传递?

许多开发者直觉认为"运行脚本就是调用本地动态库",但实际上从电脑脚本到手机底层芯片,经历了复杂的 跨设备 Socket 通信与跨进程反射调度

系统架构交互时序图如下:

text 复制代码
[宿主机 Python 脚本] 
       │ 1. adb shell am instrument -w -e class ...
       ▼
[ADB Daemon (adbd)] 
       │ 2. TCP Socket 传输 Bundle 消息
       ▼
[ActivityManagerService (AMS)] 
       │ 3. 启动 Instrumentation 进程,注入 Bundle
       ▼
[AndroidJUnitRunner (测试 APK)] 
       │ 4. ClassLoader 反射加载 @Test 方法
       ▼
[MediaCodec / mediaserver 原生 C++ 进程] 
       │ 5. 加载高通/联发科硬解 .so 芯片驱动
       ▼
[InstrumentationResultPrinter] 
       │ 6. Binder 写入 stdout
       ▼
[Python 脚本解析显示 PASS]

1. 信号下发:am instrument 与 ADB Socket

电脑脚本通过 ADB 发送核心指令:

bash 复制代码
adb shell am instrument -w -r \
  -e class com.cts.demo.VideoDecoderCapabilitiesTest#testAvcDecoderCapabilities \
  com.cts.demo.test/androidx.test.runner.AndroidJUnitRunner

-e class ... 参数以 Key-Value 的形式被打包。adb.exe 通过 5037 端口由 TCP Socket 将参数发给手机端的 adbd 守护进程,并唤醒手机系统的 ActivityManagerService (AMS)。

2. 信号接收:Manifest 契约与反射加载

在编译 app-debug-androidTest.apk 时,AndroidManifest.xml 注入了契约:

xml 复制代码
<instrumentation
    android:name="androidx.test.runner.AndroidJUnitRunner"
    android:targetPackage="com.cts.demo" />

AMS 启动 AndroidJUnitRunneronCreate(Bundle arguments) 方法,将 -e 后的参数封装为 Bundle。Runner 利用 Java 反射 (ClassLoader) 动态加载 VideoDecoderCapabilitiesTest 类并执行被 @Test 标记的方法。

3. 硬件解构:调用 Android mediaserver

当测试代码执行到 MediaCodec.createDecoderByType("video/avc") 时,JVM 通过 JNI 跨进程调用 Android 系统的原生进程 mediaserver (libstagefright.so),进而拉起手机厂商(高通/联发科/海思)的底层硬件解码驱动。

4. 结果回传:实时 stdout 流与终端呈现

测试完成后,AndroidJUnitRunner 内部的 InstrumentationResultPrinter 捕获结果,通过 Android 底层 Binder 机制将状态码(0 = SUCCESS, -1 = FAILURE)实时写入到 am 命令的 标准输出流 (stdout) 。Python 脚本捕获 stdout 文本并解析,最终呈现出直观的 PASS/FAIL 表格。


六、 总结

本文从实践与原理双维度拆解了 Android CTS 视频测试:

  1. 通过 Direct Instrumentation 架构,避免了官方 5GB+ Tradefed 套件的笨重依赖。
  2. 实现了覆盖 能力矩阵、解码 PTS、Surface 渲染与播放器状态机 的 4 大核心视频测试 Case。
  3. 汇总了音视频测试的 "五大金刚 API" 核心用法与示例代码。
  4. 深入剖析了 am instrument 信号注入 -> AndroidJUnitRunner 反射调度 -> MediaCodec 芯片级解码 -> stdout 结果回传 的全流程。

一套设计严谨的 CTS 测试套件,能为音视频 App 的流畅播放与硬件解码兼容性保驾护航。

相关推荐
我命由我123451 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
雨白3 小时前
深入理解 Kotlin 协程 (八):拾遗补阙,探秘官方框架的调度细节与取消闭环
android·kotlin
海天鹰4 小时前
PHP上传文件
android·开发语言·php
hz567896 小时前
私有云会议系统选型攻略:企业级安全与高效协同并行
安全·音视频·实时音视频·信息与通信
2501_915918416 小时前
详解iOS App上架至App Store的全流程步骤与注意事项
android·macos·ios·小程序·uni-app·cocoa·iphone
Yeauty6 小时前
渲染成图再 CLI 拼接,还是进程内直推?Rust 帧到视频的两条路
开发语言·rust·音视频
码农coding7 小时前
android12 SystemUI之StatusBar(二)
android
达子6668 小时前
第13章_HarmonyOs开发图解 视频
华为·音视频·harmonyos
Lesile9 小时前
Interview#1 历史演进:MVC · MVP · MVVM · MVI架构详解
android·android jetpack