Week 1:多媒体处理链路总览

Camera与Player

1. 核心链路总览:Camera ↔ Player 对照图

理解链路的最快方式是将采集端播放端进行对比。它们就像一个沙漏的两端,中间通过文件或网络流连接。

环节 Camera 采集链路(生产者) Player 播放链路(消费者)
物理层 Sensor 采集: 光信号转电信号 Screen/Speaker: 电信号转光/声
原始数据 YUV / RGB: 原始像素帧 Surface / AudioTrack: 渲染/输出
核心处理 Encoder (编码): 压缩数据 (H.264/H.265) Decoder (解码): 解压数据
容器层 Muxer (封装): 打包成 MP4/TS Demuxer (解封装): 拆解音视频流
持久化 File / Network: 存储或直播推流 DataSource: 读取本地文件或 URL

2. 深度拆解:数据流的走向

A. Camera 采集流程:从 Sensor 到文件

  1. 采集 (Capture) : Camera Sensor 捕获原始光电信号,通过 ISP(图像信号处理)输出 YUVRGB 格式。

  2. 缓冲区传递 (Buffer Queue) : Android 中 Camera 数据通常通过 SurfaceTextureImageReader 传递。为了性能,通常采用 Zero-copy (零拷贝) 机制,直接传递 Buffer 的句柄。

  3. 编码 (Encoding) : 使用 MediaCodec 将庞大的 YUV 数据压缩为 H.264/H.265 编码帧(I/P/B 帧)。

  4. 封装 (Muxing) : 使用 MediaMuxer 将编码后的视频 H.264 流、音频 AAC 流以及元数据(Metadata)打包进容器(如 .mp4)。

B. Player 播放流程:从 DataSource 到屏幕

  1. 解封装 (Demuxing) : MediaExtractor 读取 MP4,根据索引分离出音频轨道和视频轨道。

  2. 解码 (Decoding) : MediaCodec 接收压缩帧,解码回原始的 YUV/RGB 像素数据。

  3. 渲染 (Rendering):

    • 视频 : 将解码后的 Buffer 发送到 Surface,由 SurfaceFlinger 合成并提交给显示驱动。

    • 音频 : 通过 AudioTrack 将 PCM 数据送入音频混音器(AudioFlinger)。

3. 关键环节:时间戳 (PTS/DTS) 与同步

在音视频中,时间就是一切

  • PTS (Presentation Time Stamp): 告诉渲染器这一帧应该在什么时候显示。

  • DTS (Decoding Time Stamp): 告诉解码器这一帧应该什么时候解码(在有 B 帧的情况下,DTS 和 PTS 顺序不同)。

  • 音画同步 (AV-Sync):

    • 通常以音频时钟 (Audio Master) 为基准,因为人类对声音的停顿更敏感。

    • 视频帧会根据当前音频播放的时间点,决定是"快进(跳帧)"、"准时渲染"还是"等待(重复上一帧)"。

4. 性能瓶颈与常见问题

在实际开发(JD 对齐)中,你需要关注以下高频痛点:

  • 内存拷贝过多: 频繁在 Java 层和 Native 层、或者 CPU 和 GPU 之间拷贝大块像素数据,会导致掉帧。

    • 优化 : 使用 HardwareBufferSurface 直接传递。
  • 编解码延迟: 硬编硬解性能好但存在兼容性问题;软编软解(FFmpeg)兼容性好但耗电、发热。

  • Buffer 堆积: 采集端生产太快而编码太慢,导致内存溢出(OOM)。这个需要另外研究

  • 时间戳抖动: 采集时 PTS 赋值不准,会导致播放时视觉卡顿(Jitter)。

整个多媒体链路是一个从模拟到数字再到模拟的过程。采集端通过 Camera Sensor 获取 YUV 原始数据,经过 MediaCodec 硬编码压缩,再由 MediaMuxer 封装成容器格式。播放端则是逆向过程:Extractor 解封装,MediaCodec 解码,最后通过 PTS 时间戳对齐,将画面渲染到 Surface,声音输出到 AudioTrack。核心难点在于零拷贝下的 Buffer 流转效率以及音画同步的精度控制。

GOP

当你看着一个 MP4 文件时,它在你眼里不应该只是一个"视频文件",而应该像一辆载满了不同货物的火车

你现在最困惑的其实是:视频内部是如何组织这些"碎块"数据的,以及为什么要有 GOP 这种结构?

为了帮你建立直观理解,我们把 MP4 文件拆开来看:


1. 轨道 (Track):火车的车厢

一个多媒体文件(容器)里通常包含多个轨道,它们是并行运行的:

  • 视频轨 (Video Track):存 H.264/H.265 编码后的画面。

  • 音频轨 (Audio Track):存 AAC/Opus 编码后的声音。

  • 字幕轨 (Subtitle Track):存时间戳和文本。

核心逻辑:播放器的工作就是像"拉链"一样,把这些独立的轨道按时间线(PTS)对齐,然后同时喂给显卡和声卡。


2. GOP (Group of Pictures):视频压缩的"基本单位"

这是你最需要掌握的细节。视频之所以能压缩得很小,是因为画面之间是有冗余的(比如背景一直没变)。

GOP(画面组) 就是一组连续的、有参考关系的帧。它包含三种核心成员:

  • I 帧 (Intra-coded picture)关键帧。它是完整的图片,不需要参考任何其他帧就能解码。它是 GOP 的开头。

  • P 帧 (Predictive picture)前向预测帧。它只记录与前一帧的"差异"。如果要看这一帧,必须先解出前一帧。

  • B 帧 (Bi-predictive picture)双向预测帧。它既参考前面的帧,也参考后面的帧,压缩率最高,但最消耗性能。

JD 考点 / 常见问题:

  • GOP 越长越好吗? GOP 越长,压缩率越高(文件越小),但在直播中延迟会变大,且进度条拖动(Seek)时会变慢,因为播放器必须找到最近的 I 帧才能开始画面绘制。

  • 直播秒开优化:通常就是通过缓存最近的一个 GOP 给观众来实现的。


3. 帧率、码率与分辨率:视频的"三围"

这三个参数决定了轨道的质量:

参数 类比 对体验的影响
分辨率 (Resolution) 画布大小 1080P 比 720P 像素更多,更清晰。
帧率 (FPS) 连环画翻动的速度 24帧是电影感,60帧极度丝滑(游戏常用)。
码率 (Bitrate) 每秒消耗的数据量 最关键! 即使是 4K 分辨率,如果码率太低,画面也会全是"马赛克"。

4. 封装格式 vs 编码格式:瓶子与酒

