音视频面试题--播放器10个核心问题 文章目录Q1: 音视频同步如何处理的Q2: 播放器如何做到低延迟播放Q3: Seek 怎么做Q4如何做到变速播放Q5: 为什么播放器要多线程各线程干什么Q6: 架构总览Q7 : 为什么要用消息队列 状态机有什么用Q8: 解码后的数据格式Q9如何控制解码的Q10: 为什么需要两级队列Q11 时间戳转化问题Q12: 播放器跳转时能精确跳到指定位置吗Q1: 音视频同步如何处理的音视频不同线程、帧间隔不同、PTS 可能不准所以要同步。我们以音频为主时钟原因为1 感知敏感度 2 同步的复杂性 3 缓存策略以音频为基准视频帧可以采用丢帧或者重复帧来处理如果声音这样处理容易声音的跳跃或者重复 4 时钟源音频帧的事件源更稳定完整做法先用相邻 PTS 差得到正常帧间隔 delay再算 diff 视频时钟 − 主时钟。将diff的值和阈值进行比较阈值大约夹在 40 到 100 毫秒|diff| 很小delay 保持静态帧间隔diff 小于 −阈值说明视频慢了delay 收成 max(0, delaydiff)必要时丢帧diff 大于 阈值说明视频快了delay 加大比如变成两倍。再用 frame_timer delay 和系统时间比还没到就继续等画面停在上一帧到了或过了就显示下一帧并更新 frame_timer。为何同步 不同线程 / 不同间隔 / PTS 不准 ↓ 选音频为主时钟人对声音更敏感 ↓ ┌─ 10-1 ─────────────────────────────┐ │ diff 视频PTS − audclk │ │ 0 → 等否则 → 显示 │ └────────────────────────────────────┘ ↓完整版继续 delay₀ 相邻PTS差 或 1/fps diff 视频时钟 − 主时钟 阈值 ≈ clamp(delay₀, 40ms, 100ms) ↓ |diff| 小 → delay 不变 视频慢 → delay max(0, delaydiff)可丢帧 视频快 → delay 2*delay 或 delaydiff ↓ 系统时间 vs frame_timerdelay → 等 或 显示 frame_timer 上一帧真正上屏的系统时间 frame_timer delay 预计该结束显示、换下一帧的时刻 now frame_timer delay → 继续等画面保持上一帧 now ≥ frame_timer delay → 显示下一帧并更新 frame_timerQ2: 播放器如何做到低延迟播放直播 / 实时流rtsp时输入侧 nobuffer/low_delay、少探测PacketQueue 以字节数或时长作为浅水位一旦超出上限便停止读取或丢弃旧包FrameQueue 保持很小视频落后就丢帧直播积压可短时加速再按网络调水位深浅。解析nobuffer AVFormatContext 的 flags 里加 AVFMT_FLAG_NOBUFFER解复用demux时尽量少在内部多攒数据读到就尽快往外送low_delayAVCodecContext 的 flags 里加 AV_CODEC_FLAG_LOW_DELAY解码按低延迟模式工作减少为了重排/等待而额外拖住的时间浅水位直播常见大约 0.51 秒 量级点播可以深一点延迟小。代价是抗抖动变差网络一抖更容易卡字节水位queue.size或 音视字幕 size 之和时长水位队尾时间戳 − 队首时间戳或 duration × time_base换成秒视频丢帧diff 视频帧 PTS − 主时钟音频 diff 0 且 |diff| 超过阈值 → 视频太慢、画面过时了 取出 pictq 里当前待显示帧 若已经明显落后 frame_queue_next() // 丢掉这一帧不出屏 再 peek 下一帧继续比 直到某一帧不太落后再真正画到屏幕直播积压可短时加速积压是什么PacketQueue 里缓冲时长队尾−队首或 duration×time_base高于高水位说明囤得太多延迟变大cache_sec当前缓冲秒数ifcache_sechigh:// 比如 1srate1.05~1.2// 短暂加快视频帧间隔/ratePTS 按 rate 缩放 音频SonicSetSpeed(rate)Pitch1保调 elif cache_seclow:// 快见底了rate0.95~0.98// 略减速多攒一点else:rate1.0// 正常水位回到正常区间后rate 改回1.0阈值附近要防抖别来回切倍速网络调水位深浅 拉流 → PacketQueue │ ├─ 时长 max浅上限→ 停读或丢旧包 ├─ 时长 high → 短时加速rate1 └─ 时长 low → 略减速或再缓冲 ↓ 解码 → FrameQueue保持很小 ↓ video_refresh 帧比音频慢太多 → 丢帧理解拉流 │ ├─ nobuffer / low_delay ← 入口少囤 ▼ PacketQueue │ 计算缓冲时长队尾-队首 │ max → 停读 / 丢旧包 ← 浅水位管控 │ min → 起播或转圈再攒 ▼ 解码 → FrameQueue保持很小如约 3 帧 ▼ 显示 / 音频 │ 落后超时 → 丢帧 │ 水位 high → 短时加速追帧 │ 水位 low → 略减速 │ 中间 → 1.0x ▼ 阈值 min/max/high/low 按网络配置Q3: Seek 怎么做设 seek_req/seek_posread_thread 里 av_seek_frameflush 编解码器与队列serial旧序列的包/帧全部丢弃再重新填队列。UI 通过消息拿新进度。ijkmp_seek_to(msec)→ 设 seek_req / seek_pos / seek_flags→ read_thread 里 av_seek_frame优先关键帧→ avcodec_flush_buffers→ packet_queue_flush frame 丢弃→ serial // 旧包旧帧全部作废→ 重新读包、解码、短暂缓冲后恢复播放→ 消息通知 UI 更新进度Q4如何做到变速播放倍速用同一个 rate。视频把帧间隔和 PTS 都除以 rate画面加快或放慢。音频不用改采样率那会变调而是用 Sonic 做时域拉伸只设 speed 等于 ratepitch 固定 1.0保留音高周期。同步仍以音频为主时钟视频按缩放后的时间去追音画才不漂。text UI 选择倍速 rate (0.5 / 1.0 / 2.0) | v set_playback_rate(rate) | v -------------------------- | | v v 【视频路径】 【音频路径】 | | v v 帧间隔 delay0 rate 1.0 ? (PTS差 或 1/fps) / \ | 是 否 v | | 显示时长 delay0/rate | v PTS 也 / rate | Sonic 时域拉伸 | | speed rate v | pitch 1.0 画面变快/变慢 | (不变调) | | | | ------------ | | | v | SDL 播放 更新 audclk | | ------------------------- | v 【音频主时钟同步】 | v 视频用缩放后的时间追 audclk 超前则等 / 落后可丢帧 | v 音画对齐、不漂Q5: 为什么播放器要多线程各线程干什么解复用、解码、渲染、音频回调速度不同。read_thread 读包装队解码线程吃包产帧video_refresh 按时钟取帧SDL 回调要实时供数。用 PacketQueue/FrameQueue 条件变量解耦避免互相阻塞导致卡顿。解复用线程open_input → find_stream_info → component_open → 读包视频解码线程解码 → PTS 换秒 → 算 duration → queue_picture音频解码线程解码 → 入 sampq音频回调填缓冲、音量、更新 audclk视频刷新lastvp/vp、是否继续显示、是否丢帧、显示解复用线程read_thread负责打开媒体、找流、开解码器并循环读包送入 PacketQueue。步骤做什么avformat_alloc_context()创建AVFormatContextavformat_open_input()打开媒体读部分头信息流个数、时长、编码等探测协议网络文件会建连接avformat_find_stream_info()查找各路流音/视/字幕等确定解码器类型写入 context探测会读一些数据低延迟时常想减小探测stream_component_open()打开音视频解码器创建对应解码线程初始化输出相关参数循环av_read_frame读压缩包按类型放入audioq/videoq补充网络流格式实现可参考 demuxer如 FLV 的flvdec.c帮助理解open_input里协议与解析在干什么。avformat_alloc_context ↓ avformat_open_input ← 可配合 nobuffer、减小 probesize直播低延迟 ↓ avformat_find_stream_info ← 「少探测」主要影响这里的耗时 ↓ stream_component_open ← 拉起音/视频解码线程 ↓ 循环 av_read_frame → PacketQueue视频解码线程从videoq取包解码算出 PTS秒和显示时长放入pictq。步骤做什么取 stream time_base把帧的 PTS 换算成秒get_video_frame()取解码后的视频帧内部会走解码也可用于取帧率方便算每帧 duration解码send_packet 循环receive_frame有 B 帧时一包不一定一帧计算 duration本帧应显示多久相邻 PTS 差或1/fpsqueue_picture()写入视频 FrameQueuepictq释放帧数据用完后释放videoq 取包 ↓ 解码 get_video_frame / decode ↓ PTS × time_base → 秒 ↓ 算 duration显示时长 ↓ queue_picture → pictq ↓ pictq 满则等待 → 反向限制解码速度也利于控延迟/内存与变速的关系完整方案里显示时长、时钟会再/ rate解码线程先把 PTS/duration 算成秒放进队列。音频解码线程从audioq取包解码写入sampq。步骤做什么decoder_decode_frame读出一帧解码后的音频设置 time_base常用{1, sample_rate}便于把 PTS 换成秒取可写 Frame准备写入sampq算 duration如nb_samples / sample_rate设置并入队写入音频 FrameQueue释放帧数据清理audioq 取包 ↓ decoder_decode_frame ↓ PTS / duration按采样率 ↓ 写入 sampq音频回调SDL audio callback声卡要数据时回调从sampq取 PCM 填硬件缓冲并更新音频时钟。步骤做什么回调读数据循环填充直到本次要的字节够了缓冲空了调audio_decode_frame从帧队列取新数据中间可含重采样写出 stream音量最大可用memcpy否则SDL_MixAudioFormat做音量更新audclk用「当前音频时钟 − 缓冲里还没真正播出的部分」得到更准的同步点时钟修正讲义思路实际同步时间 ≈ 当前 audio_clock − (2 × 硬件在途数据 剩余未写完数据) / bytes_per_sec含义扣掉还在硬件/缓冲里、还没真正进耳朵的数据主时钟才更接近「现在听到哪」。与变速非 1.0x 时PCM 可先经 Sonicspeedrate,pitch1.0再送 SDL。SDL 要数据 ↓ audio_buf 不够 → audio_decode_frame← sampq ↓ 可选Sonic 变速不变调 ↓ memcpy / MixAudio → 硬件 ↓ 更新 audclk减去缓冲延迟视频刷新线程video_refresh按时钟决定这一帧是继续显示、还是丢帧、还是画新帧。步骤做什么取lastvp和待显示vp上一帧 / 当前帧算上一帧应显示多久判断是否还继续显示上一帧对应 delay / frame_timer估算当前帧显示时长判断是否丢帧落后太多则追赶调用显示如video_display/ Qt 回调画图取出 lastvp、vp ↓ 算上一帧显示时长 → 是否还等视频超前 ↓ 估当前帧 → 是否丢帧视频落后 ↓ 到点则 video_display / 回调显示Q6: 架构总览HomeWindow / DisplayWind / Playlist / CtrlBar │ 调用 APIstart/pause/stop/seek/rate │ 收消息message_loop ijkmp_get_msg ▼ 播放器控制层 —— IjkMediaPlayer 状态机 MP_STATE_* MessageQueue ▼ 媒体播放核心层 —— FFPlayer仿 ffplay read_thread / Decoder(音视频) / video_refresh PacketQueue / FrameQueue / Clock ▼ FFmpeg解复用/解码 SDL音频输出 Qt视频刷新回调UI 负责交互IjkMediaPlayer 负责状态和消息FFPlayer 负责音视频数据消息队列让它们跨线程协作又不用互相等待。Q7 : 为什么要用消息队列 状态机有什么用UI 和 demux/解码不在同一线程。事件prepared、缓冲、错误、完成异步上报控制请求可排队。线程安全用锁冲突指令连点暂停/播放可 msg_queue_remove 合并。防止非法跳转如 IDLE 直接 start。典型IDLE → INITIALIZED → ASYNC_PREPARING → PREPARED → STARTED ⇄ PAUSED → STOPPED/COMPLETED/ERROR。Q8: 解码后的数据格式音频解码后是 PCM维度含义常见取值采样精度 / 采样格式每个采样点用什么类型存s16、flt/float等排列格式多声道在内存里怎么排packed交错/planar平面s16AV_SAMPLE_FMT_S16floatAV_SAMPLE_FMT_FLT类型有符号 16 位整数32 位浮点典型取值范围约 −3276832767通常约 −1.01.0一个采样占内存2 字节4 字节谁常用SDL、很多声卡输出FFmpeg 部分解码/滤镜内部s16 是「用整数表示音量」float 是「用小数表示音量」。尺子不同不能拿错尺子去读内存。FFmpeg 里名字还带排列例如AV_SAMPLE_FMT_S16s16 packedAV_SAMPLE_FMT_S16Ps16 planarAV_SAMPLE_FMT_FLTfloat packedAV_SAMPLE_FMT_FLTPfloat planar所以真正完整格式 精度 排列。packed 和 planar多声道怎么排以双声道、4 个采样时刻为例L左R右。packed交错 / interleaved左右交替排在同一块连续内存内存L0 R0 L1 R1 L2 R2 L3 R3 ... |______| |______| 同一时刻 下一时刻一个 buffer 指针就够data[0]SDL 输出多半要这种planar平面每个声道各自一块平面平面0左L0 L1 L2 L3 ... 平面1右R0 R1 R2 R3 ...多个指针data[0]、data[1]…FFmpeg 解码后很常见尤其*P结尾的格式packed 是「左右左右」穿插planar 是「左左左…」和「右右右…」分开。顺序搞反耳朵听到的就全乱。播放器正确做法看清解码帧的formats16/float packed/planar看清输出要什么如 SDLAUDIO_S16SYS≈ s16 packed不一致就用SwrContextswr转成目标格式再送给声卡Q9如何控制解码的avcodec_send_packet 循环 avcodec_receive_frameB 帧会重排。解码后的 FrameQueue 必须浅否则内存爆掉包队列太满时读线程停读间接让解码线程拿不到包而阻塞。类型直觉依赖谁特点I 帧关键帧 / 完整画面不依赖别的帧能独立解码体积最大Seek、花屏恢复都从它开始P 帧向前预测依赖前面的I/P只存和前面的差异比 I 小B 帧双向预测依赖前、后参考帧更省码率编解码更绕要等后面的帧I 是完整图P 是「相对前面变了啥」B 是「相对前后两边变了啥」。怎么编码极简原始画面 1 2 3 4 ... 可能编成 I B B P ...编 I整幅压进去不参考别人。编 P参考前面已编码帧算运动/残差只编差异。编 B同时参考前、后已编码帧差异往往更小。B 要「看后面」码流顺序常和显示顺序不同显示顺序 I₁ B₂ B₃ P₄ 码流顺序 I₁ P₄ B₂ B₃ 先有 PB 才能编/解两个 I 帧之间叫GOPGOP 越大通常压缩越好但 Seek 精度越粗、花屏恢复越慢。怎么解码极简帧解码要点I直接解出完整画面可立刻显示P必须先有前面的参考帧再叠差异B前、后参考都齐了才能解因此 FFmpeg 里常见avcodec_send_packet 送一包 while receive_frame 有帧就取 返回 EAGAIN → 再送包B 帧重排时很常见视频压缩用三种帧I 不依赖别人能单独解出来P 参考前面的帧只编码差异B 参考前后压缩更好但编码和码流顺序会打乱解码时可能先收到后面的 P再解中间的 B。所以播放器解码要用 send_packet 再循环 receive_frame直到 EAGAIN。拖进度条时必须先 seek 到关键帧缺参考就会花屏要精准就落到前一个 I 再解码丢弃到目标 PTS。Q10: 为什么需要两级队列解耦读取、解码、渲染三个环节速度不同队列作为缓冲异步处理IO读取可能阻塞解码是CPU密集渲染是定时消费音视频同步FrameQueue中的帧带有pts便于同步计算和丢帧决策Q11 时间戳转化问题播放侧重点不是改写 PTS而是读出流里的 PTS换算成统一单位后拿来同步和显示。基准用谁无论音频还是视频解码前、解码后都以该路AVStream-time_base为准。同步时要统一单位音视频各自的time_base可能不同做 A/V 同步前要把两边的PTS 都换成同一种单位。常用单位秒或毫秒都可以本项目习惯换成秒这一帧从片子开头算起该出现的时刻 pts_秒 pts * av_q2d(stream-time_base) pts * time_base.num / time_base.den音频也常把 time_base 看成与采样率相关如{1, sample_rate}再算成秒。time_base ≈ {1, sample_rate} │ ├─ pts_秒 pts × (1/sample_rate) → 这一帧「几点该出」 │ └─ duration nb_samples / sample_rate → 这一帧「持续多久」 两者单位都是秒才能和视频 pts_秒、主时钟 audclk 一起比。pts * time_base 把时间戳变成秒nb_samples / sample_rate 把帧长变成秒。Q12: 播放器跳转时能精确跳到指定位置吗只调 FFmpeg seek API精度一般只能到关键帧I 帧附近要更准需要「seek 到前一个 I 帧 解码丢弃直到目标 PTSGOP两个 I 帧之间的间隔一帧 I 后面的 P/B。Seek 之后解码要从 I 帧起步否则缺参考会花屏帧率 25fps GOP 间隔 50 帧 → 两个 I 帧之间的时间 ≈ 50 / 25 2 秒 时间轴上 I 帧大约在 0ms -------- 2000ms -------- 4000ms -------- ... I I I 若进度条拖到 3010ms只做「向后找关键帧」的 seek往往会落到 2000ms 那个 I 帧而不是 3010ms。流程用户拖到 target_ms如 3010 │ v av_seek / seek_file BACKWARD │ v 落到前一个 I 帧如 2000ms │ v flush 队列 flush 解码器 serial │ v 继续 read decode │ ├─ frame.pts target → 丢弃不显示 └─ frame.pts ≈ target → 开始显示如 3000ms