FFmpeg入门,简单播放器

时间:2023-03-08 20:44:10

一个偶然的机缘,好像要做直播相关的项目

为了筹备,前期做一些只是储备,于是开始学习ffmpeg

这是学习的第一课

做一个简单的播放器,播放视频画面帧

思路是,将视频文件解码,得到帧,然后使用定时器,1秒显示24帧

1.创建win32工程,添加菜单项 “打开”

为了避免闪烁,MyRegisterClass中设置hbrBackground为null

2.在main函数中初始化ffmpeg库:av_register_all();

3.响应菜单打开

 void LoadVideoPlay(HWND hWnd)
{
if (gbLoadVideo)
{
return;
} TCHAR szPath[] = { };
DWORD dwPath = ;
OPENFILENAME ofn = { };
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hWnd;
ofn.hInstance = hInst;
ofn.lpstrFile = szPath;
ofn.nMaxFile = dwPath;
ofn.lpstrFilter = _T("Video(*.mp4)\0*.MP4*;*.avi*\0");
ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
ofn.lpstrInitialDir = _T("F:\\"); if (!GetOpenFileName(&ofn))
{
DWORD dwErr = CommDlgExtendedError();
OutputDebugString(_T("GetOpenFileName\n"));
return;
} std::wstring strVideo = szPath;
std::thread loadVideoThread([hWnd, strVideo]() {
gbLoadVideo = TRUE;
std::string sVideo = UnicodeToUTF_8(strVideo.c_str(), strVideo.size());
OpenVideoByFFmpeg(hWnd, sVideo.c_str());
gbLoadVideo = FALSE;
}); loadVideoThread.detach();
}

使用c++11的线程来加载视频文件并进行解码工作。

4.在加载完视频之后,设置窗口为不可缩放

创建缓存DC等显示环境

设置播放帧画面的定时器

5.将解码的帧画面转化为 RGB 32位格式,并存储至队列中等待播放

6.播放帧画面

在WM_PAINT消息中进行绘画

 void PlayFrame(HWND hWnd, UCHAR* buffer, UINT uWidth, UINT uHeight)
{
do
{
if (GetFramesSize() > ( * )) // 缓冲5秒的帧数
{
if (WaitForSingleObject(ghExitEvent, ) == WAIT_OBJECT_0)
{
return;
}
}
else
{
HDC hDC = GetDC(hWnd);
HBITMAP hFrame = CreateBitmapFromPixels(hDC, uWidth, uHeight, , buffer);
if (hFrame)
{
PushFrame(hFrame);
}
ReleaseDC(hWnd, hDC);
break;
} } while (true);
}

因为解码拿到的是像素数据,需要将像素数据转化为Win32兼容位图

 HBITMAP CreateBitmapFromPixels(HDC hDC, UINT uWidth, UINT uHeight, UINT uBitsPerPixel, LPVOID pBits)
{
if (uBitsPerPixel <= ) // NOT IMPLEMENTED YET
return NULL; HBITMAP hBitmap = ;
if (!uWidth || !uHeight || !uBitsPerPixel)
return hBitmap;
LONG lBmpSize = uWidth * uHeight * (uBitsPerPixel / );
BITMAPINFO bmpInfo = { };
bmpInfo.bmiHeader.biBitCount = uBitsPerPixel;
bmpInfo.bmiHeader.biHeight = -(static_cast<LONG>(uHeight)); // 因为拿到的帧图像是颠倒的
bmpInfo.bmiHeader.biWidth = uWidth;
bmpInfo.bmiHeader.biPlanes = ;
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
// Pointer to access the pixels of bitmap
UINT * pPixels = ;
hBitmap = CreateDIBSection(hDC, (BITMAPINFO *)&
bmpInfo, DIB_RGB_COLORS, (void **)&
pPixels, NULL, ); if (!hBitmap)
return hBitmap; // return if invalid bitmaps memcpy(pPixels, pBits, lBmpSize); return hBitmap;
}

7.播放完毕,回复窗口设定,关闭定时器

代码流程一目了然,用来学习ffmpeg入门

最后贴上总的代码

 // main.cpp : 定义应用程序的入口点。
