当前位置: 首页 > 工具软件 > SDL2-CS > 使用案例 >

使用FFMPEG和SDL编写一个简单的Duilib视频播放器

岳浩穰
2023-12-01

初次接触音视频方向,根据网上众多的博客及一些官方文档,编写了一个简单的视频播放器,中间踩了一些坑,仅供大家参考。

基本流程:解码-创建播放窗口-播放

步骤:
1、资源的初始化。

2、建立连接。通过avformat_open_input函数打开流地址,建立起连接。

3、解码器的初始化。先查找解码器(avcodec_find_decoder),成功后打开解码器(avcodec_open2)。

4、SDL的初始化并创建播放窗口。SDL初始化(SDL_Init),创建视频播放窗口(SDL_CreateWindowFrom),设置播放窗口大小等(SDL_CreateTexture)。

5、开启线程持续解码。开启一个线程持续解码(avcodec_decode_video2 ),然后传递给SDL播放窗口播放视频(SDL_UpdateYUVTexture),最后当线程退出时SDL退出(SDL_Quit)并结束视频的播放。

6、当该类析构时释放资源。

下面给出完整的类实现代码:

#include "stdafx.h"
#include "DuiVedioPlayFrame.h"

#define TIMER_RECONNECT 1


CDuiVedioPlayFrame::CDuiVedioPlayFrame(): m_nVideoIndex(-1)
										, m_bThreadRunning(true)
										, m_hThread(NULL)
										, m_bIsConnect(false)
{
	
	av_register_all();
	avformat_network_init();
	m_pFrame = av_frame_alloc();
	m_pFrameYUV = av_frame_alloc();
	m_pFormatCtx = avformat_alloc_context();
	
	InitializeCriticalSection(&m_cs);
}


CDuiVedioPlayFrame::~CDuiVedioPlayFrame()
{
	if (m_bThreadRunning)
	{
		m_bThreadRunning = false;
		WaitForSingleObject(m_hThread, INFINITE);
		CloseHandle(m_hThread);
	}
	
	DeleteCriticalSection(&m_cs);

	Release();
	
}

CDuiString CDuiVedioPlayFrame::GetSkinFolder()
{
	return _T("skin");
}

CDuiString CDuiVedioPlayFrame::GetSkinFile()
{
	return _T("VedioPlayFrame.xml");
}

LPCTSTR CDuiVedioPlayFrame::GetWindowClassName(void)const
{
	return _T("DuiVedioPlayFrame");
}

LRESULT CDuiVedioPlayFrame::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	PostQuitMessage(0);
	return 0;
}

void CDuiVedioPlayFrame::InitWindow()
{		

}

LRESULT CDuiVedioPlayFrame::HandleCustomMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	switch (uMsg)
	{
	case WM_TIMER:
	{
		if (wParam == TIMER_RECONNECT)
		{
			m_bIsConnect = Connect(m_strNetPath);

			if (m_bIsConnect)
			{
				KillTimer(this->m_hWnd, TIMER_RECONNECT);
				break;
			}
			else
			{
				WriteLog("视频流连接失败,重连中...");
			}
		}
	}
		break;
	default:
		break;
	}
	return 0;
}

bool CDuiVedioPlayFrame::Connect(string strNetPath)
{	
	//avformat_open_input设置阻塞超时值
	AVDictionary *opts = NULL ;
	av_dict_set(&opts, "rtsp_transport", "tcp", 0);                //采用tcp传输
	int iRet = av_dict_set(&opts, "stimeout", "100000", 0);


	avformat_close_input(&m_pFormatCtx);
	m_pFormatCtx = NULL;

	m_pFormatCtx = avformat_alloc_context();

	int iError = avformat_open_input(&m_pFormatCtx, strNetPath.c_str(), NULL, &opts);
	if (iError != 0)
	{
		WriteLog("码流地址错误!");
		return false;
	}

	if (avformat_find_stream_info(m_pFormatCtx, NULL) < 0) 
	{
		return false;
	}

	for (int i = 0; i < m_pFormatCtx->nb_streams; i++)
	{
		if (m_pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
			m_nVideoIndex = i;
			break;
		}
	}

	if (m_nVideoIndex == -1) 
	{
		WriteLog("Didn't find a video stream.\n");
		return false;
	}

	if (!InitCodec())
	{
		return false;
	}

	WriteLog("视频流连接成功!");
	return true;
}