这是初学者最容易搞混的地方:

  • 编码格式 (H.264 / H.265 / AV1):这是"酒"。决定了视频压缩得好不好,能不能在手机上流畅运行(硬解支持)。

  • 封装格式 (MP4 / MKV / FLV):这是"瓶子"。决定了它能不能支持流媒体播放、能不能装多个音轨、在 Android 上的兼容性。


口述练习:如果你在面试中

问:请解释一下什么是 GOP,它对播放性能有什么影响?

答: "GOP 是画面组,是以 I 帧开始的一组具有参考关系的视频帧。I 帧是全帧数据,P/B 帧是差异数据。GOP 的大小直接影响压缩效率和交互体验:GOP 越长,文件越小,但拖动进度条时的寻址时间(Seek Latency)就越长,因为必须从最近的关键帧开始重建画面。"

1. 轨道分离与选择 (Demuxing)

Java

复制代码
// 1. 初始化解封装器
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath); // 设置文件路径

// 2. 遍历所有轨道(轨道里装的是"酒"的信息)
int trackCount = extractor.getTrackCount();
int videoTrackIndex = -1;

for (int i = 0; i < trackCount; i++) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    
    // 寻找视频轨
    if (mime.startsWith("video/")) {
        videoTrackIndex = i;
        // 这里可以获取 GOP 相关的参数
        int width = format.getInteger(MediaFormat.KEY_WIDTH);
        int height = format.getInteger(MediaFormat.KEY_HEIGHT);
        break;
    }
}

// 3. 锁定视频轨,告诉 Extractor 接下来只读这一路数据
extractor.selectTrack(videoTrackIndex);

2. 按帧读取数据 (The Data Flow)

接下来的过程就像在传送带上取货。每一帧(Sample)被取出来时,都带着它的 PTS标志位

Java

