在这里插入代码片
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
音视频开发新手项目FFMPEG+SDL播放器`
雷神的代码,做了些解释,方便大家一起学习
提示:以下是本篇文章正文内容,下面案例可供参考
一、代码如下
/**
- 最简单的基于FFmpeg的视频播放器2(SDL升级版)
- Simplest FFmpeg Player 2(SDL Update)
- 雷霄骅 Lei Xiaohua
- leixiaohua1020@126.com
- 中国传媒大学/数字电视技术
- Communication University of China / Digital TV Technology
- http://blog.csdn.net/leixiaohua1020
- 本程序实现了视频文件的解码和显示(支持HEVC,H.264,MPEG2等)。
- 是最简单的FFmpeg视频解码方面的教程。
- 通过学习本例子可以了解FFmpeg的解码流程。
- 本版本中使用SDL消息机制刷新视频画面。
- This software is a simplest video player based on FFmpeg.
- Suitable for beginner of FFmpeg.
*/
#include <stdio.h>
#define __STDC_CONSTANT_MACROS//是一种解决方法,允许 C++ 程序使用 C99 标准中指定但不在C++ 标准。
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h"
};
//Refresh Event
#define SFM_REFRESH_EVENT (SDL_USEREVENT + 1)//定义一个事件用来刷新
#define SFM_BREAK_EVENT (SDL_USEREVENT + 2)
/*(SDL_USEREVENT + 1) 是一个SDL库中预定义的事件类型的索引。
SDL_USEREVENT 是SDL库为用户自定义事件保留的一个范围,
而 (SDL_USEREVENT + 1) 表示在用户自定义事件范围内向后移动了一个位置,因此它是一个新的用户自定义事件类型的索引。
这样做是为了确保自定义事件类型不会与SDL库内部事件类型冲突。
*/
int thread_exit=0;
int sfp_refresh_thread(void *opaque){//定义一个线程事件用来刷新
thread_exit=0;
while (!thread_exit) {
SDL_Event event;
event.type = SFM_REFRESH_EVENT;//定义一个事件用来刷新
SDL_PushEvent(&event);//SDL_PushEvent函数是SDL库中的一个函数,用于将一个自定义的事件推入事件队列中。
SDL_Delay(40);//让当前线程暂停执行40毫秒,然后再继续执行后续的代码。
}
thread_exit=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;//编码器上下文结构体,保存了视频(音频)编解码相关信息。
AVCodec *pCodec;//每种视频(音频)编解码器
AVFrame *pFrame,*pFrameYUV;//存储一帧解码后像素(采样)数据
uint8_t *out_buffer;//uint8_t 是C/C++语言中的一种数据类型,表示8位的无符号整数,取值范围为0到255。声明了一个缓冲区
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_Window *screen;:声明了一个指向SDL_Window类型的指针变量,通常用于表示应用程序的窗口。在SDL中,SDL_Window是窗口的抽象表示,你可以使用它来创建、管理和控制窗口。
SDL_Renderer* sdlRenderer;:声明了一个指向SDL_Renderer类型的指针变量,用于处理图形渲染相关的操作。SDL_Renderer用于将图形渲染到窗口上,你可以使用它来绘制几何形状、图像等。
SDL_Texture* sdlTexture;:声明了一个指向SDL_Texture类型的指针变量,表示在渲染器上进行绘制的纹理。SDL_Texture是SDL中用于表示图像数据的对象,你可以将图像加载到纹理中,然后在渲染器上进行绘制。
SDL_Rect sdlRect;:声明了一个SDL_Rect类型的变量,用于表示矩形区域的位置和大小。SDL_Rect通常用于指定在窗口或者纹理上进行绘制的位置和大小。*/
SDL_Thread *video_tid;
SDL_Event event;
struct SwsContext *img_convert_ctx;//struct SwsContext 是FFmpeg(或Libav)库中的一个结构体,用于图像转换和缩放的上下文。
char filepath[]="屌丝男士.mov";
av_register_all();//注册所有组件。
avformat_network_init();/*在使用FFmpeg库进行网络相关的操作(比如从网络流媒体中获取数据)之前,
需要调用 avformat_network_init() 函数进行网络初始化。这个函数会初始化FFmpeg库中的网络模块,确保网络相关的功能可以正常使用。*/
pFormatCtx = avformat_alloc_context();//用于分配一个空的 AVFormatContext 结构体,并将其指针赋值给 pFormatCtx。
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){//打开视频文件并解析为AVFormatContext格式,赋值给pFormatCtx,
printf("Couldn't open input stream.\n");//原型为int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
return -1;//fmt:要强制使用的输入格式。通常传入 NULL 表示自动检测输入文件的格式。options:一些附加选项,比如设置超时、设置缓冲区大小等。
}
if(avformat_find_stream_info(pFormatCtx,NULL)<0){//获取视频文件信息。int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
printf("Couldn't find stream information.\n");//提取出流的基本信息,比如编解码器信息、帧率、分辨率等。这些信息会填充到 AVFormatContext 结构体中的相应字段中
return -1;
}
videoindex=-1;
for(i=0; i<pFormatCtx->nb_streams; i++) //nb_streams :输入视频的AVStream 个数,本段函数就是找到视频流的开始编号
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){//AVMEDIA_TYPE_VIDEO 是一个枚举值,表示音视频流的类型之一,具体指的是视频流。
videoindex=i;
break;
/*AVMEDIA_TYPE_AUDIO:音频流
AVMEDIA_TYPE_SUBTITLE:字幕流
AVMEDIA_TYPE_ATTACHMENT:附件流(比如封面、章节等)
AVMEDIA_TYPE_DATA:数据流(比如标签、元数据等)*/
}
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
pCodecCtx=pFormatCtx->streams[videoindex]->codec;//获取视频流的解码上下文并存储在之前定义的编码器上下文结构体
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);//根据编码器上下文结构体信息查找解码器并赋值给pCodec。
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){//打开解码器。使用给定的解码器(pCodec)初始化并打开解码器上下文(pCodecCtx)
printf("Could not open codec.\n");
return -1;
}
pFrame=av_frame_alloc();//分配内存
pFrameYUV=av_frame_alloc();
out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));//计算图像大小分配内存
//out_buffer = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);//用于将一组图像数据填充到一个 AVPicture 结构体中。
/*typedef struct AVPicture {
uint8_t *data[AV_NUM_DATA_POINTERS];
int linesize[AV_NUM_DATA_POINTERS];
} AVPicture;
data:用于存储图像数据的指针数组,通常有三个元素,分别用于存储 Y、U 和 V 三个分量的数据指针。
linesize:用于存储图像数据每行的大小的数组,通常也有三个元素,分别对应于三个分量的每行大小。
*/
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); //用于创建一个图像转换上下文(SwsContext),该上下文用于执行图像格式转换和缩放操作
/*
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
srcW:源图像的宽度。
srcH:源图像的高度。
srcFormat:源图像的像素格式。
dstW:目标图像的宽度。
dstH:目标图像的高度。
dstFormat:目标图像的像素格式。
flags:转换标志,用于指定转换时的一些选项。
srcFilter:源图像的滤波器。
dstFilter:目标图像的滤波器。
param:保留参数,暂时没有使用。
在这行代码中,sws_getContext 函数被用于创建一个图像转换上下文 img_convert_ctx,用于将视频帧从 pCodecCtx->pix_fmt(视频流的原始像素格式)转换为 YUV420P 格式,以便后续处理。
*/
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) { //初始化视频和定时器子系统,使用了 SDL 库中的 SDL_Init 函数来初始化 SDL 库,并检查初始化是否成功。
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;
screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h,SDL_WINDOW_OPENGL);//创建一个窗口并返回一个指向该窗口的指针。
/*
SDL_Window* SDL_CreateWindow(const char* title, int x, int y, int w, int h, Uint32 flags);
title:窗口的标题。
x:窗口的左上角 x 坐标,SDL_WINDOWPOS_UNDEFINED 表示将窗口放置在屏幕的中央。
y:窗口的左上角 y 坐标,SDL_WINDOWPOS_UNDEFINED 表示将窗口放置在屏幕的中央。
w:窗口的宽度。
h:窗口的高度。
flags:窗口的标志,控制窗口的行为,比如是否可调整大小、是否全屏等。在这里使用了 SDL_WINDOW_OPENGL,表示创建一个支持 OpenGL 渲染的窗口。
*/
if(!screen) {
printf("SDL: could not create window - exiting:%s\n",SDL_GetError());
return -1;
}
sdlRenderer = SDL_CreateRenderer(screen, -1, 0); //这行代码调用了SDL库中的SDL_CreateRenderer函数,用于创建一个与指定窗口相关联的渲染器。
/*
SDL_Renderer* SDL_CreateRenderer(SDL_Window* window, int index, Uint32 flags);
window:指定要与渲染器关联的窗口。
index:指定渲染器所支持的驱动程序索引。通常传入-1表示使用第一个支持的渲染器。
flags:指定渲染器的标志。在这里传入0表示使用默认标志。*/
//IYUV: Y + U + V (3 planes)
//YV12: Y + V + U (3 planes)
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);
/*调用了SDL库中的SDL_CreateTexture函数,用于创建一个纹理对象。
SDL_Texture* SDL_CreateTexture(SDL_Renderer* renderer, Uint32 format, int access, int w, int h);
renderer:指定要与纹理关联的渲染器。
format:指定纹理的像素格式。在这里使用了SDL_PIXELFORMAT_IYUV,表示纹理的像素格式是IYUV(即YUV420格式)。
access:指定纹理的访问方式。在这里使用了SDL_TEXTUREACCESS_STREAMING,表示纹理支持频繁更新,比如通过像素数据流进行更新。
w:指定纹理的宽度。
h:指定纹理的高度。*/
sdlRect.x=0;
sdlRect.y=0;
sdlRect.w=screen_w;
sdlRect.h=screen_h;
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
video_tid = SDL_CreateThread(sfp_refresh_thread,NULL,NULL);
/*SDL 库中用于创建线程的函数,用于在应用程序中启动一个新的线程执行指定的任务。
SDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, const char* name, void* data);
fn:指定新线程要执行的函数,通常是一个函数指针。
name:指定新线程的名称,可以是一个字符串。
data:指定传递给新线程的数据,通常是一个指针。这个函数会返回一个 SDL_Thread* 类型的指针,指向新创建的线程对象*/
//------------SDL End------------
//Event Loop
for (;;) {
//Wait
SDL_WaitEvent(&event);
/* SDL 库中用于等待事件的函数,它会一直阻塞当前线程,直到一个事件发生并被取出处理。
int SDL_WaitEvent(SDL_Event *event);
参数 event 是一个指向 SDL_Event 结构体的指针,用于存储发生的事件。
调用 SDL_WaitEvent 函数后,程序会一直阻塞,直到某个事件发生。一旦有事件发生,函数就会返回,
同时将事件信息填充到 event 参数所指向的结构体中,并返回一个非零值表示成功。如果发生错误或者函数被中断,它会返回零。 */
if(event.type==SFM_REFRESH_EVENT){
//------------------------------
if(av_read_frame(pFormatCtx, packet)>=0){//av_read_frame 函数会从输入媒体中读取下一个音频或视频帧,并将其存储到 pkt 所指向的 AVPacket 结构体中。这个函数会自动选择适当的流并读取其下一个帧。
if(packet->stream_index==videoindex){
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);//解码视频帧数据。
if(ret < 0){//int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, const AVPacket *avpkt);
/* avctx:指向已经初始化的视频解码器上下文(AVCodecContext)的指针。
picture:指向要存储解码后视频帧的 AVFrame 结构体的指针。
got_picture_ptr:一个指向整数的指针,用于存储解码是否成功的标志。如果解码成功,会将该标志设置为非零值,否则设置为零值。
avpkt:指向包含待解码视频数据的 AVPacket 结构体的指针。
调用 avcodec_decode_video2 函数会将 avpkt 所指向的视频数据解码,
并将解码后的视频帧存储到 picture 所指向的 AVFrame 结构体中。解码是否成功会通过 got_picture_ptr 指针进行标志。
成功解码后,picture 中将包含解码后的视频帧数据,可以用于后续的处理或显示。
函数返回值为解码后的帧的字节数,或者出现错误时返回负数。*/
printf("Decode Error.\n");
return -1;
}
if(got_picture){
/*将输入图像数据转换为目标像素格式,并进行可选的图像缩放和滤波操作。
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t *const dst[], const int dstStride[]);
c:指向已经初始化的图像转换上下文(SwsContext)的指针。
srcSlice:指向源图像数据的数组,通常是一个指向每个图像平面数据指针的数组。
srcStride:指向源图像每行大小的数组,通常是一个指向每个图像平面行大小的数组。
srcSliceY:指定源图像数据的起始行。
srcSliceH:指定源图像数据的行数。
dst:指向目标图像数据的数组,通常是一个指向每个图像平面数据指针的数组。
dstStride:指向目标图像每行大小的数组,通常是一个指向每个图像平面行大小的数组。
调用 sws_scale 函数会将 srcSlice 中指定的源图像数据转换为目标像素格式,并存储到 dst 指定的目标图像数据数组中。
转换后的图像数据可以进行可选的缩放和滤波操作,具体取决于初始化图像*/
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
//SDL---------------------------
SDL_UpdateTexture( sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0] );
/*将 YUV 数据更新到纹理对象中,用于显示视频帧。
int SDL_UpdateTexture(SDL_Texture* texture, const SDL_Rect* rect, const void* pixels, int pitch);
texture:指定要更新的纹理对象。
rect:指定要更新的纹理区域的矩形区域。如果为 NULL,则更新整个纹理。
pixels:指向包含要更新到纹理的像素数据的指针。
pitch:指定像素数据的行大小(即每行像素数据的字节数)。*/
SDL_RenderClear( sdlRenderer );
/*用于清除渲染器的绘图区域。
int SDL_RenderClear(SDL_Renderer* renderer);
参数 renderer 是要清除的渲染器。
调用 SDL_RenderClear 函数会清除与指定渲染器相关联的绘图区域,
将其填充为渲染器的默认清除颜色。这样做通常是为了准备绘制新的一帧图像或场景。*/
//SDL_RenderCopy( sdlRenderer, sdlTexture, &sdlRect, &sdlRect );
SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, NULL);
/*用于将纹理的内容复制到渲染器的绘图区域中。
renderer:指定要进行绘制的渲染器。
texture:指定要复制的纹理对象。
srcrect:指定要从纹理中复制的矩形区域。如果为NULL,则表示复制整个纹理。
dstrect:指定纹理将要复制到渲染器中的目标矩形区域。如果为NULL,则表示纹理将会被复制到渲染器的整个绘图区域中。*/
SDL_RenderPresent( sdlRenderer ); //将渲染器中的绘制结果显示到屏幕
//SDL End-----------------------
}
}
av_free_packet(packet);//释放被 pkt 指向的 AVPacket 结构体所占用的内存空间。在解码或处理完音视频帧数据后,通常需要调用这个函数来释放 AVPacket 结构体的内存,以避免内存泄漏。
}else{
//Exit Thread
thread_exit=1;
}
}else if(event.type==SDL_QUIT){
thread_exit=1;
}else if(event.type==SFM_BREAK_EVENT){
break;
}
}
sws_freeContext(img_convert_ctx);//用于释放图像转换上下文内存的函数。
SDL_Quit();//用于清理和关闭 SDL 环境的函数。
//--------------
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}