1. 音视频播放原理
2. 图像篇 2.1 YUV和RGB 音视频中编码前的图像格式,即YUV和RGB。
YUV,RGB,包括YcbCr是色彩空间的模型,而平常所说的BMP、PNG、JPEG是文件的存储形式。 而YUV是音视频(编解码)最常用的格式。
YUV有两种分类方式,即空间-间
和空间-内
。空间-间的划分方式主要体现在Y、U、V的比例不同; 空间-内的划分方式主要体现在Y、U、V的比例一定,存储格式不同。
1. YUV 空间-间 的数据划分
YUV按照空间-间
的划分方式,分为YUV444
、YUV422
、YUV420
,如下所示,假设图像为1920*1080:
2. YUV 空间-内 的数据划分
YUV按照空间-内
的划分方式,主要分为packet
、planar
、semi-planar
三种:
packet:打包格式,即先存储一个yuv,再存储下一个yuv;
planar:平面格式,即先存储y平面,再存储u平面,再存储v平面;
semi-planar:先存储y平面,再存储uv平面;
YUV422各种存储格式如下:
YUV420各种存储格式如下:
针对上图中的NV12、NV21、NV16、NV61说明:
3. YUV和RGB
RGB:即red,green,blue三色存储空间,因音视频主要用的是YUV的色彩空间,只简单介绍下RGB和YUV的转换公式:
1 2 3 4 5 Y = 0.299R + 0.587G + 0.114B U= -0.147R - 0.289G + 0.436B V = 0.615R - 0.515G - 0.100B
1 2 3 4 5 R = Y + 1.14V G = Y - 0.39U - 0.58V B = Y + 2.03U
2.2 H.264
H.264是由 ITU-T视频编码专家组(VCEG) 和 ISO/IEC动态图像专家组(MPEG) 联合组成的联合视频组(JVT,Joint Video Team)提出的高度压缩数字视频编解码器标准
。
1. H.264的数据格式
H.264由视频编码层
(VCL)和 网络适配层
(NAL)组成。
NALU payload
是根据NALU头部
中 nal_unit_type
的不同而变化的,它可能包含了不同类型的视频数据。举例来说:
对于片(slice)类型的NALU,NALU payload 将包含经过编码和压缩的视频像素数据,这些数据经过了帧内预测和帧间预测、DCT 变换、量化和比特流编码等处理。
对于参数集类型(如SPS、PPS),NALU payload 将包含编码视频序列的相关参数信息,如图像分辨率、帧率、编码方式等。
对于补充增强信息(SEI),NALU payload 可能包含了额外的元数据或增强信息,例如色彩空间信息、时间戳、双流传输信息等。
2. VCL管理H264视频数据
压缩过程 :
预测 :H.264使用帧内预测和帧间预测来利用视频序列中的时间和空间相关性。帧内预测通过对当前帧内的像素进行预测来减少冗余。帧间预测利用邻近帧的信息进行运动补偿,以减少帧间冗余。
DCT 变换和量化 :对预测残差进行离散余弦变换(DCT),然后将其量化,以减少数据量。
比特流编码 :采用不同的编码技术对量化后的数据进行编码,如熵编码(如CABAC和CAVLC),以生成最终的压缩比特流。
数据切分 :
切片(slice) :VCL将帧划分为不同的切片,每个切片包含一部分宏块(macroblock),允许并行处理和解码,提高系统效率。
宏块(macroblock) :它是一个像素块,包含在切片中,用于进行预测和编码。宏块是H.264中处理的基本单元。
包装成NAL :
NAL(网络抽象层)是为了适应不同网络传输需求而封装VCL生成的编码视频数据。NAL单元由NAL头部和NAL有效负载组成,它提供了对应用层和传输层之间的接口。NAL单元的创建允许对VCL生成的数据进行适当的封装和传输,以便在网络上传输和存储。
VCL
最后会被包装成NAL
3. NAL头的数据结构体
F(forbidden_zero_bit):1 位,初始为0。 当网络识别此单元存在比特错误
时,可将其设为 1
,以便接收方丢掉该单元
NRI(nal_ref_idc):2 位,用来指示该NALU 的重要性等级。 值越大,表示当前NALU越重要。具体大于0 时取何值,没有明确规定
Type(nal_unit_type):5 位,指出NALU 的类型,如下所示:(5,7,8)
4. H.264码流结构
H.264 = start_code + NALU
3 字节的 start_code:
0000 0001
(8 比特,即一个字节)或者是 0000 01
(6 比特)。
用于分隔两个连续的 NALU 单元。
较短的起始码占用更少的比特数(6或8比特),节省了一些带宽。
在较早的H.264编码器或特定的实现中可能会使用这种较短的起始码。
4 字节的 start_code:
0000 0000 0000 0001
(16 比特,两个字节)或者是 00 00 00 01
(32 比特,四个字节)。
同样也是为了分隔两个连续的 NALU 单元。
更长的起始码提供了更大的辨识性和可靠性。在传输过程中,更长的起始码可能对于错误检测和恢复更有帮助。
在更多现代的H.264编码器和实现中使用这种较长的起始码,因为它们提供了更好的容错性。
每个NAL前有一个起始码 0x00 00 01
(或者0x00 00 00 01
) 解码器检测每个起始码,作为一个NAL的起始标识 当检测到下一个起始码时,当前NAL结束。
同时H.264规定,当检测到0x00 00 00
时,也可以表征当前NAL的结束。 那么NAL中数据出现0x00 00 01
或0x00 00 00
时怎么办? H.264引入了防止竞争机制 如果编码器检测到NAL数据存在0x00 00 01
或0x00 00 00
时,编码器会在最后个字节前插入一个新的字节0x03,这样:0x00 00 00
->0x00 00 03 00
0x00 00 01
->0x00 00 03 01
0x00 00 02
->0x00 00 03 02
0x00 00 03
->0x00 00 03 03
5. I帧、P帧和B帧
提到H.264,不得不提I帧、P帧、B帧、IDR帧、GOP。
I帧(Intra-coded picture,帧内编码图像帧),表示关键帧
,采用类似JPEG压缩的DCT(Discrete Cosine Transform,离散余弦变换)压缩技术,可达1/6压缩比而无明显压缩痕迹;
P帧(Predictive-coded picture,前向预测编码图像帧),表示的是跟之前的一个关键帧或P帧的差别
,P帧是参考帧
,它可能造成解码错误的扩散;
B帧(Bidirectionally predicted picture,双向预测编码图像帧),本帧与前后帧(I或P帧)的差别
,B帧压缩率高,但解码耗费CPU;
IDR帧(Instantaneous Decoding Refresh,即时解码刷新):首个I帧
,是立刻刷新,使错误不致传播 IDR导致DPB(DecodedPictureBuffer参考帧列表——这是关键所在)清空; 在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容; IDR具有随机访问的能力,播放器可以从一个IDR帧播放。
GOP(Group Of Picture,图像序列):两个I帧之间是一个GOP(图像序列),一个GOP包含一个I帧
6. 解码时间戳和显示时间戳
当然,H.264中还有两个重要的概念:DTS
和 PTS
3. 音频篇 3.1 PCM
PCM
(Pulse Code Modulation
,脉冲编码调制
)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。
1. 理解PCM
PCM是一种用数字表示采样模拟信号方法。主要包括采样
,量化
,编码
三个主要过程。
2. 描述PCM数据的6个参数:
Sample Rate
: 采样频率。8kHz
(电话)、44.1kHz
(CD)、48kHz
(DVD)。
Sample Size
: 量化位数。常见值为8-bit
、16-bit
。
Number of Channels
: 通道个数。常见的音频有立体声(stereo)
和单声道(mono)
两种类型,立体声包含左声道和右声道。另外还有环绕立体声等其它不太常用的类型。
Sign
: 表示样本数据是否是有符号位,比如用一字节表示的样本数据,有符号的话表示范围为-128 ~ 127,无符号是0 ~ 255。
Byte Ordering
: 字节序。字节序是little-endian
还是big-endian
。通常均为little-endian
。
Integer Or Floating Point
: 整形或浮点型。大多数格式的PCM样本数据使用整形表示,而在一些对精度要求高的应用方面,使用浮点类型表示PCM样本数据
3.2 AAC
AAC(Advanced Audio Coding,高级音频编码)
是一种声音数据的文件压缩格式
。AAC分为ADIF
和ADTS
两种文件格式。
1. 什么是ADIF和ADTS?
2. ADTS的数据结构
4. 字幕篇(SSA & ASS)
SSA
(SubStation Alpha),是由CS Low(亦称Kotus)创建,比传统字幕格式(如SRT
)功能更加先进的字幕文件格式。 该格式字幕的外挂文件以*.ssa
作为后缀。
ASS
(Advanced SubStation Alpha),是一种比SSA
更为高级的字幕格式, 其实质版本是SSA v4.00+
,它是基于SSA 4.00+编码构建的。 ASS的主要变化就是在SSA编写风格的基础上增添更多的特效和指令。 该格式字幕的外挂文件以*.ass
作为后缀。
1. SSA/ASS的基本结构
SSA/ASS
字幕是一种类ini
风格纯文本文件; 包含五个section:Script Info
、v4+ Styles
、Events
、Fonts
、Graphics
。
Script Info
:包含了脚本的头部和总体信息。Script Info
必须是 v4 版本脚本的第一行。
v4+ Styles
:包含了所有样式的定义。每一个被脚本使用的样式都应该在这里定义。 ASS 使用 v4+ Styles
。
Events
:包含了所有脚本的事件,有字幕、注释、图片、声音、影像和命令。基本上,所有在屏幕上看到的内容都在这一部分。
Fonts
:包含了脚本中内嵌字体的信息。
Graphics
:包含了脚本中内嵌图片的信息。
2. SSA字幕范例
SSA字幕样本范例如下:
参考链接
5. 视频封装格式篇 5.1 FLV 1. FLV的封装格式
FLV
(Flash Video),Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。除了播放视频,在直播时也可以使用。采用FLV格式封装的文件后缀为.flv
,格式如下(FLV = FLV Header + Body
)
2. FLV Header
Header 部分记录了FLV的类型、版本、流信息、Header 长度等。 一般整个Header占用9
个字节,大于9个字节则表示头部信息在这基础之上还存在扩展数据。 FLV Header 的信息排布如下所示:
3. FLV Body
Body 是由一个个Tag
组成的,每个Tag下面有一块4
个字节的空间,用来记录这个Tag 的长度。 这个后置的PreviousTagSize
用于逆向
读取处理,表示的是前面的Tag的大小。 FLV Body 的信息排布如下:
4. FLV Tag
每个Tag 也是由两部分组成的:Tag Header
和 Tag Data
。 Tag Header 存放了当前Tag的类型,数据长度、时间戳、时间戳扩展、StreamsID等信息,然后再接着数据区Tag Data。 Tag的排布如下:
5. Tag Data
Tag Data分成 Audio
,Video
,Script
三种。
5.1 Audio Tag Data
音频的Tag Data又分为 AudioTagHeader
和 Data
数据区,其排布结构如下图所示:
5.2 Video Tag Data
Video Tag 由一个字节的VideoTagHeader
和 Video数据区
部分组成
Video数据区部分格式不确定。对于AVC (H.264)
编码部分,Video数据区排布如下:
5.3 Script Tag Data
5.2 TS
TS
(Transport Stream,传输流
),一种常见的视频封装格式,是基于MPEG-2
的封装格式(所以也叫MPEG-TS
),后缀为.ts
。
1. TS的分层结构
TS文件分为三层,如下所示(可以倒序看更好理解):
TS层
(Transport Stream) 在PES层基础上加入了数据流识别信息和传输信息。
PES层
(Packet Elemental Stream) 在ES层基础上加入时间戳(PTS/DTS)等信息。
ES层
(Elementary Stream) 压缩编码后的音视频数据。
2. TS层
ts包大小固定为188字节
,ts层分为三个部分:ts header
、adaptation field
、payload
。
◆ ts Header + adaptation field 格式如下:
2.1 TS Header
pid
决定了负载内容
的类型,主要包括:PAT表,PMT表,视频流,音频流。常用的PID值:
表
PAT
CAT
TSDT
EIT,ST
RST,ST
TDT,TOT,ST
PID
0x0000
0x0001
0x0002
0x0012
0x0013
0x0014
2.2 调整字段
2.3 PAT表结构(指明PMT表的PID值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 typedef struct TS_PAT { unsigned table_id :8 unsigned section_syntax_indicator :1 unsigned zero :1 unsigned reserved_1 :2 unsigned section_length :12 unsigned transport_stream_id :16 unsigned reserved_2 :2 unsigned version_number :5 unsigned current_next_indicator :1 unsigned section_number :8 unsigned last_section_number :8 for (int i=0 ;i<N;i++){ unsigned Program_number :16 unsigned Reversed_3 :3 if (Program_number == 0 ) Network_id :13 else Program_MAP_PID :13 } unsigned CRC_32 :32 }TS_PAT
2.4 PMT表(指明音视频流的PID值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct TS_program_map_section { unsigned Table_id :8 unsigned Section_syntax_indicator :1 unsigned '0' :1 unsigned Reserved :2 unsigned Section_length :12 unsigned Program_number :16 unsigned Reserved :2 unsigned Version_number :5 unsigned Current_next_indicator :1 unsigned Section_number :8 unsigned Last_section_number :8 unsigned Reserved :3 unsigned PCR_PID :13 unsigned Reserved :4 unsigned Program_info_length :12 for (int i = 0 ; i < N; i++) Descriptr () for (int i = 0 ; i < N; i++){ unsigned Stream_type :8 unsigned Reserved :3 unsigned Elementary_PID :13 unsigned Reserved :4 unsigned ES_info_length :12 for (int j = 0 ; j < N2; j++) Descriptr () } unsigned CRC_32 :32 }
2.5 补充
3. PES 层
4. ES 层
5. easyice下载链接:
easyice
5.3 MP4 1. MP4(MPEG-4)
MP4
是一套用于音频、视频信息的压缩编码标准
,由国际标准化组织(ISO)和国际电工委员会(IEC)下属的“动态图像专家组”(Moving Picture Experts Group,即MPEG)制定,第一版在1998年10月通过,第二版在1999年12月通过。MPEG-4格式的主要用途在于网上流、光盘、语音发送(视频电话),以及电视广播。
MP4由许多box
组成,每个box包含不同的信息,这些box以树形结构
的方式组成,box 中可以包含 box
如下:
2. box
mp4文件由若干个box组成。下面是box结构的一个示意图:
box 由 header
和 body
组成 header 指明 box 的 size 和 type。size 是包含 box header 的整个 box 的大小。
box type
,通常是4个ASCII码的字符如ftyp
、moov
等,这些 box type都是已经预定义好的,表示固定的含义。 如果是uuid
,表示该box为用户自定义扩展类型 如果 box type 是未定义的,应该将其忽略。
如果 header 中的 size 为1
,则表示 box 长度需要更多的bits位来描述,在后面会有一个 8字节的 largesize 用来描述 box 的长度。 如果 size 为0
,表示该 box 为文件的最后一个box,文件结尾(同样只存在于mdat
类型的box中)。
box 分为两种,Box
和 Fullbox
。FullBox 是 Box 的扩展,Header 中增加了version
和 flags
字段。
◆ box 定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 aligned (8 ) class Box (unsigned int (32 ) boxtype, optional unsigned int (8 )[16 ] extended_type) { unsigned int (32 ) size ; unsigned int (32 ) type = boxtype; if (size==1 ) { unsigned int (64 ) largesize ; } else if (size==0 ) { } if (boxtype==‘uuid’) { unsigned int (8 )[16 ] usertype = extended_type; } }
◆ FullBox 定义如下:
1 2 3 4 5 aligned (8 ) class FullBox (unsigned int (32 ) boxtype, unsigned int (8 ) v, bit(24 ) f) extends Box (boxtype) { unsigned int (8 ) version = v; bit (24 ) flags = f; }
3. ISO/ICE 14496-12
4. MP4分析工具:
6. FFmpeg篇 6.1 安装 FFmpeg源码地址
FFmpeg可执行文件地址
6.2 ffmpeg, ffplay, ffprobe 1. ffmpeg.exe
1 2 3 ffmpeg -ss 00 :00 :03 -t 00 :00 :04 -i test.mpg -vcodec copy -acodec copy test_cut.mpg
2. ffplay
一个非常简单和可移植的媒体播放器,使用FFmpeg库和SDL库
。ffplay参数如下:
ffplay.exe范例:
3. ffprobe.exe
ffprobe.exe范例:
6.3 解码 正式进入FFmpeg代码的学习。结合代码的形式介绍FFmpeg的解码流程
和SDL2播放流程
。
1. 环境
FFmpeg 开发库下载地址(shared)
SDL 开发库下载地址(选择Development Libraries VC.zip)
2. 音视频解码
音视频解码
流程,以及相关的 ffmpeg
代码如下
3. 音视频播放
Windows平台使用SDL2
来播放
音视频,调用流程及相关代码如下
4. 视频解码代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <stdarg.h> #define MAX_BUF_LEN 1024 int g_log_debug_flag = 1 ;int g_log_info_flag = 1 ;int g_log_warnning_flag = 1 ;int g_log_error_flag = 1 ;enum LOG_LEVEL { LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERROR }; void set_log_flag (int log_debug_flag, int log_info_flag, int log_warnning_flag, int log_error_flag) { g_log_debug_flag = log_debug_flag; g_log_info_flag = log_info_flag; g_log_warnning_flag = log_warnning_flag; g_log_error_flag = log_error_flag; } void output_log (LOG_LEVEL log_level, const char * fmt, ...) { va_list args; va_start (args, fmt); char buf[MAX_BUF_LEN] = { 0 }; vsnprintf (buf, MAX_BUF_LEN - 1 , fmt, args); switch (log_level) { case LOG_DEBUG: if (g_log_debug_flag) printf ("[Log-Debug]:%s\n" , buf); break ; case LOG_INFO: if (g_log_info_flag) printf ("[Log-Info]:%s\n" , buf); break ; case LOG_WARNING: if (g_log_warnning_flag) printf ("[Log-Warnning]:%s\n" , buf); break ; case LOG_ERROR: if (g_log_error_flag) printf ("[Log-Error]:%s\n" , buf); break ; default : break ; } va_end (args); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 #include "vPlayer_sdl2.h" #include "output_log.h" #include <stdio.h> #include <iostream> using namespace std;#define __STDC_CONSTANT_MACROS extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> #include <SDL.h> } static int g_frame_rate = 1 ;static int g_sfp_refresh_thread_exit = 0 ;static int g_sfp_refresh_thread_pause = 0 ;#define SFM_REFRESH_EVENT (SDL_USEREVENT+1) #define SFM_BREAK_EVENT (SDL_USEREVENT+2) typedef struct FFmpeg_V_Param_T { AVFormatContext *pFormatCtx; AVCodecContext *pCodecCtx; SwsContext *pSwsCtx; int video_index; }FFmpeg_V_Param; typedef struct SDL_Param_T { SDL_Window *p_sdl_window; SDL_Renderer *p_sdl_renderer; SDL_Texture *p_sdl_texture; SDL_Rect sdl_rect; SDL_Thread *p_sdl_thread; }SDL_Param; int init_ffmpeg (FFmpeg_V_Param* p_ffmpeg_param, char * filePath) { p_ffmpeg_param->pFormatCtx = avformat_alloc_context (); const AVCodec *pCodec = NULL ; avformat_network_init (); if (avformat_open_input (&(p_ffmpeg_param->pFormatCtx), filePath, NULL , NULL ) != 0 ) { output_log (LOG_ERROR, "avformat_open_input error" ); return -1 ; } if (avformat_find_stream_info (p_ffmpeg_param->pFormatCtx, NULL ) < 0 ) { output_log (LOG_ERROR, "avformat_find_stream_info error" ); return -1 ; } for (int i = 0 ; i < p_ffmpeg_param->pFormatCtx->nb_streams; i++) { AVStream *pStream = p_ffmpeg_param->pFormatCtx->streams[i]; if (pStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { pCodec = avcodec_find_decoder (pStream->codecpar->codec_id); p_ffmpeg_param->pCodecCtx = avcodec_alloc_context3 (pCodec); avcodec_parameters_to_context (p_ffmpeg_param->pCodecCtx, pStream->codecpar); g_frame_rate = pStream->avg_frame_rate.num / pStream->avg_frame_rate.den; p_ffmpeg_param->video_index = i; } } if (!p_ffmpeg_param->pCodecCtx) { output_log (LOG_ERROR, "could not find video codecCtx" ); return -1 ; } if (avcodec_open2 (p_ffmpeg_param->pCodecCtx, pCodec, NULL )) { output_log (LOG_ERROR, "avcodec_open2 error" ); return -1 ; } p_ffmpeg_param->pSwsCtx = sws_getContext (p_ffmpeg_param->pCodecCtx->width, p_ffmpeg_param->pCodecCtx->height, p_ffmpeg_param->pCodecCtx->pix_fmt, p_ffmpeg_param->pCodecCtx->width, p_ffmpeg_param->pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL , NULL , NULL ); av_dump_format (p_ffmpeg_param->pFormatCtx, p_ffmpeg_param->video_index, filePath, 0 ); return 0 ; } int release_ffmpeg (FFmpeg_V_Param* p_ffmpeg_param) { if (!p_ffmpeg_param) return -1 ; if (p_ffmpeg_param->pSwsCtx) sws_freeContext (p_ffmpeg_param->pSwsCtx); if (p_ffmpeg_param->pCodecCtx) avcodec_close (p_ffmpeg_param->pCodecCtx); if (p_ffmpeg_param->pFormatCtx) avformat_close_input (&(p_ffmpeg_param->pFormatCtx)); if (p_ffmpeg_param->pCodecCtx) avcodec_free_context (&(p_ffmpeg_param->pCodecCtx)); if (p_ffmpeg_param->pFormatCtx) avformat_free_context (p_ffmpeg_param->pFormatCtx); delete p_ffmpeg_param; p_ffmpeg_param = NULL ; return 0 ; } int sfp_refresh_thread (void * opaque) { g_sfp_refresh_thread_exit = 0 ; g_sfp_refresh_thread_pause = 0 ; while (!g_sfp_refresh_thread_exit) { if (!g_sfp_refresh_thread_pause) { SDL_Event sdl_event; sdl_event.type = SFM_REFRESH_EVENT; SDL_PushEvent (&sdl_event); } SDL_Delay (1000 / g_frame_rate); } g_sfp_refresh_thread_exit = 0 ; g_sfp_refresh_thread_pause = 0 ; SDL_Event sdl_event; sdl_event.type = SFM_BREAK_EVENT; SDL_PushEvent (&sdl_event); return 0 ; } int init_sdl2 (SDL_Param_T *p_sdl_param, int screen_w, int screen_h) { if (SDL_Init (SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_TIMER)) { output_log (LOG_ERROR, "SDL_Init error" ); return -1 ; } p_sdl_param->p_sdl_window = SDL_CreateWindow ("vPlayer_sdl" , SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL); if (!p_sdl_param->p_sdl_window) { output_log (LOG_ERROR, "SDL_CreateWindow error" ); return -1 ; } p_sdl_param->p_sdl_renderer = SDL_CreateRenderer (p_sdl_param->p_sdl_window, -1 , 0 ); p_sdl_param->p_sdl_texture = SDL_CreateTexture (p_sdl_param->p_sdl_renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, screen_w, screen_h); p_sdl_param->sdl_rect.x = 0 ; p_sdl_param->sdl_rect.y = 0 ; p_sdl_param->sdl_rect.w = screen_w; p_sdl_param->sdl_rect.h = screen_h; p_sdl_param->p_sdl_thread = SDL_CreateThread (sfp_refresh_thread, NULL , NULL ); return 0 ; } int release_sdl2 (SDL_Param_T *p_sdl_param) { SDL_DestroyTexture (p_sdl_param->p_sdl_texture); SDL_DestroyRenderer (p_sdl_param->p_sdl_renderer); SDL_DestroyWindow (p_sdl_param->p_sdl_window); SDL_Quit (); return 0 ; } int vPlayer_sdl2 (char * filePath) { FFmpeg_V_Param *p_ffmpeg_param = NULL ; AVPacket *packet = NULL ; AVFrame *pFrame = NULL , *pFrameYUV = NULL ; int out_buffer_size = 0 ; unsigned char * out_buffer = 0 ; SDL_Param_T *p_sdl_param = NULL ; SDL_Event sdl_event; int ret = 0 ; p_ffmpeg_param = new FFmpeg_V_Param (); memset (p_ffmpeg_param, 0 , sizeof (FFmpeg_V_Param)); if (init_ffmpeg (p_ffmpeg_param, filePath)) { ret = -1 ; goto end; } packet = av_packet_alloc (); pFrame = av_frame_alloc (); pFrameYUV = av_frame_alloc (); out_buffer_size = av_image_get_buffer_size (AV_PIX_FMT_YUV420P, p_ffmpeg_param->pCodecCtx->width, p_ffmpeg_param->pCodecCtx->height, 1 ); out_buffer = (unsigned char *)av_malloc (out_buffer_size); av_image_fill_arrays (pFrameYUV->data, pFrameYUV->linesize, out_buffer, p_ffmpeg_param->pCodecCtx->pix_fmt, p_ffmpeg_param->pCodecCtx->width, p_ffmpeg_param->pCodecCtx->height, 1 ); p_sdl_param = new SDL_Param_T (); memset (p_sdl_param, 0 , sizeof (SDL_Param_T)); if (init_sdl2 (p_sdl_param, p_ffmpeg_param->pCodecCtx->width, p_ffmpeg_param->pCodecCtx->height)) { ret = -1 ; goto end; } while (true ) { int temp_ret = 0 ; SDL_WaitEvent (&sdl_event); if (sdl_event.type == SFM_REFRESH_EVENT) { while (true ) { if (av_read_frame (p_ffmpeg_param->pFormatCtx, packet) < 0 ) { g_sfp_refresh_thread_exit = 1 ; break ; } if (packet->stream_index == p_ffmpeg_param->video_index) { break ; } } if (avcodec_send_packet (p_ffmpeg_param->pCodecCtx, packet)) g_sfp_refresh_thread_exit = 1 ; do { temp_ret = avcodec_receive_frame (p_ffmpeg_param->pCodecCtx, pFrame); if (temp_ret == AVERROR_EOF) { g_sfp_refresh_thread_exit = 1 ; break ; } if (temp_ret == 0 ) { sws_scale (p_ffmpeg_param->pSwsCtx, (const unsigned char * const *)pFrame->data, pFrame->linesize, 0 , p_ffmpeg_param->pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); SDL_UpdateTexture (p_sdl_param->p_sdl_texture, &(p_sdl_param->sdl_rect), pFrameYUV->data[0 ], pFrameYUV->linesize[0 ]); SDL_RenderClear (p_sdl_param->p_sdl_renderer); SDL_RenderCopy (p_sdl_param->p_sdl_renderer, p_sdl_param->p_sdl_texture, NULL , &(p_sdl_param->sdl_rect)); SDL_RenderPresent (p_sdl_param->p_sdl_renderer); } } while (temp_ret != AVERROR (EAGAIN)); } else if (sdl_event.type == SFM_BREAK_EVENT) { break ; } else if (sdl_event.type == SDL_KEYDOWN) { if (sdl_event.key.keysym.sym == SDLK_SPACE) g_sfp_refresh_thread_pause = !g_sfp_refresh_thread_pause; if (sdl_event.key.keysym.sym == SDLK_q) g_sfp_refresh_thread_exit = 1 ; } else if (sdl_event.type == SDL_QUIT) { g_sfp_refresh_thread_exit = 1 ; } } end: release_ffmpeg (p_ffmpeg_param); av_packet_free (&packet); av_frame_free (&pFrame); av_frame_free (&pFrameYUV); release_sdl2 (p_sdl_param); return ret; }
6.4 编码 日常开发过程中,我们涉及到的主要是解码
相关内容,但FFmpeg同样具有编码
的能力。简单介绍下FFmpeg编码的内容。
1. FFmpeg编码视频
使用FFmpeg库编码YUV
,代码及调用逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 #include "encode_yuv_to_h264.h" #include "output_log.h" #include <stdio.h> #include <stdlib.h> #include <string.h> extern "C" { #include <libavcodec/avcodec.h> #include <libavutil/opt.h> #include <libavutil/imgutils.h> } static void encode (AVCodecContext* pCodecCtx, AVFrame *pFrame, AVPacket* pPacket, FILE* p_output_f) { int ret; ret = avcodec_send_frame (pCodecCtx, pFrame); while (ret >= 0 ) { ret = avcodec_receive_packet (pCodecCtx, pPacket); if (ret == AVERROR (EAGAIN) || ret == AVERROR_EOF) return ; fwrite (pPacket->data, 1 , pPacket->size, p_output_f); av_packet_unref (pPacket); } } int encode_yuv_to_h264 (const char * output_filePath) { AVCodecContext* pCodecCtx = NULL ; const AVCodec* pCodec = NULL ; AVPacket* pPacket = NULL ; AVFrame* pFrame = NULL ; char codec_name[] = "libx264" ; unsigned char endcode[] = { 0x00 , 0x00 , 0x01 , 0x7b }; FILE* p_output_f = NULL ; int i, x, y; int ret = 0 ; pCodec = avcodec_find_encoder_by_name (codec_name); if (!pCodec) { output_log (LOG_ERROR, "avcodec_find_encoder_by_name error, codec_name=%s" , codec_name); ret = -1 ; goto end; } pCodecCtx = avcodec_alloc_context3 (pCodec); if (!pCodecCtx) { output_log (LOG_ERROR, "avcodec_alloc_context3 error, pCodecCtx is NULL" ); ret = -1 ; goto end; } pPacket = av_packet_alloc (); pFrame = av_frame_alloc (); pCodecCtx->bit_rate = 400000 ; pCodecCtx->width = 352 ; pCodecCtx->height = 288 ; pCodecCtx->time_base = { 1 , 25 }; pCodecCtx->framerate = { 25 , 1 }; pCodecCtx->gop_size = 10 ; pCodecCtx->max_b_frames = 1 ; pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; if (pCodec->id == AV_CODEC_ID_H264) av_opt_set (pCodecCtx->priv_data, "preset" , "slow" , 0 ); if (avcodec_open2 (pCodecCtx, pCodec, NULL ) < 0 ) { ret = -1 ; goto end; } pFrame->format = pCodecCtx->pix_fmt; pFrame->width = pCodecCtx->width; pFrame->height = pCodecCtx->height; if (av_frame_get_buffer (pFrame, 32 ) < 0 ) { output_log (LOG_ERROR, "av_frame_get_buffer error" ); ret = -1 ; goto end; } fopen_s (&p_output_f, output_filePath, "wb" ); if (!p_output_f) { ret = -1 ; goto end; } for (i = 0 ; i < 25 * 5 ; i++) { fflush (stdout); if (av_frame_is_writable (pFrame) < 0 ) { ret = -1 ; goto end; } for (y = 0 ; y < pCodecCtx->height; y++) { for (x = 0 ; x < pCodecCtx->width; x++) { pFrame->data[0 ][y*pFrame->linesize[0 ] + x] = x + y + i * 3 ; } } for (y = 0 ; y < pCodecCtx->height / 2 ; y++) { for (x = 0 ; x < pCodecCtx->width / 2 ; x++) { pFrame->data[1 ][y * pFrame->linesize[1 ] + x] = 128 + y + i * 2 ; pFrame->data[2 ][y * pFrame->linesize[2 ] + x] = 64 + x + i * 5 ; } } pFrame->pts = i; encode (pCodecCtx, pFrame, pPacket, p_output_f); } encode (pCodecCtx, NULL , pPacket, p_output_f); fwrite (endcode, 1 , sizeof (endcode), p_output_f); fclose (p_output_f); end: if (pCodecCtx) avcodec_free_context (&pCodecCtx); if (pPacket) av_packet_free (&pPacket); if (pFrame) av_frame_free (&pFrame); printf ("=============== encode_yuv_to_h264 done ===============\n" ); return ret; }
2. FFmpeg编码音频
使用FFmpeg库编码PCM
,代码及调用逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 #include "encode_pcm_to_mp2.h" #include "output_log.h" #include <stdint.h> #include <stdio.h> #include <stdlib.h> extern "C" { #include <libavcodec/avcodec.h> #include <libavutil/channel_layout.h> #include <libavutil/common.h> #include <libavutil/frame.h> #include <libavutil/samplefmt.h> } static int check_sample_fmt (const AVCodec *pCodec, enum AVSampleFormat sample_fmt) { const enum AVSampleFormat *p = pCodec->sample_fmts; while (*p != AV_SAMPLE_FMT_NONE) { if (*p == sample_fmt) return 1 ; p++; } return 0 ; } static int select_sample_rate (const AVCodec *pCodec) { const int *p; int best_samplerate = 0 ; if (!pCodec->supported_samplerates) return 44100 ; p = pCodec->supported_samplerates; while (*p) { if (!best_samplerate || abs (44100 - *p) < abs (44100 - best_samplerate)) best_samplerate = *p; p++; } return best_samplerate; } static int select_channel_layout (const AVCodec *codec) { const uint64_t *p; uint64_t best_ch_layout = 0 ; int best_nb_channels = 0 ; if (!codec->channel_layouts) return AV_CH_LAYOUT_STEREO; p = codec->channel_layouts; while (*p) { int nb_channels = av_get_channel_layout_nb_channels (*p); if (nb_channels > best_nb_channels) { best_ch_layout = *p; best_nb_channels = nb_channels; } p++; } return best_ch_layout; } static void encode (AVCodecContext* pCodecCtx, AVFrame *pFrame, AVPacket* pPacket, FILE* p_output_f) { int ret; ret = avcodec_send_frame (pCodecCtx, pFrame); while (ret >= 0 ) { ret = avcodec_receive_packet (pCodecCtx, pPacket); if (ret == AVERROR (EAGAIN) || ret == AVERROR_EOF) return ; fwrite (pPacket->data, 1 , pPacket->size, p_output_f); av_packet_unref (pPacket); } } int encode_pcm_to_mp2 (const char * output_filepath) { AVCodecContext* pCodecCtx = NULL ; const AVCodec* pCodec = NULL ; AVPacket* pPacket = NULL ; AVFrame* pFrame = NULL ; FILE* p_output_f = NULL ; int i, j, k, ret = 0 ; uint16_t *samples; float t, tincr; pCodec = avcodec_find_encoder (AV_CODEC_ID_MP2); if (!pCodec) { output_log (LOG_ERROR, "avcodec_find_encoder(AV_CODEC_ID_MP2) error" ); ret = -1 ; goto end; } pCodecCtx = avcodec_alloc_context3 (pCodec); pPacket = av_packet_alloc (); if (!pPacket) { output_log (LOG_ERROR, "av_packet_alloc error" ); ret = -1 ; goto end; } pFrame = av_frame_alloc (); if (!pFrame) { output_log (LOG_ERROR, "av_frame_alloc error" ); ret = -1 ; goto end; } pCodecCtx->bit_rate = 64000 ; pCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16; if (!check_sample_fmt (pCodec, pCodecCtx->sample_fmt)) { output_log (LOG_ERROR, "check_sample_fmt error" ); ret = -1 ; goto end; } pCodecCtx->sample_rate = select_sample_rate (pCodec); pCodecCtx->channel_layout = select_channel_layout (pCodec); pCodecCtx->channels = av_get_channel_layout_nb_channels (pCodecCtx->channel_layout); if (avcodec_open2 (pCodecCtx, pCodec, NULL ) < 0 ) { ret = -1 ; goto end; } pFrame->nb_samples = pCodecCtx->frame_size; pFrame->format = pCodecCtx->sample_fmt; pFrame->channel_layout = pCodecCtx->channel_layout; if (av_frame_get_buffer (pFrame, 0 ) < 0 ) { output_log (LOG_ERROR, "av_frame_get_buffer error" ); ret = -1 ; goto end; } fopen_s (&p_output_f, output_filepath, "wb" ); if (!p_output_f) { output_log (LOG_ERROR, "fopen_s error" ); ret = -1 ; goto end; } t = 0 ; tincr = 2 * M_PI * 440.0 / pCodecCtx->sample_rate; for (i = 0 ; i < 200 ; i++) { if (av_frame_make_writable (pFrame) < 0 ) { output_log (LOG_ERROR, "av_frame_make_writable error" ); ret = -1 ; goto end; } samples = (uint16_t *)pFrame->data[0 ]; for (j = 0 ; j < pCodecCtx->frame_size; j++) { samples[2 * j] = (int )(sin (t) * 10000 ); for (k = 1 ; k < pCodecCtx->channels; k++) { samples[2 * j + k] = samples[2 * j]; } t += tincr; } encode (pCodecCtx, pFrame, pPacket, p_output_f); } encode (pCodecCtx, NULL , pPacket, p_output_f); fclose (p_output_f); end: if (pCodecCtx) avcodec_free_context (&pCodecCtx); if (pPacket) av_packet_free (&pPacket); if (pFrame) av_frame_free (&pFrame); printf ("=============== encode_pcm_to_mp2 done ===============\n" ); return ret; }
6.5 转码 转码比较好理解,就是将解码和编码结合
起来,过程为:解封装->解码->编码->封装
。逻辑如下:
解封装:将音视频文件的封装格式去掉,获取视频流(H.264)和音频流(AAC)
解码:将视频流解码成原始图像数据
(YUV),将音频流解码成原始音频数据
(PCM)
编码:将原始图像(YUV)进行编码
(MPG2),将音频流进行编码
(MP3)
封装:将视频流和音频流封装成视频文件
6.6 ffmpeg 结构体分析 知道了如何通过调用FFmpeg代码来进行解码和编码,知其然还要知其所以然 这篇文章分析下FFmpeg编解码过程中主要的结构体,通过对这些结构体的分析,可以更好的理解FFmpeg的编解码过程。
1. FFmpeg结构体分析
总结FFmpeg结构体主要分为三个层次:协议层
(AVIOContext)、封装层
(AVInputFormat)、解码层
(AVStream)
2. FFmpeg编解码关键结构体
按照FFmpeg编解码时代码的调用过程,总结八个关键结构体。 按照编解码调用顺序介绍如下:
2.1 AVFormatContext :描述媒体文件或媒体流的构成和基本信息,贯穿ffmpeg使用整个流程
1 2 3 4 5 6 7 8 AVInputFormat *iformat、AVOutputFormat *oformat AVIOContext *pb unsigned int nb_streams AVStream **streams char *url int64_t duration int bit_rate AVDictionary *metadata
2.2 AVInputFormat: 文件的封装格式
1 2 3 char * name char * long_name char * extensions
2.3 AVIOContext->URLContext->URLProtocol
AVIOContext
:文件(协议)操作的顶层对象
1 2 3 4 5 6 7 8 unsigned char *buffer int buffer_size unsigned char *buf_ptr unsigned char *buf_end void *opaque (*read_packet)(...) (*write_packet)(...) (*read_pause)(...)
URLContext
:每种协议,有一个协议操作对象和一个关联的协议对象
1 2 3 char * name const struct URLProtocol *prot void *priv_data
2.4 AVStream :存储音频流或视频流的结构体
1 2 3 4 5 int index AVRational time_base int64_t duration AVRational avg_frame_rate AVCodecParameters *codecpar
2.5 AVCodecParameter 和 AVCodecContext
新的 ffmpeg 中 AVStream.codecpar(struct AVCodecParameter)
代替 AVStream.codec(struct AVCodecContext)
AVCodecParameter
是由 AVCodecContext
分离出来的,AVCodecParameter中没有函数
AVCodecContext
结构体仍然是编解码时不可或缺的结构体avcodec_send_packet
和 avcodec_receive_frame
使用 AVCodecContext
AVCodecContext
和 AVCodec
的获取方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char filePath[] = "test.mp4" ;AVFormatContext *pFormatCtx; AVCodecContext *pCodecCtx; AVCodec *pCodec; pFormatCtx = avformat_alloc_context (); av_register_all ();avformat_network_init ();avformat_open_input (&pFormatCtx, filePath, NULL , NULL );avformat_find_stream_info (pFormatCtx, NULL );for (int i = 0 ; i < pFormatCtx->nb_streams; i++){ AVStream *pStream = pFormatCtx->streams[i]; pCodec = avcodec_find_decoder (pStream->codecpar->codec_id); pCodecCtx = avcodec_alloc_context3 (pCodec); avcodec_parameters_to_context (pCodecCtx, pStream->codecpar); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 char filePath[] = "test.mp4" ;AVFormatContext *pFormatCtx; AVCodecContext *pCodecCtx; AVCodec *pCodec; pFormatCtx = avformat_alloc_context (); av_register_all ();avformat_network_init ();avformat_open_input (&pFormatCtx, filePath, NULL , NULL );avformat_find_stream_info (pFormatCtx, NULL );for (int i = 0 ; i < pFormatCtx->nb_streams; i++){ AVStream *pStream = pFormatCtx->streams[i]; pCodecCtx = pStream->codec; pCodec = avcodec_find_decoder (pCodecCtx->codec_id); }
◆ 关键参数:可参考 avcodec_parameters_to_context 源码
1 2 3 4 5 6 7 8 9 10 11 12 enum AVMediaType codec_type enum AVCodecID codec_id AVCodecContext:struct AVCodec *codec int bit_rate uint8_t *extradata; int extradata_size AVCodecContext:enum AVPixelFormat pix_fmt int width, height AVCodecContext:enum AVSampleFormat sample_fmt int sample_rate int channels uint64_t channel_layout AVCodecParameters:int format
2.6 AVCodec :编解码器结构体
每一个解码器对应
一个AVCodec结构体
AVCodec ff_h264_decoder,AVCodec ff_jpeg2000_decoder
◆ 关键成员变量:
1 2 3 4 5 6 7 8 9 const char *name const char *long_name enum AVMediaType type enum AVCodecID id const AVRational *supported_framerates const enum AVPixelFormat *pix_fmts const int *supported_samplerates const enum AVSampleFormat *sample_fmts const uint64_t *channel_layouts
2.7 AVPacket :存储解码前数据的结构体
◆ 关键成员变量
1 2 3 4 5 6 AVBufferRef *buf uint8_t *data int size int64_t pts int64_t dts int stream_index
AVPacket的内存管理:AVPacket本身并不包含压缩的数据,通过data指针
引用数据的缓存空间 多个AVPacket共享
同一个数据缓存(AVBufferRef
、AVBuffer
) AVPacket拥有独立的数据缓存
1 2 3 av_read_frame (pFormatCtx, packet) av_packet_ref (dst_pkt,packet) av_packet_unref (dst_pkt);
◆ AVBuffer 关键成员变量
1 2 3 uint8_t *data size_t size atomic_uint refcount
2.8 AVFrame :存储解码后数据的结构体
1 2 3 4 5 6 7 8 9 10 11 uint8_t *data[AV_NUM_DATA_POINTERS] int linesize[AV_NUM_DATA_POINTERS] int width, height int nb_samples int format int key_frame enum AVPictureType pict_type AVRational sample_aspect_ratio int64_t pts int coded_picture_number int display_picture_number
6.7 ffmpeg 源码分析 前面分析FFmpeg的编解码调用过程,也对FFmpeg编解码过程中使用到的关键结构体进行分析 现以FFmpeg解码流程
为主线,深入分析其中层层调用的FFmpeg代码。
FFmpeg解码过程涉及到源码调用过程,如图所示 橙色部分+红色箭头为解码主流程 橙色部分为FFmpeg对外提供的接口函数 白色部分为橙色功能函数实现中涉及到主要功能函数):
主要函数
avformat_network_init():初始化网络库
avformat_open_input():打开输入流,获取头信息
avformat_find_stream_info():通过读媒体数据,获取音视频流信息
avcodec_find_decoder():通过ID查找音频解码器或者视频解码器
avcodec_parameters_to_context():填充解码器上下文
avcodec_open2():打开音频解码器或者视频解码器
av_read_frame():获取音视频(编码)数据
avcodec_send_packet():将音视频编码数据发送给解码器
avcodec_receive_frame():获取解码后的音视频数据(音视频原始数据,如YUV和PCM)
6.8 ffplay源码分析 前面介绍FFmpeg的解码、编码、转码流程及相关的函数调用,并以FFmpeg解码流程为主线,深入分析其中层层调用的FFmpeg代码
做一个真正的播放器,还有很多问题需要处理:音视频同步
,多线程解复用
、解码
,线程安全等
问题需要处理。 带着问题,分析下ffplay(FFmpeg提供的一个简单的播放器)的源码。
ffplay解码并播放音视频的源码分析如图所示 (分为两条主线,一条为解复用、解码,另一条为事件循环(包含音视频播放)):
1. 解复用、解码
stream_open:起始入口
audio_thread:音频处理线程
音频数据流转:audioq(编码数据)->sampq(解码数据)
video_thread:视频处理线程
视频数据流转:videoq(编码数据)->pictq(解码数据)
subtitle_thread:字幕解码线程
字幕数据流转:subtitleq(编码数据)->subpq(解码数据)
2. 事件循环(包含音视频播放)
3. 事件循环
7. 协议篇 7.1 简介 RTP
,RTCP
,RTMP
等等协议
如下罗列了部分流媒体相关协议,稍后详细介绍这些协议:
RTP(Real-time Transport Protocol),实时传输协议。
RTCP(Real-time Transport Control Protocol),实时传输控制协议。
RTSP(Real Time Streaming Protocol),实时流协议。
RTMP(Real Time Messaging Protocol),实时消息传输协议。
HLS(HTTP Live Streaming),苹果公司提出的基于HTTP的流媒体网络传输协议。
HTTP-FLV,将RTMP
等负载信息携带在HTTP协议之上的码流传输协议。
7.2 RTP & RTCP & RTSP RTP、RTCP这两个协议一般是成对出现,而他们与RTSP协同工作。介绍下这三个协议。
1. RTP、RTCP、RTSP的关系
RTP 负责多媒体的传输 RTCP 配合 RTP 做控制和流量统计 RTSP 负责建立和控制会话
2. RTP(Real-time Transport Protocol),实时传输协议:
◆ RTP报文格式如下:
V
:RTP协议的版本号,占2位,当前协议版本号为2P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。X
:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头CC
:CSRC计数器,占4位,指示CSRC 标识符的个数M
:标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。PT
:有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。sequence number
:序列号,16比特,发送方在每发送完一个RTP包后就将该域的值增加1,接收方可以由该域检测包的丢失及恢复包序列。序列号的初始值是随机的。timestamp
:时间戳,32比特,记录了该包中数据的第一个字节的采样时刻。在一次会话开始时,时间戳初始化成一个初始值。即使在没有信号发送时,时间戳的数值也要随时间而不断地增加(时间在流逝嘛)。时间戳是去除抖动和实现同步不可缺少的。SSRC
:同步源标识符,32比特,同步源就是指RTP包流的来源。在同一个RTP会话中不能有两个相同的SSRC值。该标识符是随机选取的 RFC1889推荐了MD5随机算法。CSRC
:贡献源列表,0~15项,每项32比特,用来标志对一个RTP混合器产生的新包有贡献的所有RTP包的源。由混合器将这些有贡献的SSRC标识符插入表中。SSRC标识符都被列出来,以便接收端能正确指出交谈双方的身份
3. RTCP(Real-time Transport Control Protocol),实时传输控制协议:
RTP
和RTCP
是一起使用的;
RTCP的主要功能是为RTP所提供的服务质量提供反馈
,RTCP收集相关媒体连接的统计信息 例如:传输字节数,传输分组数,丢失分组数,时延抖动,单向和双向网络延迟等等;
网络应用程序可以利用RTCP所提供的信息试图提高服务质量
,比如限制信息流量或改用压缩比较小的编解码器;
RTCP本身不提供
数据加密或身份认证,其伴生协议SRTCP
(安全实时传输控制协议)则可用于此类用途;
◆ RTCP报文格式如下:
4. RTSP(Real Time Streaming Protocol),实时流协议:
RTSP是一种双向实时数据传输协议
;
RTSP在体系结构上位于RTP和RTCP之上
,它使用TCP或RTP完成数据传输;
RTSP主要用来控制具有实时特性
的数据发送 比如:对流媒体提供诸如播放、暂停、快进等操作;
RTSP负责定义具体的控制消息、操作方法、状态码等,但它本身并不传输数据
,而是必须依赖于下层传输协议
所提供的某些服务;
7.3 RTMP & HLS 1. RTMP(Real Time Messaging Protocol)
RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP
,是一个协议族,包括RTMP基本协议RTMPT/RTMPS/RTMPE等多种变种。 RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。 支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。RTMP与HTTP一样,都属于TCP/IP四层模型的应用层。
应用层协议,依靠TCP保证可靠传输。
默认端口:1953
,可能被防火墙屏蔽。
在流媒体/交互服务器之间进行音视频和数据通信
。
2. HLS(HTTP Live Streaming)
HLS是一个由苹果公司提出的基于HTTP 的流媒体网络传输协议。
工作原理 : 把整个流分成一个个小的基于HTTP的TS文件来下载,每次只下载一些。 当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。
HLS请求基本的HTTP报文,与实时传输协议(RTP
)不同HLS可以穿过任何允许HTTP数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。
HLS规范:
7.4 对比RTMP,HLS和HTTP-FLV 在开始之前,我们先要明确一个问题,根据应用场景不同,流媒体协议分为:
RTMP
>> 双端HLS
>> 拉流端HTTP-FLV
>> 拉流端。
我们先放一张表格从几个维度来对比下这三种协议。
RTMP
HTTP-FLV
HLS
传输协议
TCP
HTTP
HTTP
视频封装格式
flv
flv
ts
延时
1-3秒
1-3秒
5-20秒
Web支持
H5需要使用插件
H5需要使用插件
支持H5
数据
连续流
连续流
切片文件
ps: H5 是 HTML5
1. RTMP & HTTP-FLV
这两个协议实际上传输的数据是一样的,数据都是flv文件的tag
。
RTMP:实时播放服务器的 FLV 文件或服务器转发的FLV数据,本地无 FLV 缓存文件
,FLV保密性好
。
HTTP-FLV:将 FLV 下载到本地
再播放,FLV保密性不好
。
2. HLS & RTMP
RTMP:采用1935
端口,而非HTTP80
端口,在某些网络环境下可能被屏蔽
。
RTMP:是一种有状态协议
,需要为每一个播放视频流的客户端维护状态,服务器平滑扩展难度大
。
HLS:基于无状态HTTP协议
,客户端只需要按顺序使用下载的TS文件就可,负载均衡如同普通的HTTP文件服务器负载均衡一样。
3. HTTP-FLV
HTTP-FLV 结合了 RTMP 和 HLS 的优点,易用(HTTP协议)低延时(flv)
4. 为什么 RTPM 比 HLS 快
8. 服务器篇
介绍下目前常用的流媒体服务器,并实际部署下部分服务器,感受下流媒体服务器是如何工作。 市面上优秀的流媒体服务器解决方案有很多,比如SRS,Red5,EasyDarwin,nginx-rtmp,live555,mediasoup等等。 这些服务器框架各有优缺点,需要明白的是,世上没有一款完美的流媒体服务器解决方案,在流媒体选型的时候,需要根据自己的业务选择适合的流媒体服务器
8.1 nginx-rtmp 的部署 介绍下使用nginx-rtmp来部署一套流媒体服务器,并用FFmpeg来推流,VLC来拉流。
1. 下载nginx 和 nginx-rtmp-module
1 2 https://nginx.org/en/download.html https://github.com/arut/nginx-rtmp-module.git
2. 解压压缩包
1 2 $tar xvf nginx-1.20.1.tar.gz$unzip nginx-rtmp-module-master.zip
3. 创建build目录
1 2 $cd nginx-1.20.1$mkdir build
4. config & make & make install
1 2 3 $./configure --prefix=/home/fly/work/nginx-rtmp/nginx-1.20.1/build --add-module=/home/fly/work/nginx-rtmp/nginx-rtmp-module-master $make $make install
5. config过程中错误及错误处理
1 2 3 error : ./configure: error: the HTTP rewrite module requires the PCRE library. error :./configure: error: SSL modules require the OpenSSL library. error :./configure: error: the HTTP gzip module requires the zlib library.
1 2 3 4 sudo apt-get update sudo apt-get install libpcre3 libpcre3-dev sudo apt-get install openssl libssl-dev sudo apt-get install zlib1g-dev
6. 配置文件:/home/fly/work/nginx-rtmp/nginx-1.20.1/build/conf/nginx.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 rtmp { server { listen 1935; chunk_size 4096; application rtmp_live { live on; } application rtmp_play{ play ./videos; } } }
7. 启动 nginx
1 ~/home/fly/work/nginx-rtmp/nginx-1.20.1/build$ sudo ./sbin/nginx
8. ffmpeg推流
1 ffmpeg -i test.flv -vcodec libx264 -acodec aac -f flv rtmp://192.168.2.128:1935/rtmp_live/mystream
9. VLC拉流(直播、点播)
1 2 直播:rtmp://192.168.2.128:1935/rtmp_live/mystream 点播:rtmp://192.168.2.128:1935/rtmp_play/test.flv
8.2 SRS 的部署 介绍下使用SRS来部署一套流媒体服务器,并用FFmpeg来推流,VLC来拉流。
1. 下载srs源码
1 https://github.com/ossrs/srs/releases
2. 解压压缩包
1 $tar xvf srs-3.0-r8.tar.gz
3. 创建build目录
1 2 $cd srs-3.0-r8/trunk/$mkdir build
4. config & make & make install
1 2 3 $./configure --prefix=/home/fly/work/srs/srs-3.0-r8/trunk/build --full $make $make install
5. make过程中出现错误及解决方法
1 2 3 4 5 6 In file included from ../../../objs/gtest/include/gtest/internal/gtest-port.h:197:0, from ../../../objs/gtest/include/gtest/internal/gtest-internal.h:40, from ../../../objs/gtest/include/gtest/gtest.h:57, from ../../../src/utest/srs_utest.hpp:36, from ../../../src/utest/srs_utest.cpp:24: /usr/include/c++/7/sstream:300:7: error: ‘struct std::__cxx11::basic_stringbuf<_CharT, _Traits, _Alloc>::__xfer_bufptrs’ redeclared with different access struct __xfer_bufptrs
1 2 3 $./configure --prefix=/home/fly/work/srs/srs-3.0-r8/trunk/build --full --without-utest $make $make install
6. 增加配置文件:/home/fly/work/srs/srs-3.0-r8/trunk/build/conf/my_hls.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 listen 1935; max_connections 1000; daemon on; srs_log_tank file; srs_log_level error; srs_log_file ./objs/srs.log; http_server { enabled on; listen 8081; dir ./objs/nginx/html; } vhost __defaultVhost__ { hls { enabled on; hls_fragment 10; hls_window 60; hls_path ./objs/nginx/html; hls_m3u8_file [app]/[stream].m3u8; hls_ts_file [app]/[stream]-[seq ].ts; hls_cleanup on; hls_dispose 30; hls_on_error continue ; hls_storage disk; hls_wait_keyframe on; hls_acodec aac; hls_vcodec h264; } }
7. 启动 srs
1 ~/home/fly/work/srs/srs-3.0-r8/trunk/build$sudo ./objs/srs -c ./conf/my_hls.conf
8. ffmpeg推流
1 ffmpeg -i test.mp4 -vcodec libx264 -acodec aac -f flv rtmp://192.168.2.128:1935/live/index
9. 生成的 m3u8 和 ts 文件路径
1 ~/work/srs/srs-3.0-r8/trunk/build/objs/nginx/html/live
10. VLC拉流(rtmp、http)
1 2 3 4 5 6 7 8 9 10 11 12 13 rtmp://192.168.2.128:1935/live/index http://192.168.2.128:8081/live/index.m3u8 `````` 本篇文章,我们详细介绍下使用live555来部署一套流媒体服务器,并使用VLC来拉流。 **1.下载live555源码** ```sh http://www.live555.com/liveMedia/public/
2. 解压
1 2 $tar vxf live.2021.08.24.tar.gz
3. make
1 2 $./genMakefiles linux-64bit $make
4. 启动live555
1 2 $cd mediaServer$sudo ./live555MediaServer
5. 上传视频
1 将test.mkv上传至 live/mediaServer目录下
6. 播放
1 2 拷贝 live555 生成的 url 地址 vlc拉流:rtsp://192.168.2.128:8554/test.mkv