复制代码
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 准备一个 1MB 的盆接数据
while (true) {
    // 读取当前帧的数据到 buffer
    int sampleSize = extractor.readSampleData(buffer, 0);
    
    if (sampleSize < 0) {
        break; // 文件读完了
    }

    // 获取关键信息
    long presentationTimeUs = extractor.getSampleTime(); // 这一帧的 PTS (显示时间)
    int flags = extractor.getSampleFlags(); // 这一帧的标记
    
    // 判断是否为关键帧 (I 帧)
    boolean isKeyFrame = (flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;

    if (isKeyFrame) {
        // 这就是 GOP 的开头!如果没有这一帧,后面的 P/B 帧都解不出来
        Log.d("Video", "Found IDR Frame at: " + presentationTimeUs);
    }

    // 接下来通常是把 buffer 里的数据交给 MediaCodec 去解码
    // decoder.queueInputBuffer(..., presentationTimeUs, ...);

    // 移动到下一帧
    extractor.advance();
}

extractor.release();

关键点剖析

  1. SAMPLE_FLAG_SYNC : 这是代码层面的 GOP 入口标志 。在做视频剪辑(Seek)或者快进时,代码必须先找到最近的这个 SYNC 帧。

  2. MediaFormat : 这里面存储了"元数据"。比如 H.264 的 SPS/PPS(解码参数),没有这些参数,解码器就不知道如何解析后续的像素。

  3. 时间戳 presentationTimeUs : 注意单位是微秒。播放器同步音视频,全靠对比视频轨和音频轨拿到的这个时间。

课后小思考

如果你要在视频里实现"快速拖动进度条",你是应该让 extractor 随便跳到一个时间点,还是跳到最近的一个 SAMPLE_FLAG_SYNC 帧?

我会选择直接跳到最近的关键帧

SPS

1. 它们到底是什么?

  • SPS (Sequence Parameter Set,序列参数集)

    • 作用:描述整个视频序列的"全局特征"。

    • 包含内容:分辨率(宽/高)、帧率上限、编码等级(Profile/Level)、是否包含 B 帧等。

    • 重要性最高。如果 SPS 丢了,解码器不知道该创建多大的画布,直接报错。

  • PPS (Picture Parameter Set,图像参数集)

    • 作用:描述每一帧图像是如何被切分和压缩的。

    • 包含内容:熵编码模式(CABAC/CAVLC)、加权预测等。

    • 重要性。通常一个视频序列里 PPS 会随 SPS 一起出现。


2. 为什么你必须了解它们?

在 Android 开发中,有两个场景你一定会碰到它们:

场景 A:MediaCodec 初始化

当你手动解析视频流并交给 MediaCodec 解码时,你不能直接塞给它 I 帧。你必须先通过 MediaFormat 告诉它 SPS 和 PPS,否则 MediaCodec 会报 IllegalStateException

  • MediaFormat 中,它们被命名为 csd-0 (SPS) 和 csd-1 (PPS)。
场景 B:直播/流媒体推流

在 RTMP 等推流协议中,客户端连接到服务器后的第一件事,就是发送一段特殊的包(通常叫 AVC Sequence Header),里面就死死地封装着 SPS 和 PPS。如果这个包没发,观众点进直播间只会看到黑屏,直到下一个 GOP 的 SPS/PPS 到来。


3. 数据长什么样?(极简解析)

H.264 的原始码流(Annex-B 格式)是由一系列 Start Code (00 00 00 01) 分隔的。你可以通过看 Start Code 后面跟着的第一个字节来分辨:

  • 0x67SPS (0110 0111 -> 最后 5 位是 7)

  • 0x68PPS (0110 1000 -> 最后 5 位 is 8)

  • 0x65IDR 帧 (I 帧) (0110 0101 -> 最后 5 位是 5)

顺口溜总结7 是 SPS,8 是 PPS,5 是关键帧。 只要在二进制里看到 00 00 00 01 67,你就知道:哦!视频的头信息来了!


4. 它们与 GOP 的关系

通常一个 GOP 的第一个帧是 I 帧(关键帧),但在 I 帧之前,编码器必须强制插入 SPS 和 PPS。 结构序列通常是: [SPS] -> [PPS] -> [I Frame] -> [P Frame] -> [P Frame] ...

这样做的目的是:如果用户从视频中间开始看(比如刷短视频刷到一半),只要他能刷到下一个 GOP 的开头,他就能通过那里的 SPS/PPS 重新初始化解码器,从而恢复画面。


口述建议

如果你在聊多媒体链路时提到: "在处理 MediaCodec 解码时,我会先利用 MediaExtractor 提取出 csd-0csd-1 缓冲区的数据,这其实就是 H.264 的 SPS 和 PPS。如果不先配置这些序列参数集,解码器将无法得知视频的分辨率和编码 Profile,导致后续数据无法正常解析。" ------ 这段话会显得你非常专业。

在 H.264 (AVC) 协议中,为了适配从"老旧的功能机"到"高性能的电脑"不同的硬件能力,官方定义了不同的 Profile。


1. 为什么需要 Profile?

视频编码算法非常多。有些算法压缩率极高,但计算起来非常费电、费 CPU;有些算法压缩率一般,但解码非常快。

Profile 的存在就是为了告诉解码器:"这个视频只用了这几个工具进行压缩,你看看你能不能搞定?"


2. 常见的三个"档位"

在 Android 开发和日常业务中,你只需要记住这三个最核心的 Profile:

Profile 名称 压缩效率 硬件要求 应用场景
Baseline Profile (BP) 最低 极低 老旧手机、视频通话(低延迟)、视频监控。不支持 B 帧
Main Profile (MP) 中等 中等 数字电视、早期主流智能机视频。
High Profile (HP) 最高 较高 当前主流。蓝光高清、B站/抖音视频、4K 电影。支持所有压缩工具。

3. Profile 与 Level 的"黄金搭档"

在 Android 的 MediaFormat 中,你经常会看到两个参数:ProfileLevel

  • Profile: 决定**"能用哪些压缩工具"**(决定了清晰度和文件大小)。

  • Level: 决定**"处理速度有多快"**。它规定了最大分辨率、最大帧率和最大码率。

    • 例如:Level 3.0 可能只支持 720P,而 Level 4.1 才能支持 1080P。

4. 在 Android 开发中的实际意义

作为开发者,你为什么要在意这个?

  1. 硬件兼容性(避坑):

    有些低端 Android 设备的硬解码器只支持到 Main Profile。如果你强行给它一个 High Profile 的视频,可能会出现只有声音没画面或者直接解码器崩溃的情况。

  2. 设置编码参数:

    当你使用 MediaCodec 进行录制时,你需要配置这个参数:
    Java

    复制代码
    // 伪代码:设置 H.264 的 High Profile
    mediaFormat.setInteger(MediaFormat.KEY_PROFILE, 
                           MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
    mediaFormat.setInteger(MediaFormat.KEY_LEVEL, 
                           MediaCodecInfo.CodecProfileLevel.AVCLevel41);
  3. B 帧的问题:

    Baseline Profile 是绝对没有 B 帧的。如果你在做低延迟直播,通常会选择 Baseline,因为 B 帧需要参考后面的帧,会带来额外的解压延迟。


5. 总结口述

"Profile 是 H.264 定义的算法特征集。常见的有 Baseline、Main 和 High Profile。Baseline 不支持 B 帧,适合实时性要求高的场景;而 High Profile 压缩效率最高,是目前高清视频的主流。在 Android 开发中,我们需要根据硬件的解码能力(从 SPS 里解析出来的 Profile/Level)来决定是否能流畅播放该视频。"


恭喜!

你现在已经把视频流最核心的"黑话"搞清楚了:

  • GOP: 视频帧的组织结构(I/P/B)。

  • SPS/PPS: 视频的全局配置信息。

  • Profile/Level: 视频的算法档位和性能级别。

这个视频是 High Profile 且带有 B 帧。老设备的解码器不支持处理 B 帧的重排逻辑(DTS 顺序和 PTS 顺序不一致),导致解析出错。 这个没懂

这其实是音视频开发里最烧脑 的一个逻辑点:解码顺序 \\neq 显示顺序

我们要先理解为什么 B 帧会造成这种"混乱"。

1. 为什么会有顺序不一致?

  • I 帧:自己就能解出来(独立)。

  • P 帧 :需要参考前面的帧才能解出来。

  • B 帧 :需要参考前面和后面的帧才能解出来。

关键点就在这里:如果你要解码一帧 B 帧,你必须先拿到它后面的那一帧(通常是 P 帧)。


2. 举个例子:拍皮球

假设一个视频只有 3 帧,画面是一个皮球落下的过程:

  1. 帧 1 (I):球在空中。

  2. 帧 2 (B):球在中间。

  3. 帧 3 (P):球落地。

  • 显示顺序 (PTS):1 -> 2 -> 3 (这是给观众看的)。

  • 解码顺序 (DTS):1 -> 3 -> 2。

为什么解码顺序是 1-3-2? 因为第 2 帧是 B 帧,解码器必须先读到第 3 帧(P 帧),才能根据 1 和 3 的数据"推算"出第 2 帧。所以,在视频文件里,第 3 帧的数据其实排在第 2 帧前面!


3. 老设备的"重排逻辑"崩溃

为了处理这种 1-3-2 的乱序,解码器内部需要一个缓存队列(我们叫 DPB, Decoded Picture Buffer)。

  • 现代设备:解码器很聪明。它先解出 1,存起来;再解出 3,存起来;然后解出 2。最后按照 1-2-3 的 PTS 顺序吐给屏幕。

  • 老旧设备:它的缓存空间极小,或者固件代码写得比较死。它可能默认"解出一帧就得显一帧"。

    • 当它遇到 B 帧时,它发现还没解出后面的参考帧,于是它可能直接跳过 这一帧,或者画面卡住 等待,甚至直接报错

    • 如果它强行按解出的顺序显示,观众看到的画面就是:球在空中 -> 球落地 -> 球突然回到中间(鬼畜跳动)。


4. 为什么 Baseline Profile 没这个问题?

因为 Baseline Profile 强制规定不准使用 B 帧。

在 Baseline 里,全是 I 帧和 P 帧。P 帧只参考前面的,所以:

  • 解码顺序:1 -> 2 -> 3

  • 显示顺序:1 -> 2 -> 3

    两者完全一致。这对老旧硬件极其友好,因为它不需要复杂的内存重排,解一帧显一帧就行。


总结:DTS 与 PTS 的关系

  • DTS (Decoding Time Stamp):你应该什么时候把这堆二进制数据丢进解码器。

  • PTS (Presentation Time Stamp):你应该什么时候把解出来的图片贴在屏幕上。

如果视频里有 B 帧,DTS 和 PTS 就会发生错位。 > JD 考点: 面试官问"为什么直播推流通常不用 B 帧?"

标准回答: "因为 B 帧需要参考后续帧,会产生额外的解码等待,增加端到端的延迟;同时也为了兼容那些处理不好 PTS/DTS 重排的老旧设备。"


课后小练习

如果你现在用 MediaExtractor 读取一个带 B 帧的 MP4,你会发现 getSampleTime() 拿到的时间戳序列并不是从小到大排列的(比如可能是 0, 66, 33...)。

那视频都是IBP这么排布吗

虽然 IBP 是最经典的排布方式,但视频文件里的帧顺序(存储顺序)和帧类型完全取决于编码器的策略应用场景

我们可以把视频帧的排布想象成"排队",常见的排布方式有以下三种:


1. 经典的 IBP 模式(追求极致压缩)

这是你在本地看电影(MP4/MKV)或在 B 站看视频时最常见的排布。

  • 特点:为了把文件压得尽可能小,大量使用 B 帧。

  • 存储顺序(DTS)I, P, B, B, P... (为了让 B 帧能参考到后面的 P 帧,P 帧会提前出现在文件里)。

  • 显示顺序(PTS)I, B, B, P, B, B, P...


2. IP 模式(追求低延迟/兼容性)

在**直播、视频会议(Zoom/WeChat)**中,你几乎看不到 B 帧。

  • 为什么? 因为 B 帧需要"看未来"的帧。如果你在直播时用 B 帧,编码器必须等下一帧 P 帧产生后才能编码这一帧 B 帧,这会额外增加几十甚至上百毫秒的延迟。

  • 排布I, P, P, P, P...

  • 特点DTS 永远等于 PTS。数据一进来,解出来直接就能显示,不需要任何等待。


3. 全 I 帧模式(All-I / Intra-only)

在**专业视频剪辑(如 ProRes 格式)**或高采样率的录像中,可以每一帧都是 I 帧。

  • 排布I, I, I, I, I...

  • 优点

    • 编辑极其流畅:剪辑软件拖动进度条时,不需要计算任何参考帧,点到哪一帧就是哪一帧。

    • 画质极高:没有预测误差。

  • 缺点:文件体积巨大(可能是普通 MP4 的几十倍)。


4. 更加复杂的:分层参考 (Hierarchical B-frames)

这是现代编码器(如 H.265)的高级玩法。B 帧不仅仅参考 I 和 P,甚至 B 帧还可以参考另外一个 B 帧,形成一个复杂的树状结构。这种排布能进一步提升压缩率,但对设备性能的要求也更高。


5. 关键知识点:GOP 的"封闭"与"开放"

你在看帧排布时,还会遇到两个专业词汇:

  • Closed GOP (封闭式):这个 GOP 里的所有帧只参考自己内部的帧。

    • 好处:你从这个 I 帧开始剪切,后面画面绝对不会花。
  • Open GOP (开放式):后面的 B 帧可能会参考上一个 GOP 的末尾帧。

    • 坏处:如果你把视频从中间切开,新视频的第一秒可能会花屏,因为参考帧丢了。

总结:如何判断一个视频是怎么排的?

作为 Android 开发者,你不需要猜。当你拿到一个 MediaFormat 时,你可以观察:

  1. 如果有 B 帧:PTS 的序列必然是乱的(不单调递增)。

  2. 如果没有 B 帧 :每一个 readSampleData 拿到的 getSampleTime() 都是从小到大整齐排列的。

下周实战小贴士:

当你用 MediaCodec 编码视频时,如果你设置 KEY_LATENCY 为 0 或者选择 Baseline Profile,系统就会自动帮你把排布切换成 IP 模式,从而消灭 B 帧带来的延迟。

mediacodec状态机

MediaCodec 是 Android 音视频开发中最难调教的 API,原因就在于它是一个异步的、严格依赖状态 的处理器。你可以把它想象成一个极其死板的加工工厂:你必须先按顺序开机、配置、运行,而且它只通过"传送带"和你交流。


1. 核心流程:生产者-消费者模型

在深入状态机之前,你要先理解它的底层逻辑------Buffer(缓冲区)流转。 它有两条传送带:

  1. Input Buffer 传送带:你把压缩数据(H.264)放进去,递给解码器。

  2. Output Buffer 传送带:解码器把解好的原始画面(YUV/Surface)吐出来,你拿走。


2. 状态机全景图

MediaCodec 的生命周期主要分为三个大状态:Stopped (停止)Executing (执行中)Released (释放)

第一阶段:从"出生"到"就绪" (Stopped 状态)
  1. Uninitialized (未初始化) :你刚 createByCodecName 之后。

  2. Configured (已配置) :你调用了 configure(),告诉它 MediaFormat(就是我们之前学的 SPS/PPS/Profile)。

  3. Flushed (已冲洗) :调用 start() 后的瞬时状态,或者是播放中途 Seek 之后,里面清空了。

第二阶段:忙碌的工作 (Executing 状态)

只有进入这个状态,它才开始吃数据吐数据。它内部细分为:

  • Flushed:待命状态。

  • Running:正在干活。

  • End of Stream (EOS):你告诉它"没货了",它处理完最后的存货后进入这个状态。

第三阶段:寿终正寝 (Released 状态)
  • 调用 release(),释放硬件资源(非常重要!硬解资源是全系统共享的,不释放会导致其他 App 相机或播放器打不开)。

3. 如何与状态机交互?(关键 API)

Executing 状态下,代码的操作逻辑就像是在和工厂玩"接力赛":

  1. dequeueInputBuffer (伸手要盆): 你问解码器:"喂,你现在有空的盆(Buffer)给我装压缩数据吗?"

  2. queueInputBuffer (递交货物) : 你把 H.264 数据装进盆里,并贴上标签(PTS),推回给解码器。

  3. dequeueOutputBuffer (成品出库): 你问解码器:"解好一帧了吗?解好了把盆给我。"

  4. releaseOutputBuffer (还盆并显示): 你处理完这帧(或者直接让它显示在 Surface 上),把盆还给解码器,让它去接下一帧。


4. 为什么大家说 MediaCodec 难用?

  • 非法状态异常 (IllegalStateException) : 如果你在 Stopped 状态调用了 dequeueInputBuffer,它会直接崩掉。你必须时刻清楚它现在在哪。

  • 同步 vs 异步

    • 同步模式:你主动问它要盆(适合简单的离线转码)。

    • 异步模式:你给它一个回调(Callback),它有了空盆自动通知你(Android 5.0+ 推荐,适合丝滑的播放器)。


5. 常见面试题:Seek(拖动进度条)时状态机发生了什么?

当你拖动进度条时,旧的解码数据已经没用了。

  1. 调用 flush():这会让 MediaCodec 立即回到 Flushed 状态。

  2. 丢弃所有旧 Buffer:之前在传送带上的所有帧都会被清空。

  3. 寻找新 I 帧 :从 Extractor 找到新的关键帧,再次 queueInputBuffer


口述练习建议

"MediaCodec 是一个严格的状态机。在初始化并 configure 后,调用 start 进入 Executing 状态。它通过 BufferQueue 机制与开发者交互:我们通过 dequeueInputBuffer 获取输入槽位,填充数据后 queue 回去;再通过 dequeueOutputBuffer 获取解码后的帧,最后 releaseOutputBuffer 将画面渲染到屏幕并归还缓冲区。在使用完后,必须显式调用 release 释放硬件资源。"

1. 同步模式 (Synchronous Mode)

同步模式逻辑直观,适合做离线视频处理(如视频剪辑、转码)。

Java

复制代码
// 1. 初始化并进入 Configured 状态
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, surface, null, 0);

// 2. 开启状态机,进入 Executing 状态
codec.start();

// 3. 循环处理数据
while (!isEndOfStream) {
    // --- 【左手:处理输入】 ---
    // 问工厂要一个空的输入盆 (Timeout 为 10ms)
    int inputBufferIndex = codec.dequeueInputBuffer(10000);
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
        // 从 Extractor 读取压缩数据填充到 inputBuffer
        int sampleSize = extractor.readSampleData(inputBuffer, 0);
        
        if (sampleSize < 0) {
            // 没有数据了,打上结束标记入队
            codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            isEndOfStream = true;
        } else {
            // 把装满数据的盆推回给解码器,并附带 PTS 时间戳
            codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0);
            extractor.advance();
        }
    }

    // --- 【右手:处理输出】 ---
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    // 问工厂:有没有解好的成品?
    int outputBufferIndex = codec.dequeueOutputBuffer(info, 10000);
    
    if (outputBufferIndex >= 0) {
        // 拿到解好的像素数据,true 表示渲染到配置的 Surface 上
        codec.releaseOutputBuffer(outputBufferIndex, true);
    } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 重点:如果视频分辨率变了,或者是第一次拿到 SPS/PPS,会触发这个
        MediaFormat newFormat = codec.getOutputFormat();
    }
}

