视频播放器

视频播放器 (1)

视频编码

视频中图像编码的主要依赖:

  • 帧内的图像压缩编码
  • 帧之间的数据压缩编码

帧内的图像压缩很好理解就是对于图片中相同像素的压缩。

帧之间的数据压缩就主要依赖连续帧之间具有极高的相似性的原理。如一段视频有十几秒都是不动的或者有50%的画面都是不变的,那么这块存储就可以节省了。

视频压缩的核心原理就是将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。一组GOP中的帧分为三类(有些视频只有两类,没有B帧),分别是:

I P B

I帧:是自带全部信息的独立帧,是最完整的画面(占用的空间最大),无需参考其它图像便可独立进行解码。一个GOP视频序列中的第一个帧,始终都是I帧。

P帧:“帧间预测编码帧”,需要参考前面的I帧和/或P帧的不同部分,才能进行编码。P帧对前面的P和I参考帧有依赖性,也就是视频中运动的部分。P帧压缩率比较高,占用的空间较小。

img

B帧:“双向预测编码帧”,以前帧后帧作为参考帧。不仅参考前面,还参考后面的帧,所以,它的压缩率最高,可以达到200:1。不过,因为依赖后面的帧,所以不适合实时传输(例如视频会议)。

img

视频文件封装

image-20240726190803408

总结来说,一个视频文件产生经过了: 1.图像和音频编码 2.将音频视频的编码按一定格式封装于容器中

因此解码的过程其实就是解视频的封装格式和编码格式,将视频还原成一帧帧图像和音频的过程。

传输协议

视频播放目前主要有本地播放,点播播放,和直播播放。本地播放和点播播放就是播放已处理好的有进度信息的视频,只不过本地播放在本地,而点播播放视频存储在远程服务器。直播播放则有边制作边播放的特点。

image-20240726191042530

视频处理

FFMPEG 编解码格式

  • ffmpeg是一个基于C语言的开源的音视频处理软件,目前 PC端中Youtube , iTunes ,腾讯视频,b站,爱奇艺都是使用ffmpeg来进行视频处理的。
  • ffmpeg几乎囊括了所有的视音频编码标准以及传输协议。
  • FFmpeg是软解码(利用CPU计算解码),在客户端中如果使用FFmpeg来解码,手机将面临性能损耗高,耗电量大等问题。
  • Fmpeg已经支持使用MediaCodec和VideoToolBox来进行解码。

https://ffmpeg.org/ffmpeg-formats.html

播放器工作流程

1.解协议(读取文件)

2.解封装

3.视音频分离

4.视音频分别解码

5.视音频同步

6.输出数据解码后的视音频数据

7.渲染图像和播放音频

img

整体架构 android为例

img

播放器状态管理

image-20240726191344248

使用ffmpeg解码视频

初始化FFmpeg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 初始化 FFmpeg 库
av_register_all();

// 打开视频文件
AVFormatContext *pFormatCtx = avformat_alloc_context();
if (avformat_open_input(&pFormatCtx, "video.mp4", NULL, NULL) != 0) {
fprintf(stderr, "Could not open video file\n");
return -1;
}

// 获取视频文件中的流信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
fprintf(stderr, "Could not retrieve stream info from file\n");
return -1;
}

视频流和解码器

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
// 查找视频流
int videoStream = -1;
for (int i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) {
fprintf(stderr, "Could not find a video stream\n");
return -1;
}

// 获取视频流的解码器上下文和解码器
AVCodecParameters *pCodecParams = pFormatCtx->streams[videoStream]->codecpar;
AVCodec *pCodec = avcodec_find_decoder(pCodecParams->codec_id);
if (pCodec == NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecCtx, pCodecParams);

// 打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
return -1;
}

SDL 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化 SDL 库
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}

// 创建 SDL 窗口
SDL_Window *window = SDL_CreateWindow("FFmpeg SDL Video Player",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
pCodecCtx->width, pCodecCtx->height, SDL_WINDOW_OPENGL);
if (!window) {
fprintf(stderr, "SDL: could not create window - %s\n", SDL_GetError());
return -1;
}

// 创建 SDL 渲染器和纹理
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0);
SDL_Texture *texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
pCodecCtx->width, pCodecCtx->height);

渲染视频帧

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
// 分配 AVFrame 并初始化转换上下文
AVFrame *pFrame = av_frame_alloc();
AVFrame *pFrameYUV = av_frame_alloc();
uint8_t *buffer = av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1));
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);

struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);

AVPacket packet;
while (av_read_frame(pFormatCtx, &packet) >= 0) {
if (packet.stream_index == videoStream) {
// 解码视频帧
avcodec_send_packet(pCodecCtx, &packet);
while (avcodec_receive_frame(pCodecCtx, pFrame) >= 0) {
// 转换帧格式为 YUV420P
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);

// 渲染到 SDL 纹理并显示
SDL_UpdateYUVTexture(texture, NULL,
pFrameYUV->data[0], pFrameYUV->linesize[0],
pFrameYUV->data[1], pFrameYUV->linesize[1],
pFrameYUV->data[2], pFrameYUV->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
}
av_packet_unref(&packet);
}