保存视频的每一帧,每个像素没要必要,并且也是不现实的,由于这个数据量太大了,以致于没办法存储和传输,好比说,一个视频大小是 1280×720 像素,一个像素占 12 个比特位,每秒 30 帧,那么一分钟这样的视频就要占 1280×720×12×30×60/8/1024/1024=2.3GB 的空间,因此视频数据确定要进行压缩存储和传输的。
而能够压缩的冗余数据有不少,从空间上来讲,一帧图像中的像素之间并非毫无关系的,相邻像素有很强的相关性,能够利用这些相关性抽象地存储。一样在时间上,相邻的视频帧之间内容类似,也能够压缩。每一个像素值出现的几率不一样,从编码上也能够压缩。人类视觉系统(HVS)对高频信息不敏感,因此能够丢弃高频信息,只编码低频信息。对高对比度更敏感,能够提升边缘信息的主观质量。对亮度信息比色度信息更敏感,能够下降色度的解析度。对运动的信息更敏感,能够对感兴趣区域(ROI)进行特殊处理。
视频数据压缩和传输的实现与最终将这些数据还原成视频播放出来的实现是紧密相关的,也就是说视频信息的压缩和解压缩须要一个统一标准,即音视频编码标准。html
制定音视频编码标准的有两个组织机构,一个是国际电联下属的机构 ITU-T(ITU Telecommunication Standardization Sector),一个是国际标准化组织 ISO 和国际电工委员会 IEC 下属的 MPEG(Moving Picture Experts Group) 专家组。
1988 年,ITU-T 制定了第一个实用的视频编码标准 H.261,这也是第一个 H.26x 家族的视频编码标准,以后的一些视频编码标准大多都是以此为基础的。它的的基本处理单元称为宏块,H.261 是宏块概念出现的第一个标准。每一个宏块由 16×16 阵列的亮度样本和两个对应的 8×8 色度样本阵列组成,使用 4:2:0 采样和 YCbCr 色彩空间。编码算法使用运动补偿的图片间预测和空间变换编码的混合,涉及标量量化,Z 字形扫描和熵编码。
1993 年,ISO/IEC 制定了有损压缩标准 MPEG-1,其中最著名的部分是它引入的 MP3 音频格式。
2003 年,ITU-T 和 MPEG 共同组成的 JVT(Joint Video Team)联合视频小组开发了优秀且广为流行的 H.264 标准,该标准既是 ITU-T 的 H.264 标准,也是 MPEG-4 的第十部分(第十部分也叫 AVC(Advanced Video Coding)),因此 H.264/AVC, AVC/H.264, H.264/MPEG-4 AVC, MPEG-4/H.264 AVC 都是指 H.264。而以后的 HEVC(High Efficiency Video Coding)视频压缩标准既是指 H.265 也是指 MPEG-H 第二部分。
2003 年,微软基于 WMV9(Windows Media Video 9)格式开发了视频编码标准 VC-1。
2008 年,Google 基于 VP7 开源了 VP8 视频压缩格式。 VP8 能够与 Vorbis 和 Opus 音频一块儿多路复用到基于 Matroska 的容器格式 WebM 中。图像格式 WebP 基于 VP8 的帧内编码。以后的 VP9 和 AOMedia(Alliance for Open Media)开发的 AV1(AOMedia Video 1)都是基于 VP8 的。这个系列编码标准的最大优点是它是开放的,免版权税的。java
一个多媒体文件或者多媒体流可能包含多个视频、音频、字幕、同步信息,章节信息以及元数据等数据。也就是说咱们一般看到的 .mp4 、.avi、.rmvb 等文件中的 MP四、AVI 实际上是一种容器格式(container formats),用来封装这些数据,而不是视频的编码格式。linux
muxer 就是用来封装多媒体容器格式的封装器,好比把一个 rmvb 视频文件,mp3 音频文件以及 srt 字幕文件,封装成为一个新的 mp4 文件。而 demuxer 就是解封装器,能够将容器格式分解成视频流、音频流、附加数据等信息。android
编解码器,是编码器(Encoder)和 解码器(Decoder)的统称。git
Intra-frame,也被称为 I-pictures 或 keyframes,也就是说俗称的关键帧,是指不依赖于其余任何帧进行渲染的视频帧,简单呈现一个固定图像。两个关键帧之间的视频帧是能够预测计算出来的,但两个 I 帧之间的帧数不可能特别大,由于解码的复杂度,解码器缓冲区大小,数据错误后的恢复时间,搜索能力以及在硬件解码器中最多见的低精度实现中 IDCT 错误的累积,限制了 I 帧之间的最大帧数。程序员
Predicted-frame,也被称为向前预测帧或帧间帧,仅存储与紧邻它的前一个帧(I 帧或 P 帧,这个参考帧也称为锚帧)的图像差别。使用帧的每一个宏块上的运动矢量计算 P 帧与其锚帧之间的差别,这种运动矢量数据将嵌入 P 帧中以供解码器使用。除了任何前向预测的块以外,P 帧还能够包含任意数量的帧内编码块。若是视频从一帧到下一帧(例如剪辑)急剧变化,则将其编码为 I 帧会更有效。若是 P 帧丢失,视频画面可能会出现花屏或者马赛克的现象。github
Bidirectional-frame,表明双向帧,也被称为向后预测帧或 B-pictures。 B 帧与 P 帧很是类似,B 帧可使用前一帧和后一帧(即两个锚帧)进行预测。所以,在能够解码和显示 B 帧以前,播放器必须首先在 B 帧以后顺序解码下一个 I 或 P 锚帧。这意味着解码 B 帧须要更大的数据缓冲器,并致使解码和编码期间的延迟增长。这还须要容器/系统流中的解码时间戳(DTS)特征。所以,B 帧长期以来一直备受争议,它们一般在视频中被避免,有时硬件解码器不能彻底支持它们。不存在从 B 帧 预测的帧的,所以,能够在须要时插入很是低比特率的 B 帧,以帮助控制比特率。若是这是用 P 帧完成的,则能够从中预测将来的 P 帧,而且会下降整个序列的质量。除了向后预测或双向预测的块以外,B帧还能够包含任意数量的帧内编码块和前向预测块。web
网络抽象层 NAL(Network Abstraction Layer)和 视频编码层 VCL(Video Coding Layer)是 H.264/AVC 和 HEVC 标准的一部分,NAL 的主要目的是对访问“会话”(视频通话)和“非会话”(存储、传播、转成媒体流)应用的网络友好的视频表示一个规定。NAL 用来格式化 VCL 的视频表示,并以适当的方式为经过各类传输层和存储介质进行的传输提供头信息。也就是说 NAL 有助于将 VCL 数据映射到传输层。
NALU(NAL units)是已编码的视频数据用来存储和传输的基本单元,NAL 单元的前一个(H.264/AVC)或两个(HEVC)字节是 Header 字节,用来标明该 NAL 单元中数据的类型。其它字节是有效载荷。
NAL 单元分为 VCL 和非 VCL 的 NAL 单元。VCL NAL 单元包含表示视频图像中样本值的数据,非 VCL NAL 单元包含任何相关的附加信息,例如参数集 parameter sets(可应用于大量 VCL NAL 单元的重要 header 数据)和补充加强信息 SEI(Supplemental enhancement information)(定时信息和其余能够加强解码视频信号可用性的补充数据,但对于解码视频图像中的样本的值不是必需的)。
参数集分为两种类型: SPS(sequence parameter sets)和 PPS(picture parameter sets)。SPS 应用于一系列连续的已编码的视频图像(即已编码视频序列),PPS 应用于已编码视频序列中一个或多个单独图像的解码。也就是说 SPS 和 PPS 将不频繁改变信息的传输和视频图像中样本值编码表示的传输分离开来。每一个 VCL NAL 单元包含一个指向相关 PPS 内容的标识符,而每一个 PPS 都包含一个指向相关 SPS 内容的标识符。所以仅仅经过少许数据(标识符)就能够引用大量的信息(参数集)而无需在每一个 VCL NAL 单元中重复该信息了。SPS 和 PPS 能够在它们要应用的 VCL NAL 单元以前发送,而且能够重复发送以提高针对数据丢失的顽健性。
NAL Header 字节中的 nal_ref_idc 用于表示当前 NALU 的重要性,值越大,越重要,解码器在解码处理不过来的时候,能够丢掉重要性为 0 的 NALU。SPS/PPS 时,nal_ref_idc 不可为 0。当某个图像的 slice 的 nal_ref_id 等于 0 时,该图像的全部片均应等 0。nal_unit_type 表示 NALU 的类型,7 表示这个 NALU 是 SPS,8 表示这个 NALU 是 PPS。5 表示这个 NALU 是 IDR(instantaneous decoding refresh,即 I 帧) 的 slice,1 表示这个 NALU 所在的帧是 P 帧。算法
PS(Program Streams)指将多个打包的基本码流 PES (一般是一个音频 PES 和一个视频 PES)组合成的单个流,以确保同时传送并保持同步,PS 也被称为多路传输(multiplex)或容器格式(container format)。
PTS(Presentation time stamps): PS 中的 PTS 用来校订音频和视频 SCR(system clock reference)值之间的不可避免的差别(时基校订),如 PS 头中的 90 kHz PTS 值告诉解码器哪些视频 SCR 值与哪些音频 SCR 值匹配。PTS 决定了什么时候显示 MPEG program 的一部分,而且解码器还使用它来肯定什么时候能够从缓冲器中丢弃数据。解码器将延迟视频或音频中的一个,直到另外一个的相应片断到达而且能够被解码。
DTS(Decoding Time Stamps): 对于视频流中的 B 帧,必须对相邻帧进行无序编码和解码(从新排序的帧)。DTS 与 PTS 很是类似,但它不只仅处理顺序帧,而是包含适当的时间戳,在它的锚帧(P 帧 或 I 帧)以前,告诉解码器什么时候解码并显示下一个 B 帧。若是视频中没有B帧,那么 PTS 和 DTS 值是相同的。安全
FFMPEG 项目是在 2000 年由法国著名程序员 Fabrice Bellard 发起的,名字是受到 MPEG 专家组的启发,前面的 “FF” 是 “fast forward” 快进的意思。FFMPEG 是一个能够录制音视频,转码音视频的格式,将音视频转成媒体流的完整的、跨平台的 解决方案。它是一个自由的软件项目,任何人均可以避免费使用和修改,只要遵循 GPL 或者 LGPL 协议引用或公开源码就行。它中的编解码库也是 VLC 播放器所使用的核心编解码库,B 站(Bilibili)开源的 ijkplayer 、著名的 MPlayer 等基本全部主流播放器也都是基于 FFMPEG 开发的。
libavcodec/allcodecs.c
文件中的 avcodec_register_all()
函数用来注册全部的编解码器(包括硬件加速、视频、音频、PCM、DPCM、ADPCM、字幕、文本、外部库、解析器)。
libavformat/allformats.c
文件中的 av_register_all()
函数中调用了 avcodec_register_all()
注册全部的编解码器并注册了全部 muxer 和 demuxer。
所以使用 FFMPEG 通常都要先调用 av_register_all()
。
要读取一个媒体文件,可使用 libavformat/utils.c
文件中的 avformat_open_input()
函数:
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options) 复制代码
ps
包含了媒体相关的基本全部数据,随后函数中调用的 libavformat/options.c
文件中的 avformat_alloc_context()
函数会为它分配空间,而 avformat_alloc_context()
中会调用 avformat_get_context_defaults()
给 s->io_open
设置默认值 io_open_default()
函数。
filename
是想要读取的媒体文件的路径表示,能够是本地或者网络的。
fmt
是自定义的读取格式,能够为 NULL
也能够提早经过 av_find_input_format()
函数获取。
options
是特殊操做参数,如设置 timeout
参数的值。
avformat_open_input()
中会调用 init_input()
函数打开输入文件并尽量地解析出文件格式:
static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options) 复制代码
init_input()
中的关键代码是:
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;
复制代码
而前面说的 s->io_open
默认指向的 libavformat/option.c
文件中的 io_open_default()
函数会调用 libavformat/aviobuf.c
文件中的 ffio_open_whitelist()
函数。
ffio_open_whitelist()
函数会先调用 libavformat/avio.c
文件中的 ffurl_open_whitelist()
函数初始化 URLContext
,再调用 libavformat/aviobuf.c
文件中的 ffio_fdopen()
函数根据 URLContext
的真正类型(如 HTTPContext
)初始化 AVIOContext
,这个 AVIOContext
就是常见的 s->pb
,也就是说从这时开始 pb
已经被初始化了。
ffurl_open_whitelist()
函数中会先调用 ffurl_alloc()
函数找到协议真正类型并根据类型为 URLContext
分配空间,再调用 ffurl_connect()
函数打开媒体文件。
ffurl_connect()
函数中的主要调用是这样的:
err =
uc->prot->url_open2 ? uc->prot->url_open2(uc,
uc->filename,
uc->flags,
options) :
uc->prot->url_open(uc, uc->filename, uc->flags);
复制代码
而位于 libavformat/http.c
文件中的 HTTP 协议 ff_http_protocol
的 url_open2
指向了 http_open()
函数,http_open()
中经过 HTTPContext
中的 AVApplicationContext
能够跟上层进行通信,好比告诉上层正在进行 HTTP 请求,但主要调用的 http_open_cnx()
函数调用了 http_open_cnx_internal()
。
http_open_cnx_internal()
中先是对视频 URL 进行分析,好比若是使用了代理那么还要从新组装 URL 以免将一些信息暴露给代理服务器,若是是 HTTPS 那么底层协议就是 TLS 不然底层协议就是 TCP,而后调用 ffurl_open_whitelist()
进行底层协议的处理(如 DNS 解析,TCP 握手创建 Socket 链接)。而后调用 http_connect()
函数进行 HTTP 请求,固然请求前要给 Header 设置默认值而且添加用户自定义的 Header,而后调用 libavformat/avio.c
文件中的 ffurl_write()
函数发送请求数据,它调用底层协议的 url_write
,而位于 libavformat/tcp.c
文件中的 TCP 协议 ff_tcp_protocol
的 url_write
指向了 tcp_write()
函数,tcp_write()
主要是调用系统函数 send()
发送数据(tcp_read
调用系统函数 recv()
)。最后,在发送完数据后会调用 http_read_header()
函数读取响应报文的 Header,而 http_read_header()
中有个死循环,就是不停地 http_get_line()
和 process_line()
直到全部 Header 数据处理完毕,http_get_line()
内部其实也是调用了 ffurl_read()
(跟 ffurl_write()
逻辑相似)。
至此,若是 avformat_open_input()
返回了大于等于零的数,就算是第一次拿到了媒体文件的数据,播放器就能够向上层发一个 FFP_MSG_OPEN_INPUT
的消息表示成功打开了输入流。
打开输入流并必定能精确地知道媒体流实际的时长、帧率等信息,通常状况下还须要调用 libavformat/utils.c
文件中的 avformat_find_stream_info()
函数对输入流进行探测分析:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options) 复制代码
因为读取一部分媒体数据进行分析的过程仍是很是耗时的,因此须要一个时间限制,这个时间限制不能过短以免成功率过低。max_analyze_duration
若是不指定那么默认是 5 * AV_TIME_BASE
(时间都是基于时基的,而时基 AV_TIME_BASE
是 1000000
),对于 mpeg
或 mpegts
格式的视频流 max_stream_analyze_duration = 90 * AV_TIME_BASE
。
对于媒体中的全部流(包括视频流、音频流、字幕流),先根据以前的 codec_id
调用 find_probe_decoder()
函数寻找合适的解码器,再调用 libavcodec/utils.c
文件中的 avcodec_open2()
函数打开解码器,再调用 read_frame_internal()
函数读取一个完整的 AVPacket
,再调用 try_decode_frame()
函数尝试解码 packet。
通常媒体流中都会包括 AVMEDIA_TYPE_VIDEO
、AVMEDIA_TYPE_AUDIO
和 AVMEDIA_TYPE_SUBTITLE
等媒体类型的流,能够经过 libavformat/utils.c
文件中的 av_find_best_stream()
函数获取他们的索引。
根据各个媒体流的索引就能够打开各个媒体流了,首先调用 libavcodec/utils.c
文件中的 avcodec_find_decoder()
函数找到该媒体流的解码器,而后调用 libavcodec/options.c
文件中的 avcodec_alloc_context3()
为解码器分配空间,而后调用 libavcodec/utils.c
文件中的 avcodec_parameters_to_context()
为解码器复制上下文参数,而后调用 libavcodec/utils.c
文件中的 avcodec_open2()
打开解码器,而后调用 libavutil/frame.c
文件中的 av_frame_alloc()
为 AVFrame
分配空间,而后调用 libavutil/imgutils.c
文件中的 av_image_get_buffer_size()
获取须要的缓冲区大小并为其分配空间,而后调用 libavcodec/avpacket.c
文件中的 av_init_packet()
对 AVPacket
进行初始化。
经过 libavformat/utils.c
文件中的 av_read_frame()
函数就能够读取完整的一帧数据了:
do {
if (!end_of_stream)
if (av_read_frame(fmt_ctx, &pkt) < 0)
end_of_stream = 1;
if (end_of_stream) {
pkt.data = NULL;
pkt.size = 0;
}
if (pkt.stream_index == video_stream || end_of_stream) {
got_frame = 0;
if (pkt.pts == AV_NOPTS_VALUE)
pkt.pts = pkt.dts = i;
result = avcodec_decode_video2(ctx, fr, &got_frame, &pkt);
if (result < 0) {
av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
return result;
}
if (got_frame) {
number_of_written_bytes = av_image_copy_to_buffer(byte_buffer, byte_buffer_size,
(const uint8_t* const *)fr->data, (const int*) fr->linesize,
ctx->pix_fmt, ctx->width, ctx->height, 1);
if (number_of_written_bytes < 0) {
av_log(NULL, AV_LOG_ERROR, "Can't copy image to buffer\n");
return number_of_written_bytes;
}
printf("%d, %10"PRId64", %10"PRId64", %8"PRId64", %8d, 0x%08lx\n", video_stream,
fr->pts, fr->pkt_dts, av_frame_get_pkt_duration(fr),
number_of_written_bytes, av_adler32_update(0, (const uint8_t*)byte_buffer, number_of_written_bytes));
}
av_packet_unref(&pkt);
av_init_packet(&pkt);
}
i++;
} while (!end_of_stream || got_frame);
复制代码
若是编译过程当中出现 linux-perf
相关文件未找到的错误能够在编译脚本文件中添加下面这一行以禁用相关调试功能:
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-linux-perf"
复制代码
若是想支持 webm 格式视频的播放须要修改编译脚本,添加 decoder,demuxer,parser 对相关格式的支持:
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=opus"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6a"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_cuvid"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_mediacodec"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_qsv"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vorbis"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=flac"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=theora"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=zlib"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=matroska"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=ogg"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vp8"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vp9"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vorbis"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=opus"
复制代码
若是想支持分段视频(ffconcat
协议),首先须要修改编译脚本以支持拼接协议:
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=concat"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=concat"
复制代码
而后在 Java 层将 ffconcat
协议加入白名单并容许访问不安全的路径:
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "safe", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "protocol_whitelist", "ffconcat,file,http,https");
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "protocol_whitelist", "concat,http,tcp,https,tls,file");
复制代码
ijkplayer k0.8.8 版本, 支持常见格式的 lite 版本,支持 HTTPS 协议的 .so 文件的编译命令以下:
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android
cd ijkplayer-android
git checkout -B latest k0.8.8
cd config
rm module.sh
ln -s module-lite.sh module.sh
cd ..
./init-android.sh
./init-android-openssl.sh
cd android/contrib
./compile-openssl.sh clean
./compile-openssl.sh all
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
cd ..
./compile-ijk.sh clean
./compile-ijk.sh all
复制代码
也能够简化成一个命令:
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android && cd ijkplayer-android && git checkout -B latest k0.8.8 && cd config && rm module.sh && ln -s module-lite.sh module.sh && cd .. && ./init-android.sh && ./init-android-openssl.sh && cd android/contrib && ./compile-openssl.sh clean && ./compile-openssl.sh all && ./compile-ffmpeg.sh clean && ./compile-ffmpeg.sh all && cd .. && ./compile-ijk.sh clean && ./compile-ijk.sh all
复制代码
生成的 libijkffmpeg.so
,libijkplayer.so
,libijksdl.so
文件目录位于以下目录:
ijkplayer-android/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a/libijkffmpeg.so
复制代码