// 4. 释放资源
codec.stop();
codec.release();

2. 异步模式 (Asynchronous Mode)

Android 5.0+ 推荐方式。由于不阻塞主线程,它是高帧率播放器的首选。

Java

复制代码
// 1. 创建实例
MediaCodec codec = MediaCodec.createByCodecName(name);

// 2. 设置回调(必须在 configure 之前!)
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // 工厂主动喊你:"有空盆了,快来装货!"
        ByteBuffer inputBuffer = codec.getInputBuffer(index);
        // 填充数据逻辑同同步模式...
        int sampleSize = extractor.readSampleData(inputBuffer, 0);
        codec.queueInputBuffer(index, 0, sampleSize, extractor.getSampleTime(), 0);
        extractor.advance();
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        // 工厂主动喊你:"成品出来了,快接走!"
        // 处理 info.presentationTimeUs (用于音画同步)
        codec.releaseOutputBuffer(index, true); 
    }

    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
        // 视频元数据(宽、高、SPS、PPS)发生变化或初始确定时回调
        Log.d("Codec", "New Format: " + format.toString());
    }

    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 状态机出错,通常需要 stop 并 release
    }
});

// 3. 配置并启动
codec.configure(format, surface, null, 0);
codec.start(); // 之后一切都在 Callback 驱动下运行