bool CDuiVedioPlayFrame::InitCodec()
{
	AVCodec			*pCodec;
	m_pCodecCtx = m_pFormatCtx->streams[m_nVideoIndex]->codec;

	//查找解码器
	pCodec = avcodec_find_decoder(m_pCodecCtx->codec_id);
	if (pCodec == NULL) 
	{
		return false;
	}

	//打开解码器 
	if (avcodec_open2(m_pCodecCtx, pCodec, NULL) < 0) 
	{
		WriteLog("Could not open codec");
		return false;
	}	

	return true;
}

bool CDuiVedioPlayFrame::AnalysisStream(string strNetPath)
{
	unsigned char *out_buffer;

	m_bIsConnect = Connect(strNetPath);
	m_strNetPath = strNetPath;


	KillTimer(this->m_hWnd, TIMER_RECONNECT);
	if (!m_bIsConnect)
	{
		SetTimer(this->m_hWnd, TIMER_RECONNECT, 10000, NULL);
	}
	
	static bool bFlag = false;
	if (!bFlag)
	{
		InitSDL();
		bFlag = true;
	}
	
	m_bThreadRunning = true;
	m_hThread = (HANDLE)_beginthreadex(NULL, 0, PlayThreadFun, this, 0, NULL);
	
	return true;
}

bool CDuiVedioPlayFrame::InitSDL()
{
	SDL_Window *screen;
	int screen_w = 0, screen_h = 0;

	char sdl_var[64];
	sprintf(sdl_var, "SDL_WINDOWID=%d", this->GetHWND());//主窗口句柄   
	

	//SDL---------------------------
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) 
	{
		char szLog[1024] = { 0 };
		sprintf_s(szLog, sizeof(szLog), "Could not initialize SDL - %s\n", SDL_GetError());
		OutputDebugString(szLog);
		return false;
	}

	screen_w = GetSystemMetrics(SM_CXSCREEN); //m_pCodecCtx->width;
	screen_h = GetSystemMetrics(SM_CYSCREEN);// m_pCodecCtx->height;

	//SDL 2.0 Support for multiple windows
	screen = SDL_CreateWindowFrom(this->GetHWND());

	if (!screen) 
	{
		return false;
	}

	m_psdlRenderer = SDL_CreateRenderer(screen, -1, 0); 
	m_psdlTexture = SDL_CreateTexture(m_psdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, screen_w, screen_h);

	
	m_sdlRect.x = 0;
	m_sdlRect.y = 0;
	m_sdlRect.w = screen_w;
	m_sdlRect.h = screen_h;
	//SDL End----------------------

	return true;
}

void CDuiVedioPlayFrame::Close()
{
	m_bThreadRunning = false;
	if (NULL != m_hThread)
	{
		WaitForSingleObject(m_hThread, INFINITE);
		CloseHandle(m_hThread);
	}

}

void CDuiVedioPlayFrame::Release()
{	
	av_frame_free(&m_pFrameYUV);
	av_frame_free(&m_pFrame);	
	avcodec_close(m_pCodecCtx);
	avformat_close_input(&m_pFormatCtx);

	m_pFrameYUV = NULL;
	m_pFrame = NULL;
	m_pCodecCtx = NULL;
	m_pFormatCtx = NULL;

	SDL_DestroyTexture(m_psdlTexture);
	SDL_Quit();
}