// #include "stdafx.h"
#include "testPlayVideo.h" #include <windows.h>
#include <commdlg.h>
#include <deque>
#include <string>
#include <mutex>
#include <thread> extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/pixfmt.h"
#include "libavutil/imgutils.h"
#include "libavdevice/avdevice.h"
#include "libswscale/swscale.h"
} #pragma comment(lib, "Comdlg32.lib") #define MAX_LOADSTRING 100
#define TIMER_FRAME 101
#define CHECK_TRUE(v) {if(!v) goto cleanup;}
#define CHECK_ZERO(v) {if(v<0) goto cleanup;} // 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名 std::mutex gFrameLock; // 图像位图帧的访问锁
std::deque<HBITMAP> gFrames; // 所有解码的视频图像位图帧
HDC ghFrameDC; // 视频帧图像兼容DC
HBITMAP ghFrameBmp; // 兼容DC的内存位图
HBRUSH ghFrameBrush; // 兼容DC的背景画刷
HANDLE ghExitEvent; // 程序退出通知事件
UINT uFrameTimer; // 定时器,刷新窗口显示图像位图帧
UINT uBorderWidth;
UINT uBorderHeight;
BOOL gbLoadVideo;
DWORD dwWndStyle; // 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
HBITMAP CreateBitmapFromPixels(HDC hDC, UINT uWidth, UINT uHeight, UINT uBitsPerPixel, LPVOID pBits); HBITMAP PopFrame(void)
{
std::lock_guard<std::mutex> lg(gFrameLock); if (gFrames.empty())
{
return nullptr;
}
else
{
HBITMAP hFrame = gFrames.front();
gFrames.pop_front();
return hFrame;
}
} void PushFrame(HBITMAP hFrame)
{
std::lock_guard<std::mutex> lg(gFrameLock);
gFrames.push_back(hFrame);
} size_t GetFramesSize(void)
{
std::lock_guard<std::mutex> lg(gFrameLock);
return gFrames.size();
} void ReleasePaint(void)
{
if (ghFrameDC) DeleteDC(ghFrameDC);
if (ghFrameBmp) DeleteObject(ghFrameBmp);
if (ghFrameBrush) DeleteObject(ghFrameBrush); ghFrameDC = nullptr;
ghFrameBmp = nullptr;
ghFrameBrush = nullptr;
} void CreatePaint(HWND hWnd)
{
RECT rc;
GetClientRect(hWnd, &rc);
HDC hDC = GetDC(hWnd);
ghFrameDC = CreateCompatibleDC(hDC);
ghFrameBmp = CreateCompatibleBitmap(hDC, rc.right - rc.left, rc.bottom - rc.top);
ghFrameBrush = CreateSolidBrush(RGB(, , ));
SelectObject(ghFrameDC, ghFrameBmp);
ReleaseDC(hWnd, hDC);
} void ReleaseFrames(void)
{
std::lock_guard<std::mutex> lg(gFrameLock);
for (auto& hFrame : gFrames)
{
DeleteObject(hFrame);
}
gFrames.clear(); ReleasePaint();
} void PlayFrame(HWND hWnd, UCHAR* buffer, UINT uWidth, UINT uHeight)
{
do
{
if (GetFramesSize() > ( * )) // 缓冲5秒的帧数
{
if (WaitForSingleObject(ghExitEvent, ) == WAIT_OBJECT_0)
{
return;
}
}
else
{
HDC hDC = GetDC(hWnd);
HBITMAP hFrame = CreateBitmapFromPixels(hDC, uWidth, uHeight, , buffer);
if (hFrame)
{
PushFrame(hFrame);
}
ReleaseDC(hWnd, hDC);
break;
} } while (true);
} std::string UnicodeToUTF_8(const wchar_t *pIn, size_t nSize)
{
if (pIn == NULL || nSize == )
{
return "";
} std::string s;
int n = WideCharToMultiByte(CP_UTF8, NULL, pIn, nSize, NULL, , NULL, NULL);
if (n > )
{
s.resize(n);
WideCharToMultiByte(CP_UTF8, NULL, pIn, nSize, &s[], n, NULL, NULL);
} return s;
} int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine); // TODO: 在此放置代码。
ghExitEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
av_register_all(); // 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_TESTPLAYVIDEO, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance); // 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
} HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TESTPLAYVIDEO)); MSG msg; // 主消息循环:
while (GetMessage(&msg, nullptr, , ))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
} return (int) msg.wParam;
} //
// 函数: MyRegisterClass()
//
// 目的: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = ;
wcex.cbWndExtra = ;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TESTPLAYVIDEO));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = /*(HBRUSH)(COLOR_WINDOW+1)*/nullptr;
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_TESTPLAYVIDEO);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassExW(&wcex);
} //
// 函数: InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中 HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, , CW_USEDEFAULT, , nullptr, nullptr, hInstance, nullptr); if (!hWnd)
{
return FALSE;
} ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd); return TRUE;
} HBITMAP CreateBitmapFromPixels(HDC hDC, UINT uWidth, UINT uHeight, UINT uBitsPerPixel, LPVOID pBits)
{
if (uBitsPerPixel <= ) // NOT IMPLEMENTED YET
return NULL; HBITMAP hBitmap = ;
if (!uWidth || !uHeight || !uBitsPerPixel)
return hBitmap;
LONG lBmpSize = uWidth * uHeight * (uBitsPerPixel / );
BITMAPINFO bmpInfo = { };
bmpInfo.bmiHeader.biBitCount = uBitsPerPixel;
bmpInfo.bmiHeader.biHeight = -(static_cast<LONG>(uHeight)); // 因为拿到的帧图像是颠倒的
bmpInfo.bmiHeader.biWidth = uWidth;
bmpInfo.bmiHeader.biPlanes = ;
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
// Pointer to access the pixels of bitmap
UINT * pPixels = ;
hBitmap = CreateDIBSection(hDC, (BITMAPINFO *)&
bmpInfo, DIB_RGB_COLORS, (void **)&
pPixels, NULL, ); if (!hBitmap)
return hBitmap; // return if invalid bitmaps memcpy(pPixels, pBits, lBmpSize); return hBitmap;
} void PaintFrame(HWND hWnd, HDC hDC, RECT rc)
{
FillRect(ghFrameDC, &rc, ghFrameBrush);
HBITMAP hFrame = PopFrame();
if (hFrame)
{
BITMAP bmp;
GetObject(hFrame, sizeof(bmp), &bmp);
HDC hFrameDC = CreateCompatibleDC(hDC);
HBITMAP hOld = (HBITMAP)SelectObject(hFrameDC, hFrame);
BitBlt(ghFrameDC, , , bmp.bmWidth, bmp.bmHeight, hFrameDC, , , SRCCOPY);
SelectObject(hFrameDC, hOld);
DeleteObject(hFrame);
DeleteDC(hFrameDC);
} BitBlt(hDC, , , rc.right - rc.left, rc.bottom - rc.top, ghFrameDC, , , SRCCOPY);
} void OpenVideoByFFmpeg(HWND hWnd, const char* szVideo)
{
AVFormatContext* pFmtCtx = nullptr;
AVCodecContext* pCodecCtx = nullptr;
AVCodec* pCodec = nullptr;
AVFrame* pFrameSrc = nullptr;
AVFrame* pFrameRGB = nullptr;
AVPacket* pPkt = nullptr;
UCHAR* out_buffer = nullptr;
struct SwsContext * pImgCtx = nullptr;
int ret = ;
int videoStream = -;
int numBytes = ; pFmtCtx = avformat_alloc_context();
CHECK_TRUE(pFmtCtx);
ret = avformat_open_input(&pFmtCtx, szVideo, nullptr, nullptr);
CHECK_ZERO(ret);
ret = avformat_find_stream_info(pFmtCtx, nullptr);
CHECK_ZERO(ret); for (UINT i = ; i < pFmtCtx->nb_streams; ++i)
{
if (pFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
break;
}
}
CHECK_ZERO(videoStream); pCodecCtx = avcodec_alloc_context3(nullptr);
CHECK_TRUE(pCodecCtx);
ret = avcodec_parameters_to_context(pCodecCtx, pFmtCtx->streams[videoStream]->codecpar);
CHECK_ZERO(ret);
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
CHECK_TRUE(pCodec);
ret = avcodec_open2(pCodecCtx, pCodec, nullptr);
CHECK_ZERO(ret); pFrameSrc = av_frame_alloc();
pFrameRGB = av_frame_alloc();
CHECK_TRUE(pFrameSrc);
CHECK_TRUE(pFrameRGB); pImgCtx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, nullptr, nullptr, nullptr);
CHECK_TRUE(pImgCtx); numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height, );
out_buffer = (UCHAR*)av_malloc(numBytes);
CHECK_TRUE(out_buffer); ret = av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, out_buffer,
AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height, );
CHECK_ZERO(ret); pPkt = new AVPacket;
ret = av_new_packet(pPkt, pCodecCtx->width * pCodecCtx->height);
CHECK_ZERO(ret); SetWindowPos(hWnd, nullptr, , , pCodecCtx->width + uBorderWidth, pCodecCtx->height + uBorderHeight, SWP_NOMOVE);
ReleasePaint();
CreatePaint(hWnd);
dwWndStyle = GetWindowLong(hWnd, GWL_STYLE);
::SetWindowLong(hWnd, GWL_STYLE, dwWndStyle&~WS_SIZEBOX);
if (!uFrameTimer) uFrameTimer = SetTimer(hWnd, TIMER_FRAME, , nullptr); while (true)
{
if (av_read_frame(pFmtCtx, pPkt) < )
{
break;
} if (pPkt->stream_index == videoStream)
{
ret = avcodec_send_packet(pCodecCtx, pPkt);
if (ret < ) continue;
ret = avcodec_receive_frame(pCodecCtx, pFrameSrc);
if (ret < ) continue; ret = sws_scale(pImgCtx, pFrameSrc->data, pFrameSrc->linesize,
, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
if (ret <= ) continue;
PlayFrame(hWnd, out_buffer, pCodecCtx->width, pCodecCtx->height);
} av_packet_unref(pPkt);
} if (uFrameTimer)
{
KillTimer(hWnd, uFrameTimer);
uFrameTimer = ;
} if (dwWndStyle) ::SetWindowLong(hWnd, GWL_STYLE, dwWndStyle); cleanup:
if (pFmtCtx) avformat_free_context(pFmtCtx);
if (pCodecCtx) avcodec_free_context(&pCodecCtx);
if (pFrameSrc) av_frame_free(&pFrameSrc);
if (pFrameRGB) av_frame_free(&pFrameRGB);
if (pImgCtx) sws_freeContext(pImgCtx);
if (out_buffer) av_free(out_buffer);
if (pPkt)
{
av_packet_unref(pPkt);
delete pPkt;
}
} void LoadVideoPlay(HWND hWnd)
{
if (gbLoadVideo)
{
return;
} TCHAR szPath[] = { };
DWORD dwPath = ;
OPENFILENAME ofn = { };
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hWnd;
ofn.hInstance = hInst;
ofn.lpstrFile = szPath;
ofn.nMaxFile = dwPath;
ofn.lpstrFilter = _T("Video(*.mp4)\0*.MP4*;*.avi*\0");
ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
ofn.lpstrInitialDir = _T("F:\\"); if (!GetOpenFileName(&ofn))
{
DWORD dwErr = CommDlgExtendedError();
OutputDebugString(_T("GetOpenFileName\n"));
return;
} std::wstring strVideo = szPath;
std::thread loadVideoThread([hWnd, strVideo]() {
gbLoadVideo = TRUE;
std::string sVideo = UnicodeToUTF_8(strVideo.c_str(), strVideo.size());
OpenVideoByFFmpeg(hWnd, sVideo.c_str());
gbLoadVideo = FALSE;
}); loadVideoThread.detach();
} //
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目的: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_CREATE:
{
CreatePaint(hWnd); RECT rc;
GetClientRect(hWnd, &rc);
RECT rcWnd;
GetWindowRect(hWnd, &rcWnd);
uBorderWidth = (rcWnd.right - rcWnd.left) - (rc.right - rc.left) + ;
uBorderHeight = (rcWnd.bottom - rcWnd.top) - (rc.bottom - rc.top) + ;
}
break;
case WM_TIMER:
{
if (uFrameTimer && (uFrameTimer == wParam))
{
if (IsIconic(hWnd)) // 如果最小化了,则直接移除图像帧
{
HBITMAP hFrame = PopFrame();
if (hFrame)
{
DeleteObject(hFrame);
}
}
else
{
InvalidateRect(hWnd, nullptr, FALSE);
}
}
}
break;
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_OPEN:
LoadVideoPlay(hWnd);
break;
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
RECT rc;
GetClientRect(hWnd, &rc);
HDC hdc = BeginPaint(hWnd, &ps);
PaintFrame(hWnd, hdc, rc);
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
SetEvent(ghExitEvent);
ReleaseFrames();
PostQuitMessage();
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return ;
} // “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE; case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}

main.cpp

完结撒花