3. 两种模式的深度对比

维度 同步模式 (Sync) 异步模式 (Async)
控制权 开发者主动轮询 (Poll) 状态机主动通知 (Push)
线程性能 容易阻塞线程,需要自己管理循环 效率极高,内部通过 Handler 线程分发
复杂度 简单,像流水线 稍难,需要处理多线程并发逻辑
主要用途 视频剪辑、后台转码、录屏 直播播放器、高性能视频解码

4. 为什么注释里强调 onOutputFormatChanged

在 Week 1 你学了 SPS/PPS 。当你调用 start() 之后,解码器其实还没法立刻解码,它必须先等到第一帧数据里的 SPS/PPS 被解析出来。

  • 在同步模式下,你会收到一个 INFO_OUTPUT_FORMAT_CHANGED 的返回值。

  • 在异步模式下,会触发 onOutputFormatChanged。

    此时你才能拿到视频真正的宽高、裁剪区域(Crop Rect)等关键渲染信息。

深入 MediaExtractor:数据的"精准投喂"

如果把 MediaCodec 比作加工厂,MediaExtractor 就是仓库管理员。它负责把 MP4 这个"大箱子"拆开,选出你要的那条线(视频或音频轨),并精准地把每一帧"货物"投递到传送带上。

以下是关于 MediaExtractor 深度使用的三个核心维度:


1. 核心流程:从"拆箱"到"选货"

在 Android 中使用 Extractor,必须遵循严格的顺序,否则它不知道该从哪里开始读取。

  • setDataSource: 告诉管理员箱子在哪(本地路径、URI 或 FileDescriptor)。

  • selectTrack: 这是初学者最容易忘的一步。MP4 里可能有多条轨道(不同语言的音频、不同分辨率的视频)。你必须明确告诉它:"我现在要读第 0 号轨道的数据"。

  • readSampleData: 把当前位置的一帧压缩数据拷贝到 ByteBuffer 里。

  • advance : 手动挡操作。读取完一帧后,你必须调用它,管理员才会把指针指向下一帧。如果不调用,你会死循环一直读同一帧。


2. 精准投喂的灵魂:Seek(寻址)

这是 Extractor 最有技术含量的地方。当用户拖动进度条到第 10 秒时,你不能直接从第 10 秒开始解。

为什么? 还记得 Week 1 学的 GOP 吗?

如果第 10 秒恰好是一个 P 帧,它没有参考的前置 I 帧,解码器就会报一堆错,屏幕上全是花屏。

seekTo(timeUs, mode) 的三种模式:
模式 行为描述 应用场景
SEEK_TO_PREVIOUS_SYNC 寻找目标时间点之前最近的一个关键帧(I 帧)。 最常用。保证拖动后画面立刻能解出来,不花屏。
SEEK_TO_NEXT_SYNC 寻找目标时间点之后最近的一个关键帧。 较少用,可能会导致跳过一段画面。
SEEK_TO_CLOSEST_SYNC 寻找离目标点最近的关键帧。 兼顾准确度和速度。

实战细节 :如果你追求极致的"精准定位"(比如剪辑软件),通常是先 SEEK_TO_PREVIOUS_SYNC 找到前一个 I 帧,然后解码器空跑,解出后续的帧但不显示,直到到达目标时间点再渲染。


3. 如何判断"投喂"结束?

当 readSampleData 返回 -1 时,说明这个轨道的货发完了。