unsigned int __stdcall CDuiVedioPlayFrame::PlayThreadFun(PVOID pParam)
{
	CDuiVedioPlayFrame *pThis = (CDuiVedioPlayFrame*)pParam;
	int ret, got_picture;
	AVPacket *packet = NULL;
	unsigned char *out_buffer = NULL;	
	//InitSDL();
	while (pThis->m_bThreadRunning && pThis->m_bIsConnect)
	{
		if (NULL == pThis->m_pCodecCtx)
		{
			return -1;
		}

		out_buffer = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pThis->m_pCodecCtx->width, pThis->m_pCodecCtx->height, 1));
		av_image_fill_arrays(pThis->m_pFrameYUV->data, pThis->m_pFrameYUV->linesize, out_buffer,
			AV_PIX_FMT_YUV420P, pThis->m_pCodecCtx->width, pThis->m_pCodecCtx->height, 1);


		packet = (AVPacket *)av_malloc(sizeof(AVPacket));

		pThis->m_pImg_convert_ctx = sws_getContext(pThis->m_pCodecCtx->width, pThis->m_pCodecCtx->height, pThis->m_pCodecCtx->pix_fmt,
			pThis->m_pCodecCtx->width, pThis->m_pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);

		if (NULL == packet)
		{
			WriteLog("out_buffer, packet(AVPacket)分配内存失败!");
			return -1;
		}

		while (pThis->m_bThreadRunning && av_read_frame(pThis->m_pFormatCtx, packet) >= 0)
		{
			if (packet->stream_index == pThis->m_nVideoIndex)
			{
				ret = avcodec_decode_video2(pThis->m_pCodecCtx, pThis->m_pFrame, &got_picture, packet);
				if (ret < 0)
				{
					WriteLog("Decode Error");
					return -1;
				}
				if (got_picture)
				{
					int nRes = sws_scale(pThis->m_pImg_convert_ctx, (const unsigned char* const*)pThis->m_pFrame->data, pThis->m_pFrame->linesize, 0, pThis->m_pCodecCtx->height,
						pThis->m_pFrameYUV->data, pThis->m_pFrameYUV->linesize);

					nRes = SDL_UpdateYUVTexture(pThis->m_psdlTexture, &pThis->m_sdlRect,
						pThis->m_pFrameYUV->data[0], pThis->m_pFrameYUV->linesize[0],
						pThis->m_pFrameYUV->data[1], pThis->m_pFrameYUV->linesize[1],
						pThis->m_pFrameYUV->data[2], pThis->m_pFrameYUV->linesize[2]);

					SDL_RenderClear(pThis->m_psdlRenderer);
					SDL_RenderCopy(pThis->m_psdlRenderer, pThis->m_psdlTexture, NULL, &pThis->m_sdlRect);
					SDL_RenderPresent(pThis->m_psdlRenderer);
					//SDL End-----------------------
					//Delay 1ms
					SDL_Delay(20);
				}
			}
			if (packet != NULL)
			{
				av_free_packet(packet);
			}
			
		}

		if (out_buffer != NULL)
		{
			av_free(out_buffer);
		}
		
		sws_freeContext(pThis->m_pImg_convert_ctx);
	}
	
	return 0;
}

//获取连接状态,在调用AnalysisStream函数后使用
bool CDuiVedioPlayFrame::GetConnectStates()
{
	bool bIsConnect = false;
	if (this != NULL)
	{
		bIsConnect = m_bIsConnect;
	}

 	return bIsConnect;
}

简单介绍一下各个函数的作用:

部分资源的初始化和释放分别放在了构造和析构函数中。

Connect函数:传入一个视频流地址,建立起连接。

InitCodec函数:解码器的初始化工作。

InitSDL函数:SDL相关资源的初始化及播放视频窗口的创建。

Close函数:结束视频流的解析,即关闭视频。

Release函数:资源的释放。

PlayThreadFun线程:解析流文件。

GetConnectStates函数:获取连接状态,供外部调用

AnalysisStream函数:播放视频函数,供外部调用。

TIMER_RECONNECT消息用于断线自动重连

编写过程中遇到的一些问题;

1、connect传入错误的流地址avformat_close_input函数会阻塞,设置超时值就OK了。

//设置超时值1s
AVDictionary *opts = NULL ;
av_dict_set(&opts, "rtsp_transport", "tcp", 0);                
int iRet = av_dict_set(&opts, "stimeout", "100000", 0);

2、下面这段代码是为了保证SDL播放窗口只被初始化创建一次,多次初始化会导致占用内存急剧上升,最后导致程序的崩溃。

static bool bFlag = false;
if (!bFlag)
{
    InitSDL();
    bFlag = true;
}

3、线程函数中有一个SDL睡眠时间的设置,单位ms。若设置时间过长会导致视频画面卡顿花屏等,时间值设置适中即可。SDL_Delay(20);

若有写的不好的地方请指出,我会加以改正。大部分代码都是在理解了雷神代码后,在此基础上编写的,若觉着我讲的不够明了、详细,请移步雷神博客。后面附上雷神的博客链接https://blog.csdn.net/leixiaohua1020/article/details/8652605

 类似资料: