前言
你每天都在看视频,但有没有想过:
- MP4和AVI到底有什么区别?
- 为什么有的视频文件很小但很清晰?
- 播放器是怎么把文件变成画面的?
- 为什么有时候"有画面没声音"?
今天从二进制层面,彻底搞懂视频播放的原理。
一、视频的本质
┌─────────────────────────────────────────────────────────────────────────────┐
│ 视频 = 图像序列 + 音频 + 元数据 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【原始视频数据有多大?】 │
│ ───────────────────────────────────────── │
│ │
│ 1080P视频,30fps,RGB格式: │
│ │
│ 每帧大小 = 1920 × 1080 × 3 bytes = 6,220,800 bytes ≈ 6MB │
│ 每秒大小 = 6MB × 30 = 180MB │
│ 一分钟 = 180MB × 60 = 10.8GB │
│ 一部电影(2小时) = 10.8GB × 120 = 1.3TB !!! │
│ │
│ 所以必须压缩! │
│ │
│ 【压缩后】 │
│ ───────────────────────────────────────── │
│ │
│ H.264编码后: │
│ 1080P/30fps 通常只需要 5-10 Mbps │
│ 一部电影 ≈ 2-5 GB │
│ │
│ 压缩比: 约 200-500 倍! │
│ │
│ 【视频文件结构】 │
│ ───────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 视频文件 (如 movie.mp4) │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 容器头部 │ 文件格式、时长、分辨率等元数据 │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 视频轨道 │ 压缩后的视频帧数据 (H.264/H.265/VP9...) │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 音频轨道 │ 压缩后的音频数据 (AAC/MP3/AC3...) │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 字幕轨道 │ (可选) SRT/ASS格式字幕 │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 索引信息 │ 帧位置索引,用于快速定位 │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
二、容器格式 vs 编码格式
┌─────────────────────────────────────────────────────────────────────────────┐
│ 这是两个不同的概念! │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 很多人搞混了"MP4"和"H.264" │
│ │
│ 【容器格式】 = 盒子,决定怎么打包 │
│ ───────────────────────────────────────── │
│ │
│ MP4, AVI, MKV, FLV, MOV, WebM... │
│ │
│ 作用: │
│ - 把视频、音频、字幕打包在一起 │
│ - 存储元数据(时长、分辨率) │
│ - 提供索引,支持快进快退 │
│ │
│ 【编码格式】 = 压缩算法,决定怎么压缩 │
│ ───────────────────────────────────────── │
│ │
│ 视频编码: H.264, H.265(HEVC), VP9, AV1... │
│ 音频编码: AAC, MP3, AC3, Opus... │
│ │
│ 作用: │
│ - 压缩原始数据,减小体积 │
│ - 平衡画质和大小 │
│ │
│ 【组合关系】 │
│ ───────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 容器(盒子) 可以装的编码(内容) │ │
│ │ ─────────────────────────────────────────────── │ │
│ │ MP4 (.mp4) → H.264, H.265, AAC, MP3 │ │
│ │ MKV (.mkv) → 几乎所有编码都支持 │ │
│ │ AVI (.avi) → 老编码为主,H.264也行 │ │
│ │ WebM (.webm) → VP8, VP9, Opus, Vorbis │ │
│ │ FLV (.flv) → H.264, AAC (Flash时代) │ │
│ │ MOV (.mov) → H.264, ProRes, AAC (苹果) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 类比: │
│ 容器 = 快递盒子 (纸箱/木箱/泡沫箱) │
│ 编码 = 盒子里的东西怎么包装 (真空包装/充气包装/冷冻) │
│ │
│ 同样的"手机" (H.264视频),可以装在不同的"盒子"里 (MP4/MKV/AVI) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
三、常用视频格式详解
1. MP4 (最通用)
┌─────────────────────────────────────────────────────────────────────────────┐
│ MP4 格式详解 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 全称: MPEG-4 Part 14 │
│ 扩展名: .mp4, .m4v, .m4a │
│ 标准: ISO/IEC 14496-14 │
│ │
│ 【特点】 │
│ ───────────────────────────────────────── │
│ ✅ 兼容性最好,几乎所有设备都支持 │
│ ✅ 支持流媒体播放 │
│ ✅ 文件头可以放在开头(支持边下边播) │
│ ❌ 不太适合编辑(不是每帧都是关键帧) │
│ │
│ 【文件结构: Box/Atom】 │
│ ───────────────────────────────────────── │
│ │
│ MP4由一系列"Box"组成,每个Box有类型和大小 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ftyp │ 文件类型标识 ("isom", "mp42"等) │ │
│ ├────────┼────────────────────────────────────────────────────────────┤ │
│ │ moov │ 元数据容器 (时长、轨道信息、索引等) │ │
│ │ ├─ mvhd │ 电影头 (时长、创建时间) │ │
│ │ ├─ trak │ 轨道 (视频轨道) │ │
│ │ │ ├─ tkhd │ 轨道头 │ │
│ │ │ └─ mdia │ 媒体信息 │ │
│ │ │ └─ stbl │ 采样表 (帧索引) │ │
│ │ └─ trak │ 轨道 (音频轨道) │ │
│ ├────────┼────────────────────────────────────────────────────────────┤ │
│ │ mdat │ 实际的音视频数据 │ │
│ └────────┴────────────────────────────────────────────────────────────┘ │
│ │
│ 【moov位置的重要性】 │
│ ───────────────────────────────────────── │
│ │
│ 普通MP4: [ftyp][mdat...很长...][moov] │
│ → 必须下载完整文件才能播放 │
│ │
│ 流媒体MP4: [ftyp][moov][mdat...] │
│ → 下载头部就能开始播放 (使用 ffmpeg -movflags faststart) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2. AVI (最古老)
┌─────────────────────────────────────────────────────────────────────────────┐
│ AVI 格式详解 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 全称: Audio Video Interleave │
│ 开发者: 微软 (1992年) │
│ 扩展名: .avi │
│ │
│ 【特点】 │
│ ───────────────────────────────────────── │
│ ✅ 历史悠久,Windows原生支持 │
│ ✅ 结构简单 │
│ ❌ 不支持流媒体 │
│ ❌ 不支持字幕轨道 │
│ ❌ 文件通常较大 │
│ ❌ 对新编码支持有限 │
│ │
│ 【文件结构: RIFF格式】 │
│ ───────────────────────────────────────── │
│ │
│ 基于RIFF (Resource Interchange File Format) │
│ │
│ RIFF 'AVI ' │
│ ├─ LIST 'hdrl' (头部列表) │
│ │ ├─ avih (主头部: 帧率、宽高等) │
│ │ ├─ LIST 'strl' (流列表-视频) │
│ │ │ ├─ strh (流头部) │
│ │ │ └─ strf (流格式: 编码信息) │
│ │ └─ LIST 'strl' (流列表-音频) │
│ │ ├─ strh │
│ │ └─ strf │
│ ├─ LIST 'movi' (实际数据) │
│ │ ├─ 00dc (视频帧 compressed) │
│ │ ├─ 01wb (音频块 wave bytes) │
│ │ ├─ 00dc │
│ │ ├─ 01wb │
│ │ └─ ... │
│ └─ idx1 (索引) │
│ │
│ 音视频数据是交错存储的 (Interleave) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3. MKV (最灵活)
┌─────────────────────────────────────────────────────────────────────────────┐
│ MKV 格式详解 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 全称: Matroska Video │
│ 扩展名: .mkv, .mka (纯音频), .mks (字幕) │
│ 特点: 开源、免费 │
│ │
│ 【特点】 │
│ ───────────────────────────────────────── │
│ ✅ 支持几乎所有编码格式 │
│ ✅ 支持多音轨(多语言) │
│ ✅ 支持多字幕轨道 │
│ ✅ 支持章节标记 │
│ ✅ 支持附件(字体等) │
│ ❌ 部分设备不支持 │
│ ❌ 文件稍大(头部开销) │
│ │
│ 【典型用途】 │
│ ───────────────────────────────────────── │
│ - 高清电影收藏(多音轨多字幕) │
│ - 动漫(内嵌字幕字体) │
│ - 蓝光翻录 │
│ │
│ 【文件结构: EBML格式】 │
│ ───────────────────────────────────────── │
│ │
│ EBML Header │
│ └─ Segment │
│ ├─ SeekHead (索引位置表) │
│ ├─ Info (时长、标题等) │
│ ├─ Tracks (轨道信息) │
│ │ ├─ TrackEntry (视频轨道: H.264) │
│ │ ├─ TrackEntry (音频轨道1: AAC 中文) │
│ │ ├─ TrackEntry (音频轨道2: AAC 英文) │
│ │ ├─ TrackEntry (字幕轨道1: 中文) │
│ │ └─ TrackEntry (字幕轨道2: 英文) │
│ ├─ Chapters (章节) │
│ ├─ Attachments (附件: 字体) │
│ ├─ Cluster (数据簇) │
│ │ ├─ SimpleBlock (视频/音频数据) │
│ │ └─ ... │
│ ├─ Cluster │
│ └─ ... │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4. 其他格式对比
┌─────────────────────────────────────────────────────────────────────────────┐
│ 常用视频格式对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┬──────────┬──────────┬──────────────┬────────────────────────┐│
│ │ 格式 │ 流媒体 │ 多轨道 │ 兼容性 │ 典型用途 ││
│ ├─────────┼──────────┼──────────┼──────────────┼────────────────────────┤│
│ │ MP4 │ ✅ │ 有限 │ ⭐⭐⭐⭐⭐ │ 通用、网络视频 ││
│ │ MKV │ ✅ │ ✅强 │ ⭐⭐⭐ │ 高清收藏、多语言 ││
│ │ AVI │ ❌ │ ❌ │ ⭐⭐⭐⭐ │ 老视频、Windows ││
│ │ MOV │ ✅ │ ✅ │ ⭐⭐⭐ │ 苹果设备、专业剪辑 ││
│ │ WebM │ ✅ │ 有限 │ ⭐⭐⭐ │ 网页视频 ││
│ │ FLV │ ✅ │ ❌ │ ⭐⭐ │ 直播流(已过时) ││
│ │ TS │ ✅ │ ✅ │ ⭐⭐⭐ │ 数字电视、HLS ││
│ │ RMVB │ ❌ │ ❌ │ ⭐ │ 已淘汰 ││
│ └─────────┴──────────┴──────────┴──────────────┴────────────────────────┘│
│ │
│ 【编码格式对比】 │
│ ───────────────────────────────────────── │
│ │
│ ┌─────────┬──────────────┬──────────────┬────────────────────────────────┐│
│ │ 编码 │ 压缩效率 │ 兼容性 │ 说明 ││
│ ├─────────┼──────────────┼──────────────┼────────────────────────────────┤│
│ │ H.264 │ ⭐⭐⭐ │ ⭐⭐⭐⭐⭐ │ 当前最通用,所有设备支持 ││
│ │ H.265 │ ⭐⭐⭐⭐ │ ⭐⭐⭐ │ 比H.264小50%,4K首选 ││
│ │ VP9 │ ⭐⭐⭐⭐ │ ⭐⭐⭐ │ Google开发,YouTube使用 ││
│ │ AV1 │ ⭐⭐⭐⭐⭐ │ ⭐⭐ │ 最新最强,编码慢,硬解少 ││
│ │ MPEG-2 │ ⭐⭐ │ ⭐⭐⭐⭐ │ DVD时代,已过时 ││
│ └─────────┴──────────────┴──────────────┴────────────────────────────────┘│
│ │
│ 【音频编码对比】 │
│ ───────────────────────────────────────── │
│ │
│ AAC: MP4标配,效率高,专利 │
│ MP3: 最通用,效率一般,专利已过期 │
│ Opus: 最新最强,开源,WebM/WebRTC首选 │
│ AC3: 影院标准,多声道 │
│ FLAC: 无损压缩,音乐收藏 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
四、视频编码原理
┌─────────────────────────────────────────────────────────────────────────────┐
│ 视频压缩的核心思想 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【为什么能压缩这么多?】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 空间冗余: 一帧图像内,相邻像素往往很相似 │
│ 2. 时间冗余: 相邻帧之间,大部分内容没变化 │
│ 3. 视觉冗余: 人眼对某些细节不敏感 │
│ │
│ 【帧类型: I帧、P帧、B帧】 │
│ ───────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 时间轴: I P B B P B B I P B ... │ │
│ │ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ │ │
│ │ 帧号: 0 1 2 3 4 5 6 7 8 9 │ │
│ │ │ │
│ │ 大小: 大 中 小 小 中 小 小 大 中 小 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ I帧 (Intra-frame) - 关键帧 │
│ ──────────────────────────────────────── │
│ - 完整的一帧图像,可独立解码 │
│ - 类似JPEG,只做帧内压缩 │
│ - 体积最大 │
│ - 拖动进度条时定位到I帧 │
│ │
│ P帧 (Predicted frame) - 预测帧 │
│ ──────────────────────────────────────── │
│ - 只存储与前一帧的差异 │
│ - 需要参考前面的帧才能解码 │
│ - 体积中等 │
│ │
│ B帧 (Bi-directional frame) - 双向预测帧 │
│ ──────────────────────────────────────── │
│ - 参考前后两帧来预测 │
│ - 压缩效率最高 │
│ - 体积最小 │
│ - 解码顺序和显示顺序不同! │
│ │
│ 【GOP (Group of Pictures)】 │
│ ───────────────────────────────────────── │
│ │
│ 一组连续的帧,从I帧开始到下一个I帧之前 │
│ │
│ 典型GOP: I B B P B B P B B P B B I ... │
│ └────────── GOP ──────────┘ │
│ │
│ GOP越长: 压缩率越高,但拖动定位越慢 │
│ GOP越短: 方便编辑和定位,但文件更大 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
帧间预测示意
┌─────────────────────────────────────────────────────────────────────────────┐
│ P帧是如何压缩的? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 假设: 一个球从左向右移动 │
│ │
│ 第1帧 (I帧): 第2帧 (P帧): │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ │ │ │ │
│ │ ● │ │ ● │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ P帧不存储完整图像,只存储: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 运动向量: (x+50, y+0) ← 球向右移动了50像素 │ │
│ │ 残差数据: [微小差异] ← 只存储预测不准的地方 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 解码时: │
│ 第2帧 = 第1帧 + 运动补偿 + 残差 │
│ │
│ 如果画面大部分不变,P帧可能只有几KB! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
五、播放器工作流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 视频播放器处理流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 视频文件 │ │
│ │ (movie.mp4) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 解封装 │ Demuxer │ │
│ │ │ (拆包裹) │ 把容器里的音视频轨道分离出来 │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ┌─────┴─────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │视频包│ │音频包│ Packet (压缩数据) │ │
│ │ │H.264│ │ AAC │ │ │
│ │ └──┬───┘ └──┬───┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │视频 │ │音频 │ │ │
│ │ │解码器 │ │解码器 │ Decoder (解压缩) │ │
│ │ │ │ │ │ │ │
│ │ └──┬───┘ └──┬───┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │YUV帧 │ │PCM帧 │ Frame (原始数据) │ │
│ │ │ │ │ │ │ │
│ │ └──┬───┘ └──┬───┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 音视频同步 │ 根据时间戳(PTS)对齐 │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ┌─────┴─────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │视频 │ │音频 │ │ │
│ │ │渲染 │ │播放 │ Renderer (显示/播放) │ │
│ │ │ │ │ │ │ │
│ │ └──────┘ └──────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ 屏幕 扬声器 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【关键概念】 │
│ ───────────────────────────────────────── │
│ │
│ PTS (Presentation Time Stamp): 显示时间戳 │
│ DTS (Decoding Time Stamp): 解码时间戳 │
│ │
│ 因为B帧的存在,解码顺序 ≠ 显示顺序 │
│ │
│ 显示顺序: I B B P B B P │
│ 解码顺序: I P B B P B B │
│ (要先解码P帧,才能解码前面的B帧) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
六、用代码理解(FFmpeg)
获取视频信息
c
/**
* 用FFmpeg读取视频文件信息
*/
#include <stdio.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
void print_video_info(const char* filename) {
AVFormatContext* fmt_ctx = NULL;
// 打开文件
if (avformat_open_input(&fmt_ctx, filename, NULL, NULL) < 0) {
printf("无法打开文件: %s\n", filename);
return;
}
// 获取流信息
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
printf("无法获取流信息\n");
avformat_close_input(&fmt_ctx);
return;
}
printf("========== 视频文件信息 ==========\n\n");
// 容器格式
printf("【容器格式】\n");
printf(" 格式名称: %s\n", fmt_ctx->iformat->name);
printf(" 格式全称: %s\n", fmt_ctx->iformat->long_name);
printf(" 时长: %.2f 秒\n", fmt_ctx->duration / (double)AV_TIME_BASE);
printf(" 比特率: %ld kbps\n", fmt_ctx->bit_rate / 1000);
printf(" 流数量: %d\n\n", fmt_ctx->nb_streams);
// 遍历每个流
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
AVCodecParameters* codecpar = stream->codecpar;
// 获取编解码器信息
const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id);
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
printf("【视频流 #%d】\n", i);
printf(" 编码格式: %s\n", codec ? codec->name : "未知");
printf(" 分辨率: %dx%d\n", codecpar->width, codecpar->height);
printf(" 帧率: %.2f fps\n", av_q2d(stream->r_frame_rate));
printf(" 比特率: %ld kbps\n", codecpar->bit_rate / 1000);
printf(" 像素格式: %s\n",
av_get_pix_fmt_name(codecpar->format));
printf("\n");
}
else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
printf("【音频流 #%d】\n", i);
printf(" 编码格式: %s\n", codec ? codec->name : "未知");
printf(" 采样率: %d Hz\n", codecpar->sample_rate);
printf(" 声道数: %d\n", codecpar->ch_layout.nb_channels);
printf(" 比特率: %ld kbps\n", codecpar->bit_rate / 1000);
printf("\n");
}
else if (codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
printf("【字幕流 #%d】\n", i);
printf(" 编码格式: %s\n", codec ? codec->name : "未知");
printf("\n");
}
}
avformat_close_input(&fmt_ctx);
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("用法: %s <视频文件>\n", argv[0]);
return 1;
}
print_video_info(argv[1]);
return 0;
}
/* 编译: gcc -o videoinfo videoinfo.c -lavformat -lavcodec -lavutil
输出示例:
========== 视频文件信息 ==========
【容器格式】
格式名称: mov,mp4,m4a,3gp,3g2,mj2
格式全称: QuickTime / MOV
时长: 120.50 秒
比特率: 5234 kbps
流数量: 2
【视频流 #0】
编码格式: h264
分辨率: 1920x1080
帧率: 23.98 fps
比特率: 5000 kbps
像素格式: yuv420p
【音频流 #1】
编码格式: aac
采样率: 48000 Hz
声道数: 2
比特率: 192 kbps
*/
简易播放器框架
c
/**
* 简易视频播放器框架 (FFmpeg + SDL2)
*/
#include <stdio.h>
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
typedef struct {
// FFmpeg
AVFormatContext* fmt_ctx;
AVCodecContext* video_codec_ctx;
AVCodecContext* audio_codec_ctx;
int video_stream_idx;
int audio_stream_idx;
struct SwsContext* sws_ctx;
struct SwrContext* swr_ctx;
// SDL
SDL_Window* window;
SDL_Renderer* renderer;
SDL_Texture* texture;
SDL_AudioDeviceID audio_dev;
// 状态
bool quit;
bool paused;
} Player;
/**
* 初始化播放器
*/
int player_init(Player* player, const char* filename) {
memset(player, 0, sizeof(Player));
player->video_stream_idx = -1;
player->audio_stream_idx = -1;
// 打开视频文件
if (avformat_open_input(&player->fmt_ctx, filename, NULL, NULL) < 0) {
printf("无法打开文件\n");
return -1;
}
avformat_find_stream_info(player->fmt_ctx, NULL);
// 找到视频和音频流
for (unsigned int i = 0; i < player->fmt_ctx->nb_streams; i++) {
AVCodecParameters* codecpar = player->fmt_ctx->streams[i]->codecpar;
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
player->video_stream_idx < 0) {
player->video_stream_idx = i;
}
else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
player->audio_stream_idx < 0) {
player->audio_stream_idx = i;
}
}
if (player->video_stream_idx < 0) {
printf("没有找到视频流\n");
return -1;
}
// 初始化视频解码器
AVStream* video_stream = player->fmt_ctx->streams[player->video_stream_idx];
const AVCodec* video_codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
player->video_codec_ctx = avcodec_alloc_context3(video_codec);
avcodec_parameters_to_context(player->video_codec_ctx, video_stream->codecpar);
avcodec_open2(player->video_codec_ctx, video_codec, NULL);
int width = player->video_codec_ctx->width;
int height = player->video_codec_ctx->height;
// 初始化音频解码器 (如果有)
if (player->audio_stream_idx >= 0) {
AVStream* audio_stream = player->fmt_ctx->streams[player->audio_stream_idx];
const AVCodec* audio_codec = avcodec_find_decoder(audio_stream->codecpar->codec_id);
player->audio_codec_ctx = avcodec_alloc_context3(audio_codec);
avcodec_parameters_to_context(player->audio_codec_ctx, audio_stream->codecpar);
avcodec_open2(player->audio_codec_ctx, audio_codec, NULL);
}
// 初始化图像转换器 (YUV -> RGB)
player->sws_ctx = sws_getContext(
width, height, player->video_codec_ctx->pix_fmt,
width, height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, NULL, NULL, NULL
);
// 初始化SDL
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
player->window = SDL_CreateWindow(
"简易播放器",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width, height, SDL_WINDOW_SHOWN
);
player->renderer = SDL_CreateRenderer(player->window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
player->texture = SDL_CreateTexture(
player->renderer,
SDL_PIXELFORMAT_RGB24,
SDL_TEXTUREACCESS_STREAMING,
width, height
);
printf("播放器初始化完成: %dx%d\n", width, height);
return 0;
}
/**
* 播放主循环
*/
void player_play(Player* player) {
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
AVFrame* rgb_frame = av_frame_alloc();
int width = player->video_codec_ctx->width;
int height = player->video_codec_ctx->height;
// 分配RGB帧缓冲
int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGB24, width, height, 1);
uint8_t* buffer = (uint8_t*)av_malloc(buffer_size);
av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, buffer,
AV_PIX_FMT_RGB24, width, height, 1);
// 帧率控制
AVRational time_base = player->fmt_ctx->streams[player->video_stream_idx]->time_base;
double fps = av_q2d(player->fmt_ctx->streams[player->video_stream_idx]->r_frame_rate);
uint32_t frame_delay = (uint32_t)(1000.0 / fps);
printf("开始播放, 帧率: %.2f fps\n", fps);
while (!player->quit) {
// 处理SDL事件
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
player->quit = true;
}
else if (event.type == SDL_KEYDOWN) {
switch (event.key.keysym.sym) {
case SDLK_ESCAPE:
case SDLK_q:
player->quit = true;
break;
case SDLK_SPACE:
player->paused = !player->paused;
printf(player->paused ? "暂停\n" : "继续\n");
break;
}
}
}
if (player->paused) {
SDL_Delay(10);
continue;
}
// 读取数据包
if (av_read_frame(player->fmt_ctx, packet) < 0) {
// 播放完毕或出错
printf("播放完毕\n");
break;
}
// 处理视频包
if (packet->stream_index == player->video_stream_idx) {
// 发送到解码器
if (avcodec_send_packet(player->video_codec_ctx, packet) == 0) {
// 获取解码后的帧
while (avcodec_receive_frame(player->video_codec_ctx, frame) == 0) {
// YUV -> RGB
sws_scale(player->sws_ctx,
(const uint8_t* const*)frame->data, frame->linesize,
0, height,
rgb_frame->data, rgb_frame->linesize);
// 更新纹理
SDL_UpdateTexture(player->texture, NULL,
rgb_frame->data[0], rgb_frame->linesize[0]);
// 渲染
SDL_RenderClear(player->renderer);
SDL_RenderCopy(player->renderer, player->texture, NULL, NULL);
SDL_RenderPresent(player->renderer);
// 简单的帧率控制
SDL_Delay(frame_delay);
}
}
}
av_packet_unref(packet);
}
av_free(buffer);
av_frame_free(&rgb_frame);
av_frame_free(&frame);
av_packet_free(&packet);
}
/**
* 清理资源
*/
void player_cleanup(Player* player) {
if (player->sws_ctx) sws_freeContext(player->sws_ctx);
if (player->video_codec_ctx) avcodec_free_context(&player->video_codec_ctx);
if (player->audio_codec_ctx) avcodec_free_context(&player->audio_codec_ctx);
if (player->fmt_ctx) avformat_close_input(&player->fmt_ctx);
if (player->texture) SDL_DestroyTexture(player->texture);
if (player->renderer) SDL_DestroyRenderer(player->renderer);
if (player->window) SDL_DestroyWindow(player->window);
SDL_Quit();
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("用法: %s <视频文件>\n", argv[0]);
return 1;
}
Player player;
if (player_init(&player, argv[1]) < 0) {
return 1;
}
player_play(&player);
player_cleanup(&player);
return 0;
}
/*
编译:
gcc -o player player.c \
-lavformat -lavcodec -lavutil -lswscale -lswresample \
-lSDL2 -lm
运行:
./player video.mp4
*/
七、为什么"有画面没声音"?
┌─────────────────────────────────────────────────────────────────────────────┐
│ 播放问题排查指南 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【问题1: 有画面没声音】 │
│ ───────────────────────────────────────── │
│ │
│ 可能原因: │
│ 1. 音频编码不支持 (如AC3需要解码器) │
│ 2. 音频轨道损坏 │
│ 3. 播放器没有正确的音频解码器 │
│ │
│ 解决: │
│ - 安装完整解码器包 (K-Lite, LAV Filters) │
│ - 换用VLC/PotPlayer等全能播放器 │
│ - 转码: ffmpeg -i input.mp4 -c:v copy -c:a aac output.mp4 │
│ │
│ 【问题2: 有声音没画面】 │
│ ───────────────────────────────────────── │
│ │
│ 可能原因: │
│ 1. 视频编码不支持 (如H.265/HEVC) │
│ 2. 硬件解码失败 │
│ 3. 视频轨道损坏 │
│ │
│ 解决: │
│ - 安装HEVC扩展 (Windows商店) │
│ - 关闭硬件加速试试 │
│ - 转码: ffmpeg -i input.mp4 -c:v libx264 -c:a copy output.mp4 │
│ │
│ 【问题3: 音画不同步】 │
│ ───────────────────────────────────────── │
│ │
│ 可能原因: │
│ 1. 视频时间戳(PTS)有问题 │
│ 2. 播放器同步算法问题 │
│ 3. 硬件解码延迟 │
│ │
│ 解决: │
│ - 播放器调整音频延迟 │
│ - 重新封装: ffmpeg -i input.mp4 -c copy output.mp4 │
│ │
│ 【问题4: 无法拖动进度条】 │
│ ───────────────────────────────────────── │
│ │
│ 可能原因: │
│ 1. 索引信息损坏/缺失 │
│ 2. 直播流录制的文件 │
│ │
│ 解决: │
│ - 重建索引: ffmpeg -i input.mp4 -c copy -movflags faststart output.mp4 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
八、用FFmpeg探索视频文件
bash
#!/bin/bash
# 常用FFmpeg命令
# 1. 查看视频信息
ffprobe -show_format -show_streams video.mp4
# 2. 只看关键信息
ffprobe -v quiet -show_entries format=duration,bit_rate -show_entries stream=codec_name,width,height video.mp4
# 3. 查看帧类型分布
ffprobe -show_frames -select_streams v -show_entries frame=pict_type video.mp4 | grep pict_type | sort | uniq -c
# 4. 提取视频的I帧
ffmpeg -i video.mp4 -vf "select=eq(pict_type\,I)" -vsync vfr keyframes_%03d.jpg
# 5. 查看GOP结构
ffprobe -show_frames -select_streams v video.mp4 2>/dev/null | grep pict_type | head -30
# 6. 转换容器格式 (不重新编码)
ffmpeg -i input.avi -c copy output.mp4
# 7. 转换编码格式
ffmpeg -i input.mp4 -c:v libx265 -c:a aac output.mp4
# 8. 提取音频
ffmpeg -i video.mp4 -vn -c:a copy audio.aac
# 9. 提取视频 (去掉音频)
ffmpeg -i video.mp4 -an -c:v copy video_only.mp4
# 10. 优化MP4为流媒体 (moov前置)
ffmpeg -i input.mp4 -c copy -movflags faststart output.mp4
# 11. 查看容器支持的编码
ffmpeg -formats
# 12. 查看可用的编解码器
ffmpeg -codecs
分析视频结构的代码
python
"""
分析视频文件结构 (使用Python + ffprobe)
"""
import subprocess
import json
def analyze_video(filename):
"""分析视频文件"""
# 使用ffprobe获取JSON格式信息
cmd = [
'ffprobe',
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
filename
]
result = subprocess.run(cmd, capture_output=True, text=True)
data = json.loads(result.stdout)
print("=" * 60)
print(f"文件: {filename}")
print("=" * 60)
# 容器格式
fmt = data['format']
print(f"\n【容器格式】")
print(f" 格式: {fmt.get('format_name', 'N/A')}")
print(f" 时长: {float(fmt.get('duration', 0)):.2f} 秒")
print(f" 大小: {int(fmt.get('size', 0)) / 1024 / 1024:.2f} MB")
print(f" 比特率: {int(fmt.get('bit_rate', 0)) / 1000:.0f} kbps")
# 流信息
for stream in data['streams']:
codec_type = stream.get('codec_type', 'unknown')
if codec_type == 'video':
print(f"\n【视频流】")
print(f" 编码: {stream.get('codec_name', 'N/A')}")
print(f" 分辨率: {stream.get('width')}x{stream.get('height')}")
print(f" 帧率: {eval(stream.get('r_frame_rate', '0/1')):.2f} fps")
print(f" 比特率: {int(stream.get('bit_rate', 0)) / 1000:.0f} kbps")
print(f" 像素格式: {stream.get('pix_fmt', 'N/A')}")
elif codec_type == 'audio':
print(f"\n【音频流】")
print(f" 编码: {stream.get('codec_name', 'N/A')}")
print(f" 采样率: {stream.get('sample_rate', 'N/A')} Hz")
print(f" 声道: {stream.get('channels', 'N/A')}")
print(f" 比特率: {int(stream.get('bit_rate', 0)) / 1000:.0f} kbps")
elif codec_type == 'subtitle':
print(f"\n【字幕流】")
print(f" 编码: {stream.get('codec_name', 'N/A')}")
def analyze_frames(filename, num_frames=50):
"""分析帧类型分布"""
cmd = [
'ffprobe',
'-v', 'quiet',
'-select_streams', 'v',
'-show_frames',
'-show_entries', 'frame=pict_type,key_frame,pkt_size',
'-print_format', 'json',
filename
]
result = subprocess.run(cmd, capture_output=True, text=True)
data = json.loads(result.stdout)
frames = data.get('frames', [])[:num_frames]
print(f"\n【前{len(frames)}帧分析】")
print("-" * 50)
i_count = p_count = b_count = 0
i_size = p_size = b_size = 0
frame_types = []
for frame in frames:
pict_type = frame.get('pict_type', '?')
size = int(frame.get('pkt_size', 0))
frame_types.append(pict_type)
if pict_type == 'I':
i_count += 1
i_size += size
elif pict_type == 'P':
p_count += 1
p_size += size
elif pict_type == 'B':
b_count += 1
b_size += size
print(f"帧序列: {''.join(frame_types)}")
print()
print(f"I帧: {i_count}个, 平均大小: {i_size/max(i_count,1)/1024:.1f} KB")
print(f"P帧: {p_count}个, 平均大小: {p_size/max(p_count,1)/1024:.1f} KB")
print(f"B帧: {b_count}个, 平均大小: {b_size/max(b_count,1)/1024:.1f} KB")
# GOP长度
gop_lengths = []
current_gop = 0
for ft in frame_types:
current_gop += 1
if ft == 'I' and current_gop > 1:
gop_lengths.append(current_gop - 1)
current_gop = 1
if gop_lengths:
print(f"\nGOP长度: {gop_lengths} (平均: {sum(gop_lengths)/len(gop_lengths):.1f})")
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print(f"用法: python {sys.argv[0]} <视频文件>")
sys.exit(1)
analyze_video(sys.argv[1])
analyze_frames(sys.argv[1])
"""
输出示例:
============================================================
文件: sample.mp4
============================================================
【容器格式】
格式: mov,mp4,m4a,3gp,3g2,mj2
时长: 120.50 秒
大小: 75.23 MB
比特率: 5234 kbps
【视频流】
编码: h264
分辨率: 1920x1080
帧率: 23.98 fps
比特率: 5000 kbps
像素格式: yuv420p
【音频流】
编码: aac
采样率: 48000 Hz
声道: 2
比特率: 192 kbps
【前50帧分析】
--------------------------------------------------
帧序列: IBBPBBPBBPBBPBBPBBPBBPBBPIBBPBBPBBPBBPBBPBBPBBPBBPBB
I帧: 2个, 平均大小: 125.3 KB
P帧: 16个, 平均大小: 45.2 KB
B帧: 32个, 平均大小: 12.1 KB
GOP长度: [24] (平均: 24.0)
"""
九、总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ 视频播放器原理总结 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【核心概念】 │
│ ───────────────────────────────────────── │
│ │
│ 容器格式 (MP4/MKV/AVI) ≠ 编码格式 (H.264/H.265) │
│ 容器是盒子,编码是压缩方式 │
│ │
│ 【播放流程】 │
│ ───────────────────────────────────────── │
│ │
│ 文件 → 解封装 → 解码 → 渲染/播放 │
│ (拆包) (解压) (显示) │
│ │
│ 【帧类型】 │
│ ───────────────────────────────────────── │
│ │
│ I帧: 关键帧,完整图像,体积最大 │
│ P帧: 预测帧,存储差异,参考前帧 │
│ B帧: 双向帧,压缩最好,参考前后帧 │
│ │
│ 【常用格式推荐】 │
│ ───────────────────────────────────────── │
│ │
│ 通用分享: MP4 + H.264 + AAC (兼容性最好) │
│ 高清收藏: MKV + H.265 + 多音轨 (功能最全) │
│ 网页播放: WebM + VP9 或 MP4 + H.264 │
│ 4K视频: MKV/MP4 + H.265/AV1 │
│ │
│ 【编码选择】 │
│ ───────────────────────────────────────── │
│ │
│ ┌──────────┬─────────────────────────────────────────────────────────┐ │
│ │ 需求 │ 推荐 │ │
│ ├──────────┼─────────────────────────────────────────────────────────┤ │
│ │ 最兼容 │ H.264 (所有设备) │ │
│ │ 最省空间 │ H.265/AV1 (同画质小50%) │ │
│ │ 开源免费 │ VP9/AV1 │ │
│ │ 编辑用 │ ProRes/DNxHD (每帧都是I帧) │ │
│ └──────────┴─────────────────────────────────────────────────────────┘ │
│ │
│ 【FFmpeg万能命令】 │
│ ───────────────────────────────────────── │
│ │
│ 查看信息: ffprobe video.mp4 │
│ 转容器: ffmpeg -i in.avi -c copy out.mp4 │
│ 转编码: ffmpeg -i in.mp4 -c:v libx265 -c:a aac out.mp4 │
│ 流媒体优化: ffmpeg -i in.mp4 -c copy -movflags faststart out.mp4 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
一句话总结:
视频播放 = 解封装(拆MP4盒子) + 解码(H.264解压) + 渲染(显示)。MP4是最通用的盒子,H.264是最通用的压缩,MKV功能最全,H.265压缩最好但兼容性稍差。
参考资料:
- FFmpeg官方文档: https://ffmpeg.org/documentation.html
- ISO/IEC 14496 (MPEG-4标准)
- Matroska规范: https://matroska.org/technical/specs/
- 《视频编码全角度详解》