这时你必须向 MediaCodec 发送一个特殊的信号弹:BUFFER_FLAG_END_OF_STREAM。

如果不发这个信号,解码器会一直蹲在那等下一帧,导致你的播放器最后几帧画面"憋"在解码器里出不来,表现为视频末尾卡顿一下。


4. 进阶:Extractor 的"坑"与性能

  • 网络流支持差 :原生的 MediaExtractor 虽支持 URL,但在网络不稳时表现极差(容易阻塞、超时不可控)。

    • 大厂策略 :通常会用 FFmpegavformat 来代替原生 Extractor 处理网络流(如 HLS, DASH)。
  • 多线程安全MediaExtractor 不是线程安全的。如果你在主线程 Seek,在子线程 Read,极易崩溃。

  • 内存泄露 :用完一定要 release(),否则文件句柄不释放,会导致 App 无法再次打开文件。


本周自测:数据的"身份证明"

当你通过 extractor.readSampleData 拿到一袋数据时,你还需要通过以下方法拿到它的"身份证",并一起塞给解码器:

  1. getSampleTime() : 这一帧的 PTS(非常重要,决定了这帧画面什么时候显示)。

  2. getSampleFlags() : 这一帧是不是 关键帧 (如果是 SAMPLE_FLAG_SYNC,说明 GOP 重新开始了)。

如果把 MediaExtractor 比作仓库管理员 ,那么 DataSource 就是运输车队。管理员不能凭空变出货物,他必须先指定"货在哪里"以及"怎么运过来"。


1. 它们的关系:DataSource 是 Extractor 的"输入源"

在 Android 的标准 API 流程中,它是这样组织的:

  1. DataSource (数据源):确定物理位置。可以是:

    • 本地文件路径 (/sdcard/video.mp4)

    • 网络 URL (https://example.com/movie.m3u8)

    • 资源文件 (res/raw/video)

    • 甚至是内存中的一个 FileDescriptor

  2. Extractor (解封装器):连接到 DataSource 后,开始"扫描"文件的头部(Header),寻找轨道信息(Track Info)。

代码层面的体现:

Java

复制代码
MediaExtractor extractor = new MediaExtractor();
// 这步就是在指定 DataSource
extractor.setDataSource(context, uri, headers); 

2. 为什么开发者常说 DataSource 是"第一道坑"?

虽然 setDataSource 看起来只是一行代码,但在实际 JD 对齐的工程能力中,这里学问很大:

A. 协议支持 (Protocols)
  • 原生限制 :Android 原生的 MediaExtractor 默认支持的文件格式(MP4, TS, MKV)和协议(HTTP/HTTPS)是有限的。

  • 工程现状 :如果你要做直播(RTMP)或特殊的加密流,原生的 DataSource 就搞不定了。这时大厂会用 FFmpeg 重新实现一套自定义的 DataSource 层。

B. 网络缓冲 (Buffering)
  • 如果 DataSource 是网络地址,MediaExtractor 会尝试边下边读。

  • 问题 :如果网速慢,extractor.readSampleData 就会阻塞(Block)。

  • 优化 :在 DataSource 这一层,通常会做一个缓存队列(Buffer)。先让运输车队把数据拉到本地缓存,Extractor 再从缓存里拿,这样可以有效防止播放卡顿。


3. 播放器的完整分层(从底向上)

为了让你下周写代码时不乱,请记住这个层级图:

层级 角色 功能 例子
1. DataSource 运输员 负责把二进制字节流拿过来。 HttpDataSource, FileDataSource
2. Extractor 拆包员 识别格式,把字节流拆成音频/视频帧。 MediaExtractor, MP4Extractor
3. Decoder 加工厂 把压缩帧还原成像素。 MediaCodec
4. Renderer 展示员 负责画出来、响出来。 Surface, AudioTrack

4. 一个容易混淆的概念:MediaDataSource

Android 6.0 引入了一个类叫 MediaDataSource。它允许开发者自己写代码来提供数据

  • 场景:比如你的视频数据是加密的,或者存在一个特殊的数据库里。

  • 做法 :你可以继承这个类,重写 readAt() 方法。这样当 Extractor 需要数据时,它会回调你的方法,你解密后再传给它。

核心播放器逻辑:VideoPlayer 伪代码

Java

复制代码
// 1. 设置数据源 (DataSource)
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath); // 也可以是 URL 或 AssetFileDescriptor

// 2. 寻找视频轨并初始化解码器 (Demuxing & Init)
int trackIndex = selectVideoTrack(extractor);
MediaFormat format = extractor.getTrackFormat(trackIndex);
String mime = format.getString(MediaFormat.KEY_MIME);

MediaCodec decoder = MediaCodec.createDecoderByType(mime);
// 关键:将 Surface 传给 configure,解码后画面会自动渲染到屏幕上
decoder.configure(format, surface, null, 0); 
decoder.start();
extractor.selectTrack(trackIndex);

// 3. 数据流转循环 (The Pumping Loop)
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean isInputEOS = false;
boolean isOutputEOS = false;

while (!isOutputEOS) {
    
    // --- 【第一步:喂数据 (Input)】 ---
    if (!isInputEOS) {
        int inputIdx = decoder.dequeueInputBuffer(10000); // 10ms 超时
        if (inputIdx >= 0) {
            ByteBuffer inputBuffer = decoder.getInputBuffer(inputIdx);
            // 从 Extractor 掏出一帧压缩数据
            int sampleSize = extractor.readSampleData(inputBuffer, 0);
            
            if (sampleSize < 0) {
                // 读取完毕,发送结束标志
                decoder.queueInputBuffer(inputIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                isInputEOS = true;
            } else {
                // 投递到解码器,并附带最重要的 PTS
                decoder.queueInputBuffer(inputIdx, 0, sampleSize, extractor.getSampleTime(), 0);
                extractor.advance(); // 推进到下一帧
            }
        }
    }

    // --- 【第二步:拿画面 (Output)】 ---
    int outputIdx = decoder.dequeueOutputBuffer(info, 10000);
    if (outputIdx >= 0) {
        // 渲染同步逻辑(伪代码):
        // 这里需要根据 info.presentationTimeUs 与系统时间对比,决定是否 sleep
        // 否则视频会瞬间播完(解码多快播多快)
        sleepUntilRenderTime(info.presentationTimeUs);
        
        // render=true 表示将此 Buffer 发送到配置好的 Surface 渲染
        decoder.releaseOutputBuffer(outputIdx, true);
        
        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            isOutputEOS = true;
        }
    } else if (outputIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 这里可以拿到视频真实的宽、高、Crop矩形等信息
        MediaFormat newFormat = decoder.getOutputFormat();
    }
}

