【C++】从零开始,只使用FFmpeg,Win32 API,实现一个播放器(三)
前情提要
前篇:https://www.cnblogs.com/judgeou/p/14728617.html
上一集我们攻略了 Direct3D 11 渲染,充分发挥现代 GPU 的性能。这一集比较轻松,主要是完善剩下需要的功能。
利用垂直同步控制播放速度
正确控制播放速度其实有非常多的方式,比较常见的是将视频和音频同步,或者与外部时钟同步。但这里我要介绍一种比较少见的方式,可以在没有音频的时候使用,就是利用显示屏的垂直同步信号来同步视频画面。
当调用 IDXGISwapChain::Present 并且第一个参数为 1 时,会阻塞线程,直到屏幕完成一帧画面的显示,发送垂直同步信号,才会返回继续执行,利用这一特性,来完成播放速度的正确处理。
假设我们的屏幕刷新率是 60Hz,视频是 30fps,那么处理起来很简单,每 2 个呈现周期,更新一次视频画面即可,可以保证每一帧画面的出现,时机都恰到好处。但如果视频是 24fps,就需要每 2.5 个呈现周期更新一次画面,导致你的视频画面几乎在绝大多数时候会与正确的播放时机错开,你能做的,只能是这帧慢了,下一帧就快点,这一帧快了,下一帧就慢点。
// 获取视频帧率
double GetFrameFreq(const DecoderParam& param) {
auto avg_frame_rate = param.fmtCtx->streams[param.videoStreamIndex]->avg_frame_rate;
auto framerate = param.vcodecCtx->framerate;
if (avg_frame_rate.num > 0) {
return (double)avg_frame_rate.num / avg_frame_rate.den;
}
else if (framerate.num > 0) {
return (double)framerate.num / framerate.den;
}
}
// ...
DEVMODE devMode = {};
devMode.dmSize = sizeof(devMode);
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &devMode);
// 屏幕刷新率
auto displayFreq = devMode.dmDisplayFrequency;
// 记录屏幕呈现了多少帧
int displayCount = 1;
// 记录视频播放了多少帧
int frameCount = 1;
MSG msg;
while (1) {
// ...
if (hasMsg) {
// ...
}
else {
double frameFreq = GetFrameFreq(decoderParam);
double freqRatio = displayFreq / frameFreq;
double countRatio = (double)displayCount / frameCount;
if (freqRatio < countRatio) {
auto frame = RequestFrame(decoderParam);
UpdateVideoTexture(frame, scenceParam, decoderParam);
frameCount++;
av_frame_free(&frame);
}
Draw(d3ddeivce.Get(), d3ddeviceCtx.Get(), swapChain.Get(), scenceParam);
swapChain->Present(1, 0);
displayCount++;
}
}
用 displayCount 和 frameCount 分别记录渲染的帧数和播放的帧数,这两个数字的比值(countRatio)应当与 屏幕刷新率 和 视频帧率 的比值(freqRatio )尽可能接近,所以判断一旦 freqRatio < countRatio 就解码下一帧视频,否则就继续渲染上一次的画面。经过这个改动后,低于或等于屏幕刷新率的视频就可以正常播放了。
但是如果是高刷新率的视频,比如120fps的视频,此时你的屏幕是60帧,那么就要放弃渲染一些帧。
// ...
double frameFreq = GetFrameFreq(decoderParam);
double freqRatio = displayFreq / frameFreq;
double countRatio = (double)displayCount / frameCount;
while (freqRatio < countRatio) {
auto frame = RequestFrame(decoderParam);
frameCount++;
countRatio = (double)displayCount / frameCount;
if (freqRatio >= countRatio) {
UpdateVideoTexture(frame, scenceParam, decoderParam);
}
av_frame_free(&frame);
}
把原来的 if (freqRatio < countRatio) 改为 while (freqRatio < countRatio),这样视频解码一帧后会再触发判断,如果是120fps视频则继续解码下一帧并跳过 UpdateVideoTexture。
这样不管是什么帧率的视频,在什么刷新率的屏幕上都可以以正确的速度播放了。
注意:通过 EnumDisplaySettings 获取的屏幕刷新率其实是不太精确的,实际刷新率通常不是整数,而是带小数点,这里就不深究了,有兴趣的看 DwmGetCompositionTimingInfo。
保持画面比例
先把windows窗体样式改回 WS_OVERLAPPEDWINDOW,方便我们对窗口进行任意缩放。
auto window = CreateWindow(className, L"Hello World 标题", WS_OVERLAPPEDWINDOW, 100, 100, clientWidth, clientHeight, NULL, NULL, hInstance, NULL);
想要保持画面比例,就要根据当前窗口的 width height 对四边形进行缩放调整,要么变胖变瘦,要么变高变矮,这些都属于缩放变换,那么四边形每一个顶点要如何变化呢?答案就是把每一个顶点坐标,乘以相对应的缩放矩阵即可。其他的诸如平移、旋转等也是通过与矩阵相乘实现的:
当物体的顶点数量十分庞大时,在CPU做矩阵变换太耗费时间了,GPU就非常适合干这个活儿。尽管我们只有4个点,但这里还是使用业界标准做法,把矩阵传送到图形管线,在着色器里面对各个顶点进行矩阵乘法。
这里要用上微软提供的库:DirectXMath,已经包含在 Windows SDK 里了,先引入必要的头文件:
#include <DirectXMath.h>
namespace dx = DirectX;
相关函数是在命名空间 DirectX 下的,为了写起来方便,就用 dx 别名代替。
为了把矩阵放进管线,需要一个新的 ID3D11Buffer。
struct ScenceParam {
// ...
ComPtr<ID3D11Buffer> pConstantBuffer;
// ...
int viewWidth;
int viewHeight;
};
在结构体 ScenceParam 添加 ComPtr<ID3D11Buffer> pConstantBuffer
,并且添加两个属性 viewWidth viewHeight,保存当前窗口大小。
修改 InitScence 函数,添加创建常量缓冲区的代码:
void InitScence(ID3D11Device* device, ScenceParam& param, const DecoderParam& decoderParam) {
// ...
// 常量缓冲区
auto constant = dx::XMMatrixScaling(1, 1, 1);
constant = dx::XMMatrixTranspose(constant);
D3D11_BUFFER_DESC cbd = {};
cbd.Usage = D3D11_USAGE_DYNAMIC;
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
cbd.ByteWidth = sizeof(constant);
cbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA csd = {};
csd.pSysMem = &constant;
device->CreateBuffer(&cbd, &csd, ¶m.pConstantBuffer);
// ...
}
因为需要每一帧都更新 pConstantBuffer 的内容,所以 Usage 必须要是 D3D11_USAGE_DYNAMIC,CPUAccessFlags 必须是 D3D11_CPU_ACCESS_WRITE。初始的时候,先给一个 缩放(1, 1, 1) 矩阵,其实就相当于啥也没变,这里注意 XMMatrixTranspose 函数,他把矩阵的行和列置换了,为什么要干这个呢,因为GPU看待矩阵行列的形式反了过来,CPU他是一行一行的读,GPU是一列一列的读。所以传送到GPU前需要处理一下。不过,缩放矩阵就算你不置换,结果都是正常的