实验平台:openSUSE Leap 42.3
FFmpeg版本:4.1
SDL版本:2.0.9
基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文件解码和调用SDL显示两大部分。详细流程可参考代码注释。
本篇实验笔记主要参考如下两篇文章:
[1]. 最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)
[2]. An ffmpeg and SDL Tutorial
1. 播放器基本原理
下图及解释内容引用自“雷霄骅,视音频编解码技术零基础学习方法”,
因原图太小,看不太清楚,故重新制作了一张图片。
解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,
例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括
对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音
频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。
解封装
将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,
MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。
例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。
解码
将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,
视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,
压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的
音频抽样数据,例如PCM数据。
视音频同步
根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显
卡和声卡播放出来。
2. 源码清单
/*******************************************************************************
*
ffplayer.c
*
* history:
* 2018-11-27 - [lei]
Create file: a simplest
ffmpeg player
* 2018-11-29 - [lei]
Refresh decoding thread with SDL event
*
* details:
* A simple ffmpeg
player.
*
* refrence:
* 1. https://blog.csdn.net/leixiaohua1020/article/details/38868499
* 2. #tutorial01.html
* 3. #tutorial02.html
*******************************************************************************/
#include <stdio.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_video.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_rect.h>
#define SDL_USEREVENT_REFRESH (SDL_USEREVENT + 1)
static bool s_playing_exit = false;
static bool s_playing_pause = false;
// 每40ms发送一个解码刷新事件,使解码器以25FPS的帧率工作
int sdl_thread_handle_refreshing(void *opaque)
{
SDL_Event sdl_event;
while (!s_playing_exit)
{
if (!s_playing_pause)
{
sdl_event.type = SDL_USEREVENT_REFRESH;
SDL_PushEvent(&sdl_event);
}
SDL_Delay(40);
}
return 0;
}
int main(int argc, char *argv[])
{
// Initalizing these to NULL prevents segfaults!
AVFormatContext* p_fmt_ctx = NULL;
AVCodecContext*
p_codec_ctx = NULL;
AVCodecParameters* p_codec_par = NULL;
AVCodec*
p_codec = NULL;
AVFrame*
p_frm_raw = NULL;
// 帧,由包解码得到原始帧
AVFrame*
p_frm_yuv = NULL;
// 帧,由原始帧色彩转换得到
AVPacket*
p_packet = NULL;
// 包,从流中读出的一段数据
struct SwsContext* sws_ctx = NULL;
int
buf_size;
uint8_t*
buffer = NULL;
int
i;
int
v_idx;
int
ret;
int
res;
SDL_Window*
screen;
SDL_Renderer*
sdl_renderer;
SDL_Texture*
sdl_texture;
SDL_Rect
sdl_rect;
SDL_Thread*
sdl_thread;
SDL_Event
sdl_event;
res = 0;
if (argc < 2)
{
printf("Please provide a movie file\n");
return -1;
}
// 初始化libavformat(所有格式),注册所有复用器/解复用器
// av_register_all(); // 已被申明为过时的,直接不再使用即可
// A1. 打开视频文件:读取文件头,将文件格式信息存储在"fmt context"中
ret = avformat_open_input(&p_fmt_ctx, argv[1], NULL, NULL);
if (ret != 0)
{
printf("avformat_open_input() failed %d\n", ret);
res = -1;
goto exit0;
}
// A2. 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入pFormatCtx->streams
//
p_fmt_ctx->streams是一个指针数组,数组大小是pFormatCtx->nb_streams
ret = avformat_find_stream_info(p_fmt_ctx, NULL);
if (ret < 0)
{
printf("avformat_find_stream_info() failed %d\n", ret);
res = -1;
goto exit1;
}
// 将文件相关信息打印在标准错误设备上
av_dump_format(p_fmt_ctx, 0, argv[1], 0);
// A3. 查找第一个视频流
v_idx = -1;
for (i=0; i<p_fmt_ctx->nb_streams; i++)
{
if (p_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
v_idx = i;
printf("Find a video stream, index %d\n", v_idx);
break;
}
}
if (v_idx == -1)
{
printf("Cann't find a video stream\n");
res = -1;
goto exit1;
}
// A5. 为视频流构建解码器AVCodecContext
// A5.1 获取解码器参数AVCodecParameters
p_codec_par = p_fmt_ctx->streams[v_idx]->codecpar;
// A5.2 获取解码器
p_codec = avcodec_find_decoder(p_codec_par->codec_id);
if (p_codec == NULL)
{
printf("Cann't find codec!\n");
res = -1;
goto exit1;
}
// A5.3 构建解码器AVCodecContext
// A5.3.1 p_codec_ctx初始化:分配结构体,使用p_codec初始化相应成员为默认值
p_codec_ctx = avcodec_alloc_context3(p_codec);
// A5.3.2 p_codec_ctx初始化:p_codec_par ==> p_codec_ctx,初始化相应成员
ret = avcodec_parameters_to_context(p_codec_ctx, p_codec_par);
if (ret < 0)
{
printf("avcodec_parameters_to_context() failed %d\n", ret);
res = -1;
goto exit2;
}
// A5.3.3 p_codec_ctx初始化:使用p_codec初始化p_codec_ctx,初始化完成
ret = avcodec_open2(p_codec_ctx, p_codec, NULL);
if (ret < 0)
{
printf("avcodec_open2() failed %d\n", ret);
res = -1;
goto exit2;
}
// A6. 分配AVFrame
// A6.1 分配AVFrame结构,注意并不分配data buffer(即AVFrame.*data[])
p_frm_raw = av_frame_alloc();
if (p_frm_raw == NULL)
{
printf("av_frame_alloc() for p_frm_raw failed\n");
res = -1;
goto exit2;
}
p_frm_yuv = av_frame_alloc();
if (p_frm_yuv == NULL)
{
printf("av_frame_alloc() for p_frm_raw failed\n");
res = -1;
goto exit3;
}
// A6.2 为AVFrame.*data[]手工分配缓冲区,用于存储sws_scale()中目的帧视频数据
//
p_frm_raw的data_buffer由av_read_frame()分配,因此不需手工分配
//
p_frm_yuv的data_buffer无处分配,因此在此处手工分配
buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
p_codec_ctx->width,
p_codec_ctx->height,
1
);
// buffer将作为p_frm_yuv的视频数据缓冲区
buffer = (uint8_t *)av_malloc(buf_size);
if (buffer == NULL)
{
printf("av_malloc() for buffer failed\n");
res = -1;
goto exit4;
}
// 使用给定参数设定p_frm_yuv->data和p_frm_yuv->linesize
ret = av_image_fill_arrays(p_frm_yuv->data,
// dst data[]
p_frm_yuv->linesize, // dst linesize[]
buffer,
// src buffer
AV_PIX_FMT_YUV420P, // pixel format
p_codec_ctx->width, // width
p_codec_ctx->height, // height
1
// align
);
if (ret < 0)
{
printf("av_image_fill_arrays() failed %d\n", ret);
res = -1;
goto exit5;
}
// A7. 初始化SWS context,用于后续图像转换
sws_ctx = sws_getContext(p_codec_ctx->width, // src width
p_codec_ctx->height, // src height
p_codec_ctx->pix_fmt, // src format
p_codec_ctx->width, // dst width
p_codec_ctx->height, // dst height
AV_PIX_FMT_YUV420P, // dst format
SWS_BICUBIC,
// flags
NULL,
// src filter
NULL,
// dst filter
NULL
// param
);
if (sws_ctx == NULL)
{
printf("sws_getContext() failed\n");
res = -1;
goto exit6;
}
// B1. 初始化SDL子系统:缺省(事件处理、文件IO、线程)、视频、音频、定时器
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))
{
printf("SDL_Init() failed: %s\n", SDL_GetError());
res = -1;
goto exit6;
}
// B2. 创建SDL窗口,SDL 2.0支持多窗口
//
SDL_Window即运行程序后弹出的视频窗口,同SDL 1.x中的SDL_Surface
screen = SDL_CreateWindow("Simplest ffmpeg player's Window",
SDL_WINDOWPOS_UNDEFINED,// 不关心窗口X坐标
SDL_WINDOWPOS_UNDEFINED,// 不关心窗口Y坐标
p_codec_ctx->width,
p_codec_ctx->height,
SDL_WINDOW_OPENGL
);
if (screen == NULL)
{
printf("SDL_CreateWindow() failed: %s\n", SDL_GetError());
res = -1;
goto exit7;
}
// B3. 创建SDL_Renderer
//
SDL_Renderer:渲染器
sdl_renderer = SDL_CreateRenderer(screen, -1, 0);
if (sdl_renderer == NULL)
{
printf("SDL_CreateRenderer() failed: %s\n", SDL_GetError());
res = -1;
goto exit7;
}
// B4. 创建SDL_Texture
//
一个SDL_Texture对应一帧YUV数据,同SDL 1.x中的SDL_Overlay
sdl_texture = SDL_CreateTexture(sdl_renderer,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
p_codec_ctx->width,
p_codec_ctx->height);
if (sdl_texture == NULL)
{
printf("SDL_CreateTexture() failed: %s\n", SDL_GetError());
res = -1;
goto exit7;
}
sdl_rect.x = 0;
sdl_rect.y = 0;
sdl_rect.w = p_codec_ctx->width;
sdl_rect.h = p_codec_ctx->height;
p_packet = (AVPacket *)av_malloc(sizeof(AVPacket));
if (p_packet == NULL)
{
printf("SDL_CreateThread() failed: %s\n", SDL_GetError());
res = -1;
goto exit7;
}
// B5. 创建定时刷新事件线程,按照预设帧率产生刷新事件
sdl_thread = SDL_CreateThread(sdl_thread_handle_refreshing, NULL, NULL);
if (sdl_thread == NULL)
{
printf("SDL_CreateThread() failed: %s\n", SDL_GetError());
res = -1;
goto exit8;
}
while (1)
{
// B6. 等待刷新事件
SDL_WaitEvent(&sdl_event);
if (sdl_event.type == SDL_USEREVENT_REFRESH)
{
// A8. 从视频文件中读取一个packet
//
packet可能是视频帧、音频帧或其他数据,解码器只会解码视频帧或音频帧,非音视频数据并不会被
//
扔掉、从而能向解码器提供尽可能多的信息
//
对于视频来说,一个packet只包含一个frame
//
对于音频来说,若是帧长固定的格式则一个packet可包含整数个frame,
//
若是帧长可变的格式则一个packet只包含一个frame
while (av_read_frame(p_fmt_ctx, p_packet) == 0)
{
if (p_packet->stream_index == v_idx) // 取到一帧视频帧,则退出
{
break;
}
}
// A9. 视频解码:packet ==> frame
// A9.1 向解码器喂数据,一个packet可能是一个视频帧或多个音频帧,此处音频帧已被上一句滤掉
ret = avcodec_send_packet(p_codec_ctx, p_packet);
if (ret != 0)
{
printf("avcodec_send_packet() failed %d\n", ret);
res = -1;
goto exit8;
}
// A9.2 接收解码器输出的数据,此处只处理视频帧,每次接收一个packet,将之解码得到一个frame
ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw);
if (ret != 0)
{
if (ret == AVERROR_EOF)
{
printf("avcodec_receive_frame(): the decoder has been fully flushed\n");
}
else if (ret == AVERROR(EAGAIN))
{
printf("avcodec_receive_frame(): output is not available in this state - "
"user must try to send new input\n");
}
else if (ret == AVERROR(EINVAL))
{
printf("avcodec_receive_frame(): codec not opened, or it is an encoder\n");
}
else
{
printf("avcodec_receive_frame(): legitimate decoding errors\n");
}
res = -1;
goto exit8;
}
// A10. 图像转换:p_frm_raw->data ==> p_frm_yuv->data
// 将源图像中一片连续的区域经过处理后更新到目标图像对应区域,处理的图像区域必须逐行连续
// plane: 如YUV有Y、U、V三个plane,RGB有R、G、B三个plane
// slice: 图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部
// stride/pitch: 一行图像所占的空间字节数,Stride = BytesPerPixel * Width,4字节对齐
// AVFrame.*data[]: 每个数组元素指向对应plane
// AVFrame.linesize[]: 每个数组元素表示对应plane中一行图像所占的空间字节数
sws_scale(sws_ctx,
// sws context
(const uint8_t *const *)p_frm_raw->data, // src slice
p_frm_raw->linesize,
// src stride
0,
// src slice y
p_codec_ctx->height,
// src slice height
p_frm_yuv->data,
// dst planes
p_frm_yuv->linesize
// dst strides
);
// B7. 使用新的YUV像素数据更新SDL_Rect
SDL_UpdateYUVTexture(sdl_texture,
// sdl texture
&sdl_rect,
// sdl rect
p_frm_yuv->data[0],
// y plane
p_frm_yuv->linesize[0],
// y pitch
p_frm_yuv->data[1],
// u plane
p_frm_yuv->linesize[1],
// u pitch
p_frm_yuv->data[2],
// v plane
p_frm_yuv->linesize[2]
// v pitch
);
// B8. 使用特定颜色清空当前渲染目标
SDL_RenderClear(sdl_renderer);
// B9. 使用部分图像数据(texture)更新当前渲染目标
SDL_RenderCopy(sdl_renderer,
// sdl renderer
sdl_texture,
// sdl texture
NULL,
// src rect, if NULL copy texture
&sdl_rect
// dst rect
);
// B10. 执行渲染,更新屏幕显示
SDL_RenderPresent(sdl_renderer);
av_packet_unref(p_packet);
}
else if (sdl_event.type == SDL_KEYDOWN)
{
printf("SDL event KEYDOWN\n");
if (sdl_event.key.keysym.sym == SDLK_SPACE)
{
// 用户按空格键,暂停/继续状态切换
printf("player %s\n", s_playing_pause ? "pause" : "continue");
s_playing_pause = !s_playing_pause;
}
}
else if (sdl_event.type == SDL_QUIT)
{
// 用户按下关闭窗口按钮
printf("SDL event QUIT\n");
s_playing_exit = true;
break;
}
else
{
// printf("Ignore SDL event 0x%04X\n", sdl_event.type);
}
}
exit8:
SDL_Quit();
exit7:
av_packet_unref(p_packet);
exit6:
sws_freeContext(sws_ctx);
exit5:
av_free(buffer);
exit4:
av_frame_free(&p_frm_yuv);
exit3:
av_frame_free(&p_frm_raw);
exit2:
avcodec_close(p_codec_ctx);
exit1:
avformat_close_input(&p_fmt_ctx);
exit0:
return res;
}
2.1 流程简述