// 4. 释放资源
decoder.stop();
decoder.release();
extractor.release();

关键点解析:

1. 关于 Surface 的"魔法"

注意 decoder.configure(format, surface, ...)。在 Android 中,如果你配置了 SurfaceMediaCodec 解码后的数据不会返回给 CPU 内存,而是直接留在 显存 (BufferQueue)中。当你调用 releaseOutputBuffer(index, true) 时,系统只需交换一下指针,画面就显示出来了。这就是 Zero-Copy (零拷贝)

2. 关于 sleepUntilRenderTime (音画同步雏形)

这是你下周实战最容易漏掉的地方。

  • 原因MediaCodec 解码速度极快(一秒钟能解几百帧)。

  • 现象:如果你不写控制时间的逻辑,你的视频会像"闪电侠"一样几秒钟就播完了。

  • 做法 :你需要记录一个 startTime,然后每次对比 info.presentationTimeUs,如果还没到时间,就让线程 Thread.sleep() 一下。

3. 关于 INFO_OUTPUT_FORMAT_CHANGED

为什么不直接用 extractor.getTrackFormat() 拿到的宽高? 因为有些 H.264 视频存在 Crop(裁剪区) 。例如视频声明是 1080P,但实际有效画面可能只有 1080x800。只有等解码器解析了第一个 SPS 后,从 getOutputFormat() 拿到的数据才是最终准确的。

深入 Surface:画面的"最后触达"

在 Android 音视频开发中,Surface 并不是一个"控件",而是一条通往屏幕的隧道


1. Surface 的本质:一个 BufferQueue 的生产者端

在底层,Surface 的工作原理是一个极其高效的**"双缓冲"或"三缓冲"队列**。

  • BufferQueue:这是核心。它像是一个装着几个空脸盆(GraphicBuffer)的架子。

  • Producer (生产者) :在我们的场景里,就是 MediaCodec。它负责把解码后的像素塞进盆里。

  • Consumer (消费者) :就是 SurfaceFlinger(系统服务)。它负责把盆里的像素贴到屏幕上。


2. 画面的"最后触达"经历了什么?(物理过程)

当你调用 decoder.releaseOutputBuffer(index, true) 时,底层发生了这几件事:

  1. 数据的搬运 :实际上并没有发生像素拷贝。解码器只是把这个 Buffer 的所有权交还给了 BufferQueue。

  2. 入队 (Queue) :这个 Buffer 被标记为 Filled(已装满),进入 BufferQueue 的待处理队列。

  3. 信号通知:BufferQueue 通知消费者(SurfaceFlinger):"有新货到了!"

  4. 合成 (Composition):在下一个屏幕刷新信号(V-Sync)到来时,SurfaceFlinger 把这个 Buffer 的数据取出来,配合其他 App 的窗口(状态栏、导航栏),交给 GPU 合成出一张最终的屏幕图像。

  5. 归还 (Release) :合成完成后,SurfaceFlinger 把 Buffer 标记为 Free(空闲),还给 BufferQueue,循环开始。


3. "零拷贝" (Zero-Copy) 为何重要?

这是音视频开发最核心的性能点。

  • 如果没有 Surface:解码器解出像素 \\rightarrow 拷贝到内存 \\rightarrow 拷贝给显卡 \\rightarrow 渲染。这会消耗大量的 CPU 和内存带宽,导致手机发热、4K 视频卡顿。

  • 有了 Surface:解码器直接把像素解到**显存(GraphicBuffer)**里。SurfaceFlinger 也是直接操作这块显存。

    真相:像素数据从始至终都在显存里没动过,改变的只是这块内存的"访问权限"。


4. 两个关键组件:SurfaceView vs TextureView

在 Android 上展示这个 Surface,你有两种选择,它们的"触达"路径完全不同:

特性 SurfaceView TextureView
渲染位置 拥有自己独立的 Window,在主窗口后面 在 App 的 View 层级中,像个普通 View
打洞逻辑 系统会在你的 App 窗口上"挖个洞",让后面的 Surface 露出来 将视频帧转换为 GL 纹理,在你的 View 树里合成
性能 极高。数据直接给 SurfaceFlinger。 略低。需要先过一遍 GPU 合成到 View 树。
适用场景 全屏电影、游戏、视频通话。 抖音式列表、需要旋转/半透明/缩放视频的场景。

5. 一个深层次的坑:Surface 的生命周期

这是开发者最常犯错的地方。Surface 的生命周期和 Activity不同步的。

  • 当你按 Home 键,Activity 只是 onStop,但你的 Surface 可能会被销毁(Destroyed)。

  • 后果 :如果你此时还在调用 MediaCodec.releaseOutputBuffer,系统会因为找不到接收方而直接抛出异常(IllegalStateException)。

  • 对策 :必须监听 SurfaceHolder.CallbackSurfaceTextureListener。当 onSurfaceDestroyed 触发时,必须立刻停止解码线程!


总结:底层视角

Surface 是一个 Buffer 的传送带。

MediaCodec 负责填满 Buffer,releaseOutputBuffer(..., true) 负责启动传送带,SurfaceFlinger 在传送带末头负责把 Buffer 贴在屏幕上。


本周结课:你已经掌握了整个闭环!

  • 数据源DataSource -> MediaExtractor

  • 加工厂MediaCodec (Input/Output Buffer 流转)

  • 显示路径Surface -> BufferQueue -> SurfaceFlinger -> 屏幕

你现在的理论深度,已经足以支持你写出一个高性能的播放器了。

显存是积累一定数据 再送到surfaceflinger吗?

简单直接的回答是:不是积累一堆,而是"按需、逐帧"地发送。 但在底层,为了保证画面的丝滑(不掉帧、不撕裂),它确实存在一个极小规模的缓存机制


1. 物理真相:逐帧提交 (One by One)

当你调用 decoder.releaseOutputBuffer(index, true) 时,你实际上是下达了一个**"发货指令"**。

  • 动作 :解码器把这一帧 (一个显存 Buffer)标记为"已填满",并推入 BufferQueue

  • 频率 :如果是 30fps 的视频,这个动作大约每 33ms 发生一次。

  • 结论:它不会等攒够 10 帧再送,因为那样会导致严重的播放延迟。


2. 这里的"缓存"是怎么回事?(三缓冲机制)

虽然是逐帧送,但 Surface 底层的 BufferQueue 通常会准备 2 到 3 个显存盆(Buffer) 。这就是著名的 Double/Triple Buffering

