媒体播放
媒体播放把文件或网络中的音视频数据,按媒体时间线转换成声音和画面。下载、解码和输出并不是同一个动作:数据已经到达,不代表已经解码;帧已经解码,也不代表现在就应该显示。
播放器项目的复盘需要解释这条过程,快进、重连与歌词同步也依赖这些机制。重点是分清格式、时间和状态的责任,再说明暂停、缓冲、切换媒体与失败时怎样恢复。本文使用平台播放器的通用机制,示例采用 Swift 5 与 Core Media。
1. 面试核心
先记住这几条,再读正文- 容器、编码格式和传输方式描述不同层次。MP4 可以装载不同音视频编码,HLS 用播放列表组织媒体段,不能把扩展名当作完整的解码能力说明。卡 · 容器、编码与传输
- 播放过程包含获取、解封装、解码、缓冲、时钟调度和输出。各阶段可以并行准备,也可能因资源不足互相等待。卡 · 格式与播放链路
- 呈现时间决定何时输出,解码顺序不一定等于显示顺序。音画同步需要统一时间基准,不能各按网络到达顺序播放。卡 · 时间戳与音画同步
- 用户想播放、播放器已准备好和当前真的在播放是不同状态。暂停、缓冲、结束和错误要分别处理。卡 · 缓冲与播放状态
- 跳转通常需要定位可解码位置,再恢复目标附近的时间线。不能把媒体时间简单换算成任意文件字节偏移;重复 seek 还需要处理过期完成结果。卡 · 综合题
- 歌词应根据播放器当前媒体时间定位。定时器只用于触发更新,不能累计它的触发次数作为播放进度。卡 · 综合题
- 播放监听、解码资源和输出设备都有生命周期。离开页面、切换媒体、耳机变化和后台行为需要明确的状态与清理规则。
2. 格式与播放链路
2.1 容器、编码与传输
容器格式组织轨道、媒体样本、时间和相关元数据,例如 MP4。编码格式定义图像或声音怎样表示、压缩和恢复,例如 H.264 视频与 AAC 音频。一个文件可以同时有视频、音频和字幕轨道,各自使用不同编码。
HLS 则通过播放列表描述媒体段和不同播放版本,客户端读取列表并获取媒体数据。它不能替代容器解析或音视频解码;同样是 HLS,段格式、编码、加密与设备支持不同,也可能产生不同结果。
判断“这个文件能不能播”,需要确认容器解析、样本编码、分辨率与编码参数、平台解码能力,以及必要的解密或授权条件。文件扩展名只是线索,不足以完成判断。
2.2 从数据到声音和画面
| 阶段 | 主要责任 | 输出或等待条件 |
|---|---|---|
| 获取 | 读取本地文件或网络数据 | 编码后的字节、完整性与网络错误 |
| 解封装 | 从容器中分出轨道、样本和时间信息 | 各轨道的压缩样本与时间戳 |
| 解码 | 将压缩样本恢复为像素或音频采样 | 可用于输出的图像与声音数据 |
| 缓冲 | 在获取、解码与消费速度之间储备数据 | 有界队列与可播放范围 |
| 时间调度 | 根据媒体时钟决定何时呈现 | 等待、显示、丢弃过期视频等策略 |
| 输出 | 将图像交给显示链路、音频交给输出设备 | 实际画面与声音 |
解封装也叫 demux,它从混合容器中分出不同轨道,不负责把 H.264 恢复成像素。解码则处理编码本身,不能用“把文件读取到内存”替代。
这些阶段可以重叠执行:输出当前音频时,同时下载和解码后面的数据。队列需要有上限,否则网络很快或多个播放器同时准备时,仍可能积累大量内存。硬件解码也有格式、实例与资源限制,不是无限且没有开销的通道。
媒体的来源也可能是麦克风或摄像头采集。采集端先获得原始音频或图像,再根据需求编码、封装和发送;播放端进行对应的读取与恢复。实时通话还涉及更紧的时延和网络抖动约束,不能直接照搬长缓冲的视频点播策略。
3. 时间戳与音画同步
3.1 媒体时间与墙上时间
媒体时间表示内容中的位置,例如这首歌的第 35 秒;墙上时间是现实世界的日期和时间。暂停五秒后,现实时间过去了五秒,媒体位置却可以保持不变;倍速、跳转和缓冲也会改变两者关系。
Core Media 用 CMTime 表示时间,数值部分可以理解为 value / timescale,还包含有效性和时间线等信息。300/600 与 1/2 表示相同长度,不能只比较分子;无效、无限或未确定时间也不能直接当成普通秒数使用。
播放器给出的时间还要对应同一个媒体或播放窗口。直播窗口、分段时间线和媒体切换可能改变起点;拿上一首歌的回调更新下一首歌,数值再准确也属于错误内容。
3.2 解码时间与呈现时间
呈现时间戳(PTS)表示媒体样本应该在时间线上何时呈现;解码时间戳(DTS)描述需要按什么时间顺序送入解码过程。编码依赖可能使这两个顺序不同。
例如某个双向预测视频帧需要前后参考帧,后面的参考帧必须先准备好,解码器再恢复中间帧。不能把解码器每次产出的顺序无条件当成屏幕显示顺序;具体的重排序规则由编码与播放器处理。
3.3 对齐输出的时间基准
音画同步让音频和视频围绕同一个媒体时间基准输出。播放器可以选择音频、视频或外部时钟作为参考,具体取决于场景;有音频的普通播放通常更重视连续的声音输出,但不能把“永远以音频为主时钟”当成所有实现的规定。
视频帧到得太早可以等待,到得太晚可能需要丢弃或调整调度;策略要结合允许的时延和同步误差。网络到达时间、解码完成时间和最终显示时间不同,不能只让两条线程各自“收到就播”。
播放器的主时钟与平台输出设备也有关系。蓝牙、外部播放设备、音频中断或输出路由变化时,应用应处理平台事件,不能假定一套固定延迟始终成立。
4. 缓冲与播放状态
4.1 缓冲解决速率波动
缓冲在数据生产与消费之间保存一段待播放内容。网络有短暂抖动时,可以继续消费已有数据;缓冲不足时,播放器可能暂停推进媒体时间,等待补充。
更大缓冲可能减少部分停顿,但会增加内存、首次播放等待或直播延迟,也可能下载用户根本不会看到的内容。还要分清下载字节缓冲、压缩样本队列与已解码帧,它们的成本不同。
自适应码率根据带宽、缓冲和设备条件选择合适版本。码率降低可能改善持续播放,但切换仍要满足轨道与时间线衔接,不能任意拼接两个不同编码片段。
4.2 准备好与正在播放
播放器状态应至少区分未准备、缓冲、可播放、暂停、播放中、结束和失败。具体 API 可能把这些信息拆成多个字段,不能只读一个布尔值。
在 Android Media3 的 Player 模型中,STATE_READY 表示能够从当前位置播放,是否正在播放还要结合 playWhenReady 和播放抑制原因。iOS 的 AVPlayerItem.status == .readyToPlay 也不代表正在输出,AVPlayer.timeControlStatus 进一步区分暂停、等待和播放。
用户点过播放后发生缓冲,不应把“播放意图”丢掉;用户主动暂停后网络恢复,也不应擅自恢复播放。结束与错误则有不同的恢复动作:重播、切换内容、检查授权或按预算重试,不能都归为“再调一次 play”。
5. 跳转与重新连接
5.1 时间定位与解码依赖
跳转(seek)把播放位置移动到目标媒体时间。播放器通常根据容器索引、分段信息和可随机访问位置定位,再解码必要的依赖数据,直到能够呈现目标附近的内容。
压缩视频不一定能从任意帧独立开始解码,变码率文件也不能按“总字节数 × 时间比例”准确定位。关键帧或其他随机访问点有助于恢复,但不是每个目标时间都刚好落在可独立解码的位置。
精确跳转与近似跳转存在代价差异。AVPlayer 的 seek 接口允许指定前后容差,零容差可以请求更精确的定位,但可能增加解码或等待成本;不能把它等同于任意格式、任意网络条件下立即精确显示。
5.2 连续跳转与旧任务
拖动进度条可能产生多个 seek。界面可以按需求合并中间请求,区分“用户正在预览的位置”和“播放器已经确认的位置”,避免每移动一个像素都重做大量解码。
异步完成回调要对应当前媒体和当前请求。用户从 A 切到 B 后,A 的 seek、加载或错误回调不能覆盖 B 的状态;旧请求被中断的完成结果也不应被当成新跳转成功。
重连时先确认原媒体、可用时间范围和授权条件。点播可以尝试恢复保存的进度,直播则可能已经滑出原窗口;旧段不存在时需要按产品策略回到可用位置。恢复连接不等于直接复用所有旧缓冲与时间戳。
6. 歌词与播放时间轴
6.1 按媒体时间查找当前歌词
歌词时间轴把歌词条目与媒体位置对应。更新时读取播放器当前时间,找到开始时间不晚于该位置的最后一条,而不是让一个定时器每触发一次就把行号加一。
下面的 Swift 5 示例使用 CMTime 比较有理数时间。初始化时拒绝无效、负数、非零 epoch 或乱序时间;相同开始时间允许出现,并选择其中最后一条。若产品要同时显示合唱或双语行,可以先把同一时间的文本合并成一个条目。
import Foundation
import CoreMedia
struct LyricCue {
let start: CMTime
let text: String
}
struct LyricTimeline {
let cues: [LyricCue]
init?(cues: [LyricCue]) {
guard cues.allSatisfy({ $0.start.isNumeric && $0.start.epoch == 0 &&
CMTimeCompare($0.start, .zero) >= 0 }),
zip(cues, cues.dropFirst()).allSatisfy({
CMTimeCompare($0.0.start, $0.1.start) <= 0
}) else { return nil }
self.cues = cues
}
func index(at time: CMTime) -> Int? {
guard time.isNumeric, time.epoch == 0,
CMTimeCompare(time, .zero) >= 0 else { return nil }
var low = 0
var high = cues.count
while low < high {
let middle = low + (high - low) / 2
if CMTimeCompare(cues[middle].start, time) <= 0 {
low = middle + 1
} else {
high = middle
}
}
return low == 0 ? nil : low - 1
}
}
假设歌词从 1 秒、3 秒和 5 秒开始,时间为 0.5 秒时没有当前行,3 秒时定位第二行。跳回 1.5 秒时会重新定位第一行,不依赖上次行号;超过最后一行后,示例保留最后一行,由调用方根据媒体结束状态决定是否清空。
这段代码只处理已经解析好的时间轴,不负责 LRC 文件格式、逐字动画或播放器授权。初始化检查为线性复杂度,之后每次定位为二分查找;实际 UI 只在行号或展示内容变化时更新,避免每次回调都滚动列表。
6.2 播放时间观察与清理
AVPlayer 可以通过 addPeriodicTimeObserver(forInterval:queue:using:) 提供周期性媒体时间观察。回调间隔是更新请求,不能当作精确实时钟;发生暂停、跳转或速率变化时,都应以回调或播放器给出的实际媒体时间定位。
保存注册返回的观察 token,并在不再需要时对创建它的同一个 player 调用 removeTimeObserver。不要仅把 token 置空,也不要让回调长期强持有已经退出的页面;回调队列必须是串行队列,更新 UI 时使用主队列。移除观察后,已经开始执行的回调仍可能继续,因此媒体身份检查也要保留。
播放器切换媒体时,歌词也要切换到对应时间轴,并使旧回调失效。实时播放或带偏移的字幕需要先把播放器时间映射到歌词使用的时间基准,不能默认所有时间都是从零开始的同一条轴。
7. 资源、生命周期与验证
播放器可能持有解码器、缓冲、网络连接、音频会话和图形资源。页面离开不一定等于播放必须停止,例如音乐可能按产品要求继续后台播放;但不再需要的页面监听和 UI 资源应独立清理。
是否支持后台播放、如何应对音频焦点或会话中断、耳机断开后是否暂停,都要按平台和产品规则处理。不能靠一个界面定时器维持后台播放,也不能默认多个播放器能无限并行占用解码资源。
验证先区分事实:读取到轨道、成功解码样本、媒体时间推进和用户实际听到或看到,分别需要对应证据。本地静音文件的解码或 seek 测试,不能证明网络 HLS、真实蓝牙输出或真机音画同步已经通过。
故障定位可以依次检查源数据与授权、容器和编码支持、缓冲与状态、时间戳和跳转、输出资源与生命周期。保留首帧时间、重新缓冲、seek 完成和错误等记录,具体性能口径与设备条件见 移动端性能指标。
8 · 面试题卡片
本页题目与参考回答
8.1 MP4、H.264 和 HLS 分别是什么,为什么不能只看扩展名判断能否播放?
考察重点:容器、样本编码与分段传输。
相关内容:容器、编码与传输
参考回答
MP4 是容器,H.264 是视频编码,HLS 用播放列表组织媒体传输。 容器中的音视频样本还需要相应解码能力,并可能受到编码参数、分辨率、加密与平台支持限制,所以扩展名不足以说明整个播放链路都受支持。
8.2 视频播放的基本原理是什么?
考察重点:获取、解封装、解码、缓冲、时钟与输出。
参考回答
先取得媒体数据,解封装得到轨道和带时间信息的样本,再解码为音频采样或图像,并按播放时钟输出。 缓冲协调获取、解码与消费速度,各阶段可以重叠。数据到达、解码完成与显示时机不同,暂停、seek 和失败都要维护一致的状态与资源生命周期。
8.3 为什么音频和视频不能各自在解码完成后立即输出?
考察重点:呈现时间、解码顺序与共同时间基准。
相关内容:时间戳与音画同步
参考回答
完成解码不代表已经到呈现时间,两条链路的处理速度也不相同。 需要把样本的呈现时间映射到统一媒体时钟,再决定等待、输出或处理过期帧。编码依赖还可能使解码顺序与显示顺序不同。
8.4 播放器已经 ready,为什么界面仍不能显示为正在播放?
考察重点:播放意图、资源准备与实际推进。
相关内容:缓冲与播放状态
参考回答
准备好只表示具备播放条件,用户可能暂停,输出也可能受到抑制。 应结合平台的实际播放状态、用户意图和等待原因。缓冲恢复与主动暂停要区别处理,不能只因网络恢复就自动播放。
8.5 设计一个 C/S 播放器时,快进快退和时间跳转应该怎样处理?
题型:综合题
考察重点:媒体索引、解码依赖、请求身份与恢复。
参考回答
按目标媒体时间查找可用数据和可解码位置,再准备目标附近的样本并恢复输出时钟。 不直接用时间比例当文件字节偏移。连续 seek 可合并中间请求,但完成回调要核对当前媒体与请求;重连还要确认可用时间范围、授权和旧缓冲是否有效。
8.6 怎样让歌词与播放器的进度同步?
题型:综合题
考察重点:媒体时间轴、查找、跳转与监听生命周期。
相关内容:歌词与播放时间轴、时间戳与音画同步、跳转与重新连接、资源、生命周期与验证。
参考回答
把歌词整理为有序时间轴,按播放器实际媒体时间定位当前行。 定时器或时间观察只触发刷新,不靠次数累积进度。暂停、倍速和跳转都重新依据真实位置;切换媒体时更换时间轴并使旧回调失效,离开时移除观察 token。
9. 参考资料
- Android Media3:Supported Formats
- Android Media3:Player Events
- RFC 8216:HTTP Live Streaming
- FFmpeg 7.1:FFplay 文档源码
- Apple:CMTime
- Apple:AVPlayer
- Apple:Periodic Time Observer
- Apple:Removing a Time Observer
- Apple:Seek with Tolerances
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明