雷神simplest_ffmpeg_player解析(四)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/asd501823206/article/details/97136239

写在前面

学习雷神的博客,向雷神致敬~

看了雷神的小学期视频课,在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模块就结束了~

1.simple_ffmpeg_play_sdl2r.cpp
/**
 * 最简单的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 <stdio.h>

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;
}

2.simple_ffmpeg_player_su.cpp
/**
 * 最简单的基于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 <stdio.h>

#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 <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <SDL2/SDL.h>
#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; i<pFormatCtx->nb_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;
}

猜你喜欢

转载自blog.csdn.net/asd501823206/article/details/97136239