这个"小积累"的作用是:

  • Buffer A (正在显示):SurfaceFlinger 正在把它画在屏幕上。

  • Buffer B (正在解压):解码器正在往里写下一帧的数据。

  • Buffer C (准备就绪):如果解码器跑得快,解好了一帧,它会停在队列里等屏幕刷新。

这种"积累"是为了对抗波动。 万一解码器下一帧卡了一下(比如遇到复杂的 I 帧),屏幕还可以先显示已经准备好的 Buffer C,不至于让用户看到黑屏或卡顿。


3. 谁来决定什么时候"送"?(V-Sync 信号)

即使解码器把 Buffer 送到了 BufferQueue,画面也不会立刻出现在屏幕上。这里有一个**"守门员"**:V-Sync(垂直同步信号)

  • 原理 :手机屏幕通常每秒刷新 60 次(60Hz)。每隔 16.6ms,系统会发出一个 V-Sync 信号。

  • 过程

    1. 你调用 releaseOutputBuffer

    2. Buffer 进入队列"排队"。

    3. V-Sync 信号到来

    4. SurfaceFlinger 准时现身,从队列里取走最新的一帧,贴到屏幕上。


4. 特殊情况:如果解得太快会怎样?

如果你关闭了时间控制(音画同步),让 while 循环疯跑:

  1. 解码器瞬间填满了 BufferQueue(假设有 3 个盆)。

  2. 当它想塞第 4 个盆时,dequeueInputBufferdequeueOutputBuffer 就会阻塞(或者返回等待)。

  3. 现象:解码器被迫降速,变得和屏幕刷新率一样快。


总结:你的直觉对了一半

  • 不对的地方:它不是为了节约成本去"攒一波"发送。

  • 对的地方 :它确实利用 2-3 个 Buffer 做了微量积累 ,目的是为了平滑抖动,让解码器的"生产"和屏幕的"消费"能够完美错开。


深度思考题(带入 Week 2 实战)

如果一个视频是 120fps(每帧 8ms),但屏幕是 60Hz(每帧 16ms),会发生什么?

答案 :解码器会辛苦地解出所有帧,但 SurfaceFlinger 每次刷新只会拿最新的一帧,中间的那一帧会被"丢弃"掉。这就是为什么高帧率视频在低刷新率屏幕上看起来没区别的原因。

底层原理聊到这里,你已经把从"比特流"到"光信号"的最后一段路走通了!

下周我们写代码时,你会亲眼看到 releaseOutputBuffer 是如何驱动这些 Buffer 流转的。

1. 传统路径:CPU 搬运工(低效)

如果没有 Surface 这种直接通道,像素数据需要经历一次漫长的"大循环":

  1. 解码 :CPU(或硬解后回传)把数据解压到 系统内存

  2. 转换:CPU 把 YUV 格式转换成屏幕能读的 RGB 格式。

  3. 拷贝 :CPU 将几兆大小的像素阵列通过 系统总线 拷贝给显卡。

  4. 渲染:显卡拿到数据画出来。

代价 :对于一个 4K 60fps 的视频,每秒要拷贝约 1.5GB 的数据。这会直接占满内存带宽,导致手机发烫,其他 App 运行卡顿。


2. Surface 路径:显存"瞬移"(高效)

有了 Surface 和显卡直接对接后,情况变成了这样:

  • 物理上的"原地踏步" : 由于 MediaCodec 配置了 Surface,解码器解出来的像素其实直接就在显卡的显存(GraphicBuffer)里

  • 逻辑上的"所有权变更" : 当调用 releaseOutputBuffer(..., true) 时,并没有发生数据搬运,只是发了一个**"令牌"**。

    • 解码器说:这块显存我写完了,令牌给你。

    • 显卡(GPU)说:收到令牌,我现在去这块显存地址读数据并画出来。


3. 具体节省了哪些性能?

A. 极大的带宽节省(最重要)
  • 原理:数据不需要在"系统内存(RAM)"和"显存(VRAM)"之间飞来飞去。

  • 结果:节省了大量的电量。手机看视频能看 10 小时,很大程度归功于这种**"零拷贝" (Zero-Copy)** 机制。

B. 解放 CPU
  • 原理:CPU 不再需要参与像素的搬运和 YUV 到 RGB 的色彩空间转换(这部分由 GPU 硬件单元瞬时完成)。

  • 结果:CPU 占用率可能从 80% 降到 5%。你的 App 可以在播放视频的同时,流畅地处理评论区滚动或复杂的 UI 动画。

C. 降低延迟
  • 原理:由于少了多次内存拷贝的耗时(一次 4K 拷贝可能要几毫秒),画面能更及时地响应 V-Sync 信号。

  • 结果:音画同步更精准,直播延迟更低。


4. 总结:这是一种"特权通道"

你可以把 Surface 理解为 Android 系统给视频和游戏开的一条**"特权高速公路"**。

普通的 UI(如按钮、文字)通常要在主线程慢慢画、慢慢合成;而视频数据通过 Surface 绕过了复杂的 View 树层级,直接在硬件层完成投递

底层真相 : 显卡并不是"被动接收数据",而是和解码器"共用一间仓库"。解码器在仓库左边装货,显卡在仓库右边出货。节省性能的本质,就是"不动数据,只动指针"。

相关推荐
知南x4 小时前
【STM32MP157 视频监控项目】(2) 移植 Nginx
stm32·nginx·音视频
却道天凉_好个秋9 小时前
音视频学习(八十四):视频压缩:MPEG 1、MPEG 2和MPEG 4
学习·音视频
却道天凉_好个秋10 小时前
音视频学习(八十三):视频压缩:MJPEG技术
学习·音视频·mjpeg·视频压缩
qianbo_insist10 小时前
基于图像尺寸的相机内参拼接视频
数码相机·音视频·拼接
水中加点糖10 小时前
RagFlow实现多模态搜索(文、图、视频)与(关键字/相似度)搜索原理(二)
python·ai·音视频·knn·ragflow·多模态搜索·相似度搜索
却道天凉_好个秋11 小时前
音视频学习(八十二):mp4v
学习·音视频·mp4v
winfredzhang11 小时前
从零构建:基于 Node.js 的全栈视频资料管理系统开发实录
css·node.js·html·音视频·js·收藏,搜索,缩略图
行业探路者1 天前
二维码标签是什么?主要有线上生成二维码和文件生成二维码功能吗?
学习·音视频·语音识别·二维码·设备巡检
Android系统攻城狮1 天前
Android16音频之获取Record状态AudioRecord.getState:用法实例(一百七十七)
音视频·android16·音频进阶