关于《最简单的基于FFMPEG+SDL的视频播放器》记录二

一、概述

       之前写过一篇关于《最简单的基于FFMPEG+SDL的视频播放器》的记录,主要对FFMPEG的解码流程及代码做了比较详细的解释,但是对SDL部分并未做任何的解释,这次记录二将重点放在了SDL部分。由于SDL已经升级到2.0,所以此次将1.x和2.0一起记录下来。

二、SDL工作流程

  • SDL 1.x   

1.流程图

这里直接借鉴作者的原图,处理流程图贴出来:


关于《最简单的基于FFMPEG+SDL的视频播放器》记录二_第1张图片

这里不对该图作解释,可以参考下面的流程处理代码,里面有比较详细的解释。

2.各个处理流程代码

1、初始化SDL库

	//SDL Begin----------------------------  
	//初始化SDL库
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
		printf("Could not initialize SDL - %s\n", SDL_GetError());
		return -1;
	}

2、创建窗口

//在显示器上创建一个窗口(window),在SDL中显示图像的窗口叫做surface。
	screen_w = pCodecCtx->width;
	screen_h = pCodecCtx->height;
	screen = SDL_SetVideoMode(screen_w, screen_h, 0, 0);//第一个0表示使用和当前屏幕一样的颜色深度,第二个0是标志位,暂时可以忽略

	if (!screen) {
		printf("SDL: could not set video mode - exiting:%s\n", SDL_GetError());
		return -1;
	}
3、创建YUV overlay

//创建一个YUV 覆盖以便于我们输入视频上去
	//bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);//YUV平面模式
	  bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_IYUV_OVERLAY, screen);//YVU平面模式
	rect.x = 0;
	rect.y = 0;
	rect.w = screen_w;
	rect.h = screen_h;

4、显示YUV overlay

                                 SDL_LockYUVOverlay(bmp); //锁定这个YUV覆盖,因为我们将要去改写它
				//YVU模式
				//pFrameYUV->data[0] = bmp->pixels[0];//将三个通道数据分别指向YUV覆盖的三个平面
				//pFrameYUV->data[1] = bmp->pixels[2];
				//pFrameYUV->data[2] = bmp->pixels[1];
				//pFrameYUV->linesize[0] = bmp->pitches[0];
				//pFrameYUV->linesize[1] = bmp->pitches[2];
				//pFrameYUV->linesize[2] = bmp->pitches[1];
				//YUV模式
				pFrameYUV->data[0] = bmp->pixels[0];//将三个通道数据分别指向YUV覆盖的三个平面
				pFrameYUV->data[1] = bmp->pixels[1];
				pFrameYUV->data[2] = bmp->pixels[2];
				pFrameYUV->linesize[0] = bmp->pitches[0];//设置行大小
				pFrameYUV->linesize[1] = bmp->pitches[1];
				pFrameYUV->linesize[2] = bmp->pitches[2];
				//进行格式转换及缩放(未进行缩放)
				sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0,pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);

				//解锁YUV覆盖
				SDL_UnlockYUVOverlay(bmp);
				//显示YUV图片
				SDL_DisplayYUVOverlay(bmp, &rect);
				//延迟40ms,否则将解码一帧立即显示一帧,播放速度将取决于解码速度  
				SDL_Delay(40);

5、SDL相关变量解释

//SDL  
	int screen_w, screen_h;//窗口宽、高  
	SDL_Surface *screen;//一个窗口,用于显示YUV覆盖
	SDL_Overlay *bmp;//YUV覆盖,可以理解为一张一张的图片
	SDL_Rect rect;//YUV显示区域,以窗口左上角为(0,0)
这里的surface、rect、  overlay 理解如下图所示:

关于《最简单的基于FFMPEG+SDL的视频播放器》记录二_第2张图片

其中整个窗口是surface,绿色部分是rect,左上角放映的部分就是overlay。

  • SDL 2.0

1.流程图

这里直接借鉴作者的原图,处理流程图贴出来:

关于《最简单的基于FFMPEG+SDL的视频播放器》记录二_第3张图片

这里对SDL2.0进行一下说明:

SDL_Window就是使用SDL的时候弹出的那个窗口。在SDL1.x版本中,只可以创建一个一个窗口。在SDL2.0版本中,可以创建多个窗口。
SDL_Texture用于显示YUV数据。一个SDL_Texture对应一帧YUV数据,但不等于YUV数据帧。
SDL_Renderer用于渲染SDL_Texture至SDL_Window。
SDL_Rect用于确定SDL_Texture显示的位置。注意:一个SDL_Texture可以指定多个不同的SDL_Rect,这样就可以在SDL_Window不同位置显示相同的内容(使用SDL_RenderCopy()函数),下面的代码就做了一个二分屏的例子。
关于
他们的关系如下图所示:

关于《最简单的基于FFMPEG+SDL的视频播放器》记录二_第4张图片

2、源代码及注释

开发环境:VS 2013

#include 
#include
#include
//包含库
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h" 
#include "libswresample/swresample.h"  
};
int main(int argc, char* argv[])
{
	//FFmpeg相关变量  
	AVFormatContext *pFormatCtx;//AVFormatContext主要存储视音频封装格式中包含的信息  
	unsigned             i;
	int             videoindex;//视频流所在序号  
	AVCodecContext  *pCodecCtx;//AVCodecContext,存储该视频/音频流使用解码方式的相关数据  
	AVCodec         *pCodec;//解码器  
	AVFrame *pFrame, *pFrameYUV;//解码后数据  
	AVPacket packet;//解码前数据  
	struct SwsContext *img_convert_ctx;//格式转换器
	//SDL  
	SDL_Window *screen;//一个窗口,用于显示YUV覆盖
	SDL_Renderer* sdlRenderer;//渲染器
	SDL_Texture* sdlTexture;//YUV纹理,可以理解为一张一张的图片,但不同于原始图片
	SDL_Rect sdlRect;//YUV显示区域,以窗口左上角为(0,0)
	SDL_Rect sdlRect_2;
	int screen_w = 0, screen_h = 0;//窗口宽、高  
	uint8_t * out_buffer;
	int ret, got_picture;
	char* filepath = "1.mp4";//输入文件
	av_register_all();//初始化libformat库和注册编解码器
	avformat_network_init();//初始化网络组件  
	pFormatCtx = avformat_alloc_context();
	//打开视频文件然后读取头部信息到pFormatCtx  
	if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0){
		printf("Couldn't open input stream.\n");
		return -1;
	}
	//获取流信息  
	if (avformat_find_stream_info(pFormatCtx, NULL) < 0){
		printf("Couldn't find stream information.\n");
		return -1;
	}
	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;
	}
	pCodecCtx = pFormatCtx->streams[videoindex]->codec;//获取解码环境  
	pCodec = avcodec_find_decoder(pCodecCtx->codec_id);//获取解码器  
	if (pCodec == NULL){
		printf("Codec not found.\n");
		return -1;
	}
	//打开解码器 
	if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0){
		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));
	//avpicture_fill是让picture的data[0]、data[1]、data[2]等正确的指向av_frame_alloc()分配空间地址,  
	//因为av_frame_alloc()分配的空间是一个线性地址(一个连续的缓冲区),而pFrameYUV的data[]是分别指向  
	//不同的平面的,如YUV420P中的Y平面、U平面、V平面,通过avpicture_fill之后,pFrameYUV的data[]就分别指向  
	//这个线性地址的不同位置了。完成avpicture_fill后,你对pFrameYUV中的data[]进行操作时,实际是操作avcodec_alloc_frame()  
	//分配的空间。
	avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

	//SDL Begin----------------------------  
	//初始化SDL库
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
		printf("Could not initialize SDL - %s\n", SDL_GetError());
		return -1;
	}
	//在显示器上创建一个原始视频大小的窗口(window)
	screen_w = pCodecCtx->width;
	screen_h = pCodecCtx->height;

	screen = SDL_CreateWindow("my video player", 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;
	}
	//创建渲染器
	sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
	//创建纹理,可以理解为一帧一帧的图片,但不同于原始的图片
	//SDL_PIXELFORMAT_IYUV: 图像格式为YUV平面模式
	//sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);
	//做二分屏
	sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width / 2, pCodecCtx->height);
	//设置显示区域
	//第一个显示区域
	sdlRect.x = 0;                                      
	sdlRect.y = 0;                                      
	sdlRect.w = pCodecCtx->width / 2;                   
	sdlRect.h = pCodecCtx->height;                      
	//第二个显示区域                                    
	sdlRect_2.x = pCodecCtx->width / 2 + 20;            
	sdlRect_2.y = 0;                                    
	sdlRect_2.w = pCodecCtx->width / 2;                
	sdlRect_2.h = pCodecCtx->height;                   

	//SDL End------------------------  
	//设置scale环境,转换像素格式为PIX_FMT_YUV420P,使用SWS_BICUBIC缩放算法
	//img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
	//将原始图片宽度缩小1/2
	img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width / 2, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
	//------------------------------  

	while (av_read_frame(pFormatCtx, &packet) >= 0){//读取下一帧数据  

		if (packet.stream_index == videoindex){//必须是视频流帧  
			//Decode  
			ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);//解码数据帧到pFrame  
			if (ret < 0){
				printf("Decode Error.\n");
				return -1;
			}
			if (got_picture){
				//进行格式转换及缩放
				sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);

				//用新的数据帧更新纹理
				SDL_UpdateYUVTexture(sdlTexture, &sdlRect,
					pFrameYUV->data[0], pFrameYUV->linesize[0],
					pFrameYUV->data[1], pFrameYUV->linesize[1],
					pFrameYUV->data[2], pFrameYUV->linesize[2]);

				//清除当前正在渲染的区域
				SDL_RenderClear(sdlRenderer);
				//将纹理拷贝到待显示的区域
				SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
				SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect_2);
				SDL_RenderPresent(sdlRenderer);//进行渲染
				//延迟40ms,否则将解码一帧立即显示一帧,播放速度将取决于解码速度  
				SDL_Delay(40);
			}
		}
		av_free_packet(&packet);
	}

	//FIX: Flush Frames remained in Codec  
	while (1) {
		ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
		if (ret < 0)//出错  
			break;
		if (!got_picture)//没有可解码的数据帧  
			break;
		sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);

		SDL_UpdateTexture(sdlTexture, &sdlRect, pFrameYUV->data[0], pFrameYUV->linesize[0]);
		SDL_RenderClear(sdlRenderer);
		SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
		SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect_2);
		SDL_RenderPresent(sdlRenderer);
		//延迟40ms,否则将解码一帧立即显示一帧,播放速度将取决于解码速度  
		SDL_Delay(40);
	}
	sws_freeContext(img_convert_ctx);
	SDL_Quit();//退出SDL

	av_free(pFrameYUV);//释放帧数据占用的内存  
	avcodec_close(pCodecCtx);//释放解码器环境  
	avformat_close_input(&pFormatCtx);//释放输入环境  
	return 0;
}

3.二分屏效果图

中间黑色的部分是隔开的20个像素的距离,便于分开两个视频。

关于《最简单的基于FFMPEG+SDL的视频播放器》记录二_第5张图片

注:
在SDL 2.0中要好好理解window,rect,avframe,texture的关系,其中window,rect,texture的关系可以参考上面的surface、rect、 overlay之间的关系,它们一一对应。此外avframe,texture也是一一对应的关系,要做到显示与数据帧大小合适,那么texture和avframe大小必须保持一致。可以将texture的大小设置的和avframe一样,也可以将avframe进行缩放以适合texture的大小。例如上面的例子中,为了做到二分屏,我将原始图像进行了缩小,以适应texture大小。

你可能感兴趣的:(FFmpeg)