ffmpeg+SDL的使用之获取视频帧将其存储为PPM格式图片

前言

首先再次说明下使用ffmpeg处理音视频流的步骤如下:
1:从本地或者网络位置打开音视频流;
2:获取音视频流中的包,将其转换为帧;
3:如果帧不完整,转到2;
4:对帧做一些事情;
5:转到2;

我们所能做的就是在步骤4上进行一些处理。本项目所做的事流程如下:

首先,找到包中的视频流。

然后,解码成帧。

最后,将原始帧转换成YUV420P格式使用SDL进行覆盖显示。

注意:本项目中对视频的播放并没有做延时,解码转换出来直接播放,所以播放速度较快。


流媒体相关知识科普

YUV和RGB一样也是一种颜色空间。其原理是把亮度与色度分离,研究证明,人眼对亮度的敏感超过色度。利用这个原理,可以把色度信息减少一点,人眼也无法查觉这一点。所以使用该颜色空间可以有效降低带宽。简单的来说,现在视频流一般采用的视频编码格式为H264(工程中test.mp4视频就是),该格式是经过压缩编码后的,是不能直接被显示出来的。必须经过解码到未压缩的格式,如YUV,才可以播放。事实上现在很多视频流的格式已经是YUV格式或者很容易被转换成YUV格式了。SDL支持多种格式的YUV覆盖,下面程序使用较为通用的一种YUV420P。使用VLC播放器对工程中的test.mp4视频文件编解码信息进行查看如下图:

其中,MP4是一种视频的封装格式,其余的还有MKV,AVI等。我们一般所说的视频都包括音频,所以准确的来说,MP4格式用来对编码之后的H264(视频压缩格式)和AAC(音频压缩格式)进行封装,使用播放器播放的时候需要将视频和音频分离出来,该过程叫解复用。然后对视音频分别进行解码,视频一般解码为YUV,音频一般解码为PCM。播放器将解码后的音视频数据经过音视频同步之后,分别输送到声卡和网卡驱动。这就是我们平时从点开一个视频文件到播放出来所经历的步骤。


源码

有了前一篇博文ffmpeg+SDL的使用之获取视频帧将其存储为PPM格式图片的技术积累,现在需要学习的新知识就是如何使用SDL将视频显示出来。SDL库的使用相对来讲还是比较简单头文件采用和前篇博文一样的头文件ffmpegsdl.h,工程配置方式也相同。

videoPlay.cpp的源码如下:

#include <iostream>
#include "ffmpegsdl.h"
using namespace std;

