初次接触音视频方向,根据网上众多的博客及一些官方文档,编写了一个简单的视频播放器,中间踩了一些坑,仅供大家参考。
基本流程:解码-创建播放窗口-播放
步骤:
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