前篇: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,保存当前窗口大小。