学习雷神的博客,向雷神致敬~
看了雷神的小学期视频课,在Github上下载了simplest_ffmpeg_player的代码,为代码加上了注释,作为留存。
2019.07.24
simple_ffmpeg_play_sdl2r.cpp注释
simple_ffmpeg_player_su.cpp注释
simple_ffmpeg_play_sdl2r.cpp是单纯的SDL播放器,simple_ffmpeg_player_su是将FFmpeg读取视频文件数据然后交给SDL播放器播放的完整代码。至此,雷神小学期项目simple_ffmpeg_player模块就结束了~
/**
* 最简单的SDL2播放视频的例子(SDL2播放RGB/YUV)
* Simplest Video Play SDL2 (SDL2 play RGB/YUV)
*
* 雷霄骅 Lei Xiaohua
* [email protected]
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
* 本程序使用SDL2播放RGB/YUV视频像素数据。SDL实际上是对底层绘图
* API(Direct3D,OpenGL)的封装,使用起来明显简单于直接调用底层
* API。
*
* 函数调用步骤如下:
*
* [初始化]
* SDL_Init(): 初始化SDL。
* SDL_CreateWindow(): 创建窗口(Window)。
* SDL_CreateRenderer(): 基于窗口创建渲染器(Render)。
* SDL_CreateTexture(): 创建纹理(Texture)。
*
* [循环渲染数据]
* SDL_UpdateTexture(): 设置纹理的数据。
* SDL_RenderCopy(): 纹理复制给渲染器。
* SDL_RenderPresent(): 显示。
*
* This software plays RGB/YUV raw video data using SDL2.
* SDL is a wrapper of low-level API (Direct3D, OpenGL).
* Use SDL is much easier than directly call these low-level API.
*
* The process is shown as follows:
*
* [Init]
* SDL_Init(): Init SDL.
* SDL_CreateWindow(): Create a Window.
* SDL_CreateRenderer(): Create a Render.
* SDL_CreateTexture(): Create a Texture.
*
* [Loop to Render data]
* SDL_UpdateTexture(): Set Texture's data.
* SDL_RenderCopy(): Copy Texture to Render.
* SDL_RenderPresent(): Show.
*/
#include
extern "C"
{
#include "sdl/SDL.h"
};
const int bpp=12;
int screen_w=500,screen_h=500;
const int pixel_w=320,pixel_h=180;
unsigned char buffer[pixel_w*pixel_h*bpp/8];
//Refresh Event
#define REFRESH_EVENT (SDL_USEREVENT + 1)
#define BREAK_EVENT (SDL_USEREVENT + 2)
int thread_exit=0;
/**
* 每隔40ms发送一次消息,eventType为REFRESH_EVENT
* 退出循环后,会发送一次eventType为BREAK_EVENT的事件
*
* thread_exit:退出
*/
int refresh_video(void *opaque){
thread_exit=0;
while (!thread_exit) {
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);
SDL_Delay(40);
}
thread_exit=0;
//Break
SDL_Event event;
event.type = BREAK_EVENT;
SDL_PushEvent(&event);
return 0;
}
int main(int argc, char* argv[])
{
if(SDL_Init(SDL_INIT_VIDEO)) {
printf( "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
SDL_Window *screen;
//SDL 2.0 Support for multiple windows
// 创建窗口SDL_CreateWindow(SDL_WINDOW_RESIZABLE:可调节大小)
screen = SDL_CreateWindow("Simplest Video Play SDL2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h,SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
if(!screen) {
printf("SDL: could not create window - exiting:%s\n",SDL_GetError());
return -1;
}
// 创建渲染器SDL_Renderer
SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
Uint32 pixformat=0;
//IYUV: Y + U + V (3 planes)
//YV12: Y + V + U (3 planes)
pixformat= SDL_PIXELFORMAT_IYUV;
// 创建纹理SDL_Texture
SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer,pixformat, SDL_TEXTUREACCESS_STREAMING,pixel_w,pixel_h);
FILE *fp=NULL;
fp=fopen("test_yuv420p_320x180.yuv","rb+");
if(fp==NULL){
printf("cannot open this file\n");
return -1;
}
// 视频显示区域
SDL_Rect sdlRect;
// 发送读取视频事件的线程
SDL_Thread *refresh_thread = SDL_CreateThread(refresh_video,NULL,NULL);
SDL_Event event;
while(1){
//Wait
SDL_WaitEvent(&event);
if(event.type==REFRESH_EVENT){
/**
* fread()函数用于从文件流中读取数据,其原型为:
* size_t fread(void *buffer, size_t size, size_t count, FILE * stream);
*
* 【参数】buffer为接收数据的地址,size为一个单元的大小,count为单元个数,stream为文件流。
* fread()函数每次从stream中最多读取count个单元,每个单元大小为size个字节,将读取的数据放到buffer;文件流的位置指针后移 size * count 字节。
*
* 【返回值】返回实际读取的单元个数。如果小于count,则可能文件结束或读取出错;可以用ferror()检测是否读取出错,用feof()函数检测是否到达文件结尾。如果size或count为0,则返回0。
* 与fread()相对应的函数为fwrite(),fread() 和 fwrite() 一般用于二进制文件的输入输出
*/
if (fread(buffer, 1, pixel_w*pixel_h*bpp/8, fp) != pixel_w*pixel_h*bpp/8){
// Loop
// SEEK_SET – 移动到文件开始处
// 更多信息参见https://fresh2refresh.com/c-programming/c-file-handling/fseek-seek_set-seek_cur-seek_end-functions-c/
fseek(fp, 0, SEEK_SET);
fread(buffer, 1, pixel_w*pixel_h*bpp/8, fp);
}
// 设置纹理数据
SDL_UpdateTexture( sdlTexture, NULL, buffer, pixel_w);
//FIX: If window is resize
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = screen_w;
sdlRect.h = screen_h;
// 清理渲染器
SDL_RenderClear( sdlRenderer );
// 将纹理数据copy到渲染器
SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect);
// 显示
SDL_RenderPresent( sdlRenderer );
}else if(event.type==SDL_WINDOWEVENT){
//If Resize
SDL_GetWindowSize(screen,&screen_w,&screen_h);
}else if(event.type==SDL_QUIT){
thread_exit=1;
}else if(event.type==BREAK_EVENT){
break;
}
}
SDL_Quit();
return 0;
}
/**
* 最简单的基于FFmpeg的视频播放器2(SDL升级版)
* Simplest FFmpeg Player 2(SDL Update)
*
* 雷霄骅 Lei Xiaohua
* [email protected]
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
* 第2版使用SDL2.0取代了第一版中的SDL1.2
* Version 2 use SDL 2.0 instead of SDL 1.2 in version 1.
*
* 本程序实现了视频文件的解码和显示(支持HEVC,H.264,MPEG2等)。
* 是最简单的FFmpeg视频解码方面的教程。
* 通过学习本例子可以了解FFmpeg的解码流程。
* 本版本中使用SDL消息机制刷新视频画面。
* This software is a simplest video player based on FFmpeg.
* Suitable for beginner of FFmpeg.
*
* 备注:
* 标准版在播放视频的时候,画面显示使用延时40ms的方式。这么做有两个后果:
* (1)SDL弹出的窗口无法移动,一直显示是忙碌状态
* (2)画面显示并不是严格的40ms一帧,因为还没有考虑解码的时间。
* SU(SDL Update)版在视频解码的过程中,不再使用延时40ms的方式,而是创建了
* 一个线程,每隔40ms发送一个自定义的消息,告知主函数进行解码显示。这样做之后:
* (1)SDL弹出的窗口可以移动了
* (2)画面显示是严格的40ms一帧
* Remark:
* Standard Version use's SDL_Delay() to control video's frame rate, it has 2
* disadvantages:
* (1)SDL's Screen can't be moved and always "Busy".
* (2)Frame rate can't be accurate because it doesn't consider the time consumed
* by avcodec_decode_video2()
* SU(SDL Update)Version solved 2 problems above. It create a thread to send SDL
* Event every 40ms to tell the main loop to decode and show video frames.
*/
#include
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "SDL2/SDL.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include
#include
#include
#include
#include
#ifdef __cplusplus
};
#endif
#endif
//Refresh Event
#define SFM_REFRESH_EVENT (SDL_USEREVENT + 1)
#define SFM_BREAK_EVENT (SDL_USEREVENT + 2)
int thread_exit=0;
int thread_pause=0;
/**
* 每隔40ms发送一次消息,eventType为SFM_REFRESH_EVENT
*
* thread_pause:暂停
* thread_exit:退出
*/
int sfp_refresh_thread(void *opaque){
thread_exit=0;
thread_pause=0;
while (!thread_exit) {
if(!thread_pause){
SDL_Event event;
event.type = SFM_REFRESH_EVENT;
SDL_PushEvent(&event);
}
SDL_Delay(40);
}
thread_exit=0;
thread_pause=0;
//Break
SDL_Event event;
event.type = SFM_BREAK_EVENT;
SDL_PushEvent(&event);
return 0;
}
int main(int argc, char* argv[])
{
// 封装格式上下文的结构体,也是统领全局的结构体,保存了视频文件封装格式的相关信息
AVFormatContext *pFormatCtx;
// 视频流在文件中的位置
int i, videoindex;
// 编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodecContext *pCodecCtx;
// 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
AVCodec *pCodec;
// 存储一帧解码后像素(采样)数据
AVFrame *pFrame,*pFrameYUV;
// 存储图像数据
unsigned char *out_buffer;
// 存储一帧压缩编码数据
AVPacket *packet;
// 是否获取到数据的返回值
int ret, got_picture;
//------------SDL----------------
int screen_w,screen_h;
// 窗口
SDL_Window *screen;
// 渲染器
SDL_Renderer* sdlRenderer;
// 纹理
SDL_Texture* sdlTexture;
// 一个简单的矩形
SDL_Rect sdlRect;
// 线程
SDL_Thread *video_tid;
// 事件
SDL_Event event;
struct SwsContext *img_convert_ctx;
//char filepath[]="bigbuckbunny_480x272.h265";
char filepath[]="Titanic.ts";
// 注册复用器,编码器等(参考FFmpeg解码流程图)
av_register_all();
// 进行网络组件的全局初始化(详细代码请参考第三篇文章中代码对应位置的描述)
avformat_network_init();
/**
* Allocate an AVFormatContext.
* avformat_free_context() can be used to free the context and everything
* allocated by the framework within it.
* 分配AVFormatContext。
* avformat_free_context()可用于释放上下文以及框架在其中分配的所有内容。
*/
pFormatCtx = avformat_alloc_context();
// 打开多媒体数据并且获得一些相关的信息(参考FFmpeg解码流程图)
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return -1;
}
// 读取一部分视音频数据并且获得一些相关的信息(参考FFmpeg解码流程图)
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return -1;
}
// 每个视频文件中有多个流(视频流、音频流、字幕流等,而且可有多个),循环遍历找到视频流
// 判断方式:AVFormatContext->AVStream->AVCodecContext->codec_type是否为AVMEDIA_TYPE_VIDEO
videoindex=-1;
for(i=0; inb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
// 如果没有视频流,返回
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
// 保存视频流中的AVCodecContext
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
// 用于查找FFmpeg的解码器(参考FFmpeg解码流程图)
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
// 初始化一个视音频编解码器的AVCodecContext(参考FFmpeg解码流程图)
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Could not open codec.\n");
return -1;
}
// 创建AVFrame,用来存放解码后的一帧的数据
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
// av_image_get_buffer_size:返回使用给定参数存储图像所需的数据量的字节大小
out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
// 根据指定的图像参数和提供的数组设置数据指针和线条(data pointers and linesizes)
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,
AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);
//Output Info-----------------------------
printf("---------------- File Information ---------------\n");
av_dump_format(pFormatCtx,0,filepath,0);
printf("-------------------------------------------------\n");
// sws_getContext():初始化一个SwsContext
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
// 初始化SDL系统
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf( "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
//SDL 2.0 Support for multiple windows
// 保存视频的实际宽高,用于下面代码中复用
screen_w = pCodecCtx->width;
screen_h = pCodecCtx->height;
// 创建窗口SDL_CreateWindow
screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h,SDL_WINDOW_OPENGL);
if(!screen) {
printf("SDL: could not create window - exiting:%s\n",SDL_GetError());
return -1;
}
// 创建渲染器SDL_Renderer
sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
//IYUV: Y + U + V (3 planes)
//YV12: Y + V + U (3 planes)
// 创建纹理SDL_Texture
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);
sdlRect.x=0;
sdlRect.y=0;
sdlRect.w=screen_w;
sdlRect.h=screen_h;
// 创建一个AVPacket,用来存放下面循环获取到的未解码帧
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
// 创建线程sfp_refresh_thread,进行视频数据的读取
video_tid = SDL_CreateThread(sfp_refresh_thread,NULL,NULL);
//------------SDL End------------
//Event Loop
for (;;) {
//Wait
SDL_WaitEvent(&event);
if(event.type==SFM_REFRESH_EVENT){
// sfp_refresh_thread中定义的事件,视频播放的事件
while(1){
// 读取到的数据为空,则停止播放
if(av_read_frame(pFormatCtx, packet)<0)
thread_exit=1;
// 循环找到视频流
if(packet->stream_index==videoindex)
break;
}
// 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
// 详细讲解请查看我的源码系列文章:https://blog.csdn.net/asd501823206/article/details/97013677
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0){
printf("Decode Error.\n");
return -1;
}
if(got_picture){
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
//SDL---------------------------
// 设置纹理数据
SDL_UpdateTexture( sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0] );
// 清理渲染器
SDL_RenderClear( sdlRenderer );
//SDL_RenderCopy( sdlRenderer, sdlTexture, &sdlRect, &sdlRect );
// 将纹理数据copy到渲染器
SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, NULL);
// 显示
SDL_RenderPresent( sdlRenderer );
//SDL End-----------------------
}
av_free_packet(packet);
}else if(event.type==SDL_KEYDOWN){
//Pause
if(event.key.keysym.sym==SDLK_SPACE)
// 空格键
thread_pause=!thread_pause;
}else if(event.type==SDL_QUIT){
// 系统event,点击关闭时返回该event
// thread_exit置为1,退出监听事件的循环
thread_exit=1;
}else if(event.type==SFM_BREAK_EVENT){
break;
}
}
sws_freeContext(img_convert_ctx);
SDL_Quit();
//--------------
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}