int main()
{
	// Register all formats and codecs
	av_register_all();

	// Initalizing to NULL prevents segfaults!
	AVFormatContext   *pFormatCtx = NULL;
	// Open video file
	if (avformat_open_input(&pFormatCtx, "..\\test.mp4", NULL, NULL) != 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
		return -1; // Couldn't open file
	}
	// Dump information about file onto standard error
	av_dump_format(pFormatCtx, 0, "1=========================>test.mp4", 0);
	// Retrieve stream information
	if (avformat_find_stream_info(pFormatCtx, NULL)<0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
		return -1; // Couldn't find stream information
	}
	// 对比上次的Dump可以看出通过avformat_find_stream_info函数往AVFormatContext中填充了流的信息
	av_dump_format(pFormatCtx, 0, "2=========================>test.mp4", 0);

	AVCodec* pDec = NULL;
	/* select the video stream and find the decoder*/
	int video_stream_index = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &pDec, 0);
	if (video_stream_index < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot find a video stream in the input file\n");
		return -1;
	}

	//Note that we must not use the AVCodecContext from the video stream directly! 
	//So we have to use avcodec_copy_context() to copy the context to a new location (after allocating memory for it, of course).	
	/* create decoding context */
	AVCodecContext* pDecCtx = avcodec_alloc_context3(pDec);
	if (!pDecCtx)
	{
		return AVERROR(ENOMEM);
	}

	//Fill the codec context based on the values from the supplied codec
	avcodec_parameters_to_context(pDecCtx, pFormatCtx->streams[video_stream_index]->codecpar);

	//Initialize the AVCodecContext to use the given AVCodec.
	if (avcodec_open2(pDecCtx, pDec, NULL) < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot open video decoder\n");
		return -1;
	}


	AVPacket* packet = (AVPacket *)av_malloc(sizeof(AVPacket));
	av_init_packet(packet);
	//Allocate an AVFrame and set its fields to default values.
	AVFrame *pFrame = av_frame_alloc();
	AVFrame *pFrameYUV = av_frame_alloc();

	// Determine required buffer size and allocate buffer
	int byteSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
		pDecCtx->width,
		pDecCtx->height,
		1
	);
	uint8_t* buffer = (uint8_t *)av_malloc(byteSize);

	// Assign appropriate parts of buffer to image planes in pFrameRGB
	// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
	// of AVPicture
	av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,
		buffer, AV_PIX_FMT_YUV420P,
		pDecCtx->width,
		pDecCtx->height,
		1
	);

	int screen_w = pDecCtx->width / 2;
	int screen_h = pDecCtx->height / 2;
	// initialize SWS context for software scaling
	SwsContext *pSws_ctx = sws_getContext(pDecCtx->width,
		pDecCtx->height,
		pDecCtx->pix_fmt,
		screen_w,
		screen_h,
		AV_PIX_FMT_YUV420P,
		SWS_BICUBIC,
		NULL,
		NULL,
		NULL
	);

	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  
	SDL_Window* screen = SDL_CreateWindow("SDL",
		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)  
	SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer,
		SDL_PIXELFORMAT_IYUV,
		SDL_TEXTUREACCESS_STREAMING,
		screen_w,
		screen_h
	);
	SDL_Rect sdlRect;
	sdlRect.x = 0;
	sdlRect.y = 0;
	sdlRect.w = screen_w;
	sdlRect.h = screen_h;

	int ret = -1;
	SDL_Event event;

	while (1)
	{
		/**
		*Technically a packet can contain partial frames or other bits of data,
		*but ffmpeg's parser ensures that the packets we get contain either complete or multiple frames.
		*/
		ret = av_read_frame(pFormatCtx, packet);
		if (ret < 0)
		{
			break;
		}

		if (packet->stream_index == video_stream_index)
		{
			//Supply raw packet data as input to a decoder.
			ret = avcodec_send_packet(pDecCtx, packet);
			if (ret < 0)
			{
				av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
				break;
			}

			while (ret >= 0)
			{
				//Return decoded output data from a decoder.
				ret = avcodec_receive_frame(pDecCtx, pFrame);//一个包可能有多个帧
				if (ret >= 0)
				{
					// Convert the image from its native format to YUV
					sws_scale(pSws_ctx,
						(const uint8_t* const *)pFrame->data,
						pFrame->linesize,
						0,
						pDecCtx->height,
						pFrameYUV->data,
						pFrameYUV->linesize
					);

				}
				else if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
				{
					break;
				}
				else
				{
					av_log(NULL, AV_LOG_ERROR, "Error while receiving a frame from the decoder\n");
					goto end;
				}

				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, NULL);
				SDL_RenderPresent(sdlRenderer);
			}

		}
		// Wipe the packet.		
		av_packet_unref(packet);
		SDL_PollEvent(&event);
		switch (event.type)
		{
		case SDL_QUIT:
			SDL_Quit();
			break;
		default:
			break;
		}
	}
end:
	// Free the packet that was allocated by av_read_frame
	av_packet_free(&packet);
	//Free the swscaler context swsContext
	sws_freeContext(pSws_ctx);
	//Free the codec context and everything associated with it and write NULL to the provided pointer.
	avcodec_free_context(&pDecCtx);
	// Close the video file
	avformat_close_input(&pFormatCtx);
	//Free the frame and any dynamically allocated objects in it
	av_frame_free(&pFrame);
	av_frame_free(&pFrameYUV);
	// Free the RGB image
	av_free(buffer);
	return 0;
}

工程下载

github: https://github.com/zhuyinglong/ffmpeg-SDL


后续

虽然我尽力让自己正确,但避免不了有些理解不到位的误导大家,有些不清楚的地方,推荐大家还是参考官方原版一手的资料。这也是为什么我的代码注释很少的原因,只是帮大家将流程打通。大家将我的工程下载之后,使用source Insight可以很方便的看到所有函数的官方原版注释,那是最准确的。

参考资料如下:
http://dranger.com/ffmpeg/tutorial02.html
http://ffmpeg.org/doxygen/trunk/filtering_video_8c-example.html

猜你喜欢

转载自blog.csdn.net/zhuyinglong2010/article/details/78140500