使用 IMFSinkWriter 编码的视频的播放速度根据宽度变化
Playback speed of video encoded with IMFSinkWriter changes based on width
我正在制作一个屏幕录像机(没有音频),使用 Win32s Sink Writer 将一系列位图编码为 MP4 文件。
出于某种原因,视频播放速度(看似)与视频宽度成比例增加。
根据 this post,我认为这很可能是因为我错误地计算了缓冲区大小。这里的不同之处在于,一旦音频缓冲区大小的计算正确,他们的视频播放问题就得到了解决,但是由于我根本没有对任何音频进行编码,所以我不确定从中得到什么。
我也尝试阅读有关 how the buffer works 的内容,但我真的很困惑 缓冲区大小如何 导致不同的播放速度。
Here is a pastebin 对于代码的完整性,除了缓冲区大小 and/or 帧 index/duration.
,我真的无法找到问题所在
即:
根据成员变量 m_width
的宽度(以像素为单位),播放速度会发生变化。那是;宽度越大,视频播放速度越快,反之亦然。
这里有两个视频示例:
3840x1080 and 640x1080,注意系统时钟。
Imugr 不保留文件的原始分辨率,但我在上传前仔细检查,程序确实创建了声明分辨率的文件。
rtStart 和 rtDuration 是这样定义的,并且都是 MP4File class.
的私有成员
LONGLONG rtStart = 0;
UINT64 rtDuration;
MFFrameRateToAverageTimePerFrame(m_FPS, 1, &rtDuration);
这是更新 rtStart 的地方,位图的各个位被传递给帧编写器。
将 LPVOID
对象移动到私有成员以希望提高性能。现在每次附加帧时都不需要堆分配。
HRESULT MP4File::AppendFrame(HBITMAP frame)
{
HRESULT hr = NULL;
if (m_isInitialFrame)
{
hr = InitializeMovieCreation();
if (FAILED(hr))
return hr;
m_isInitialFrame = false;
}
if (m_hHeap && m_lpBitsBuffer) // Makes sure buffer is initialized
{
BITMAPINFO bmpInfo;
bmpInfo.bmiHeader.biBitCount = 0;
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
// Get individual bits from bitmap and loads it into the buffer used by `WriteFrame`
GetDIBits(m_hDC, frame, 0, 0, NULL, &bmpInfo, DIB_RGB_COLORS);
bmpInfo.bmiHeader.biCompression = BI_RGB;
GetDIBits(m_hDC, frame, 0, bmpInfo.bmiHeader.biHeight, m_lpBitsBuffer, &bmpInfo, DIB_RGB_COLORS);
hr = WriteFrame();
if (SUCCEEDED(hr))
{
rtStart += rtDuration;
}
}
return m_writeFrameResult = hr;
}
最后,帧写入器实际将位加载到缓冲区,然后写入接收器写入器。
HRESULT MP4File::WriteFrame()
{
IMFSample *pSample = NULL;
IMFMediaBuffer *pBuffer = NULL;
const LONG cbWidth = 4 * m_width;
const DWORD cbBufferSize = cbWidth * m_height;
BYTE *pData = NULL;
// Create a new memory buffer.
HRESULT hr = MFCreateMemoryBuffer(cbBufferSize, &pBuffer);
// Lock the buffer and copy the video frame to the buffer.
if (SUCCEEDED(hr))
{
hr = pBuffer->Lock(&pData, NULL, NULL);
}
if (SUCCEEDED(hr))
{
hr = MFCopyImage(
pData, // Destination buffer.
cbWidth, // Destination stride.
(BYTE*)m_lpBitsBuffer, // First row in source image.
cbWidth, // Source stride.
cbWidth, // Image width in bytes.
m_height // Image height in pixels.
);
}
if (pBuffer)
{
pBuffer->Unlock();
}
// Set the data length of the buffer.
if (SUCCEEDED(hr))
{
hr = pBuffer->SetCurrentLength(cbBufferSize);
}
// Create a media sample and add the buffer to the sample.
if (SUCCEEDED(hr))
{
hr = MFCreateSample(&pSample);
}
if (SUCCEEDED(hr))
{
hr = pSample->AddBuffer(pBuffer);
}
// Set the time stamp and the duration.
if (SUCCEEDED(hr))
{
hr = pSample->SetSampleTime(rtStart);
}
if (SUCCEEDED(hr))
{
hr = pSample->SetSampleDuration(rtDuration);
}
// Send the sample to the Sink Writer and update the timestamp
if (SUCCEEDED(hr))
{
hr = m_pSinkWriter->WriteSample(m_streamIndex, pSample);
}
SafeRelease(&pSample);
SafeRelease(&pBuffer);
return hr;
}
一些关于编码的细节:
- 帧率:30FPS
- 比特率:15000000
- 输出编码格式:H264(MP4)
对我来说,这种行为是有道理的。
见https://github.com/mofo7777/Whosebug/tree/master/ScreenCaptureEncode
我的程序使用 DirectX9 而不是 GetDIBits,但行为是相同的。使用不同的屏幕分辨率尝试此程序,以确认此行为。
并且我确认,在我的程序中,视频播放速度与视频宽度(以及视频高度)成比例增加。
为什么?
要复制的数据越多,传递的时间就越多。样本 time/sample 持续时间错误。
使用 30 FPS,表示每 33.3333333 毫秒一帧:
- 执行 GetDIBits、MFCopyImage、WriteSample 正好在 33.3333333 毫秒结束...不。
- 你写的每一帧都准确地在 33.3333333 毫秒吗...没有。
所以只执行 rtStart += rtDuration 是错误的,因为此时您没有准确地捕获和写入屏幕。 GetDIBits/DirectX9 无法以 30 FPS 的速度处理,相信我。为什么 Microsoft 提供 Windows 桌面复制(仅限 windows 8/10)?
关键是延迟。
你知道 GetDIBits、MFCopyImage 和 WriteSample 需要多长时间吗?你应该知道,理解问题。通常,它需要超过 33.3333333 毫秒。但它是可变的。
您必须了解它才能为编码器调整正确的 FPS。但是您还需要在适当的时候 WriteSample。
如果您使用 MF_MT_FRAME_RATE 5-10 FPS 而不是 30 FPS,您会发现它更逼真,但不是最优的。
例如,使用 IMFPresentationClock 来处理正确的 WriteSample 时间。
我正在制作一个屏幕录像机(没有音频),使用 Win32s Sink Writer 将一系列位图编码为 MP4 文件。
出于某种原因,视频播放速度(看似)与视频宽度成比例增加。
根据 this post,我认为这很可能是因为我错误地计算了缓冲区大小。这里的不同之处在于,一旦音频缓冲区大小的计算正确,他们的视频播放问题就得到了解决,但是由于我根本没有对任何音频进行编码,所以我不确定从中得到什么。
我也尝试阅读有关 how the buffer works 的内容,但我真的很困惑 缓冲区大小如何 导致不同的播放速度。
Here is a pastebin 对于代码的完整性,除了缓冲区大小 and/or 帧 index/duration.
,我真的无法找到问题所在即:
根据成员变量 m_width
的宽度(以像素为单位),播放速度会发生变化。那是;宽度越大,视频播放速度越快,反之亦然。
这里有两个视频示例: 3840x1080 and 640x1080,注意系统时钟。 Imugr 不保留文件的原始分辨率,但我在上传前仔细检查,程序确实创建了声明分辨率的文件。
rtStart 和 rtDuration 是这样定义的,并且都是 MP4File class.
的私有成员LONGLONG rtStart = 0;
UINT64 rtDuration;
MFFrameRateToAverageTimePerFrame(m_FPS, 1, &rtDuration);
这是更新 rtStart 的地方,位图的各个位被传递给帧编写器。
将 LPVOID
对象移动到私有成员以希望提高性能。现在每次附加帧时都不需要堆分配。
HRESULT MP4File::AppendFrame(HBITMAP frame)
{
HRESULT hr = NULL;
if (m_isInitialFrame)
{
hr = InitializeMovieCreation();
if (FAILED(hr))
return hr;
m_isInitialFrame = false;
}
if (m_hHeap && m_lpBitsBuffer) // Makes sure buffer is initialized
{
BITMAPINFO bmpInfo;
bmpInfo.bmiHeader.biBitCount = 0;
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
// Get individual bits from bitmap and loads it into the buffer used by `WriteFrame`
GetDIBits(m_hDC, frame, 0, 0, NULL, &bmpInfo, DIB_RGB_COLORS);
bmpInfo.bmiHeader.biCompression = BI_RGB;
GetDIBits(m_hDC, frame, 0, bmpInfo.bmiHeader.biHeight, m_lpBitsBuffer, &bmpInfo, DIB_RGB_COLORS);
hr = WriteFrame();
if (SUCCEEDED(hr))
{
rtStart += rtDuration;
}
}
return m_writeFrameResult = hr;
}
最后,帧写入器实际将位加载到缓冲区,然后写入接收器写入器。
HRESULT MP4File::WriteFrame()
{
IMFSample *pSample = NULL;
IMFMediaBuffer *pBuffer = NULL;
const LONG cbWidth = 4 * m_width;
const DWORD cbBufferSize = cbWidth * m_height;
BYTE *pData = NULL;
// Create a new memory buffer.
HRESULT hr = MFCreateMemoryBuffer(cbBufferSize, &pBuffer);
// Lock the buffer and copy the video frame to the buffer.
if (SUCCEEDED(hr))
{
hr = pBuffer->Lock(&pData, NULL, NULL);
}
if (SUCCEEDED(hr))
{
hr = MFCopyImage(
pData, // Destination buffer.
cbWidth, // Destination stride.
(BYTE*)m_lpBitsBuffer, // First row in source image.
cbWidth, // Source stride.
cbWidth, // Image width in bytes.
m_height // Image height in pixels.
);
}
if (pBuffer)
{
pBuffer->Unlock();
}
// Set the data length of the buffer.
if (SUCCEEDED(hr))
{
hr = pBuffer->SetCurrentLength(cbBufferSize);
}
// Create a media sample and add the buffer to the sample.
if (SUCCEEDED(hr))
{
hr = MFCreateSample(&pSample);
}
if (SUCCEEDED(hr))
{
hr = pSample->AddBuffer(pBuffer);
}
// Set the time stamp and the duration.
if (SUCCEEDED(hr))
{
hr = pSample->SetSampleTime(rtStart);
}
if (SUCCEEDED(hr))
{
hr = pSample->SetSampleDuration(rtDuration);
}
// Send the sample to the Sink Writer and update the timestamp
if (SUCCEEDED(hr))
{
hr = m_pSinkWriter->WriteSample(m_streamIndex, pSample);
}
SafeRelease(&pSample);
SafeRelease(&pBuffer);
return hr;
}
一些关于编码的细节:
- 帧率:30FPS
- 比特率:15000000
- 输出编码格式:H264(MP4)
对我来说,这种行为是有道理的。
见https://github.com/mofo7777/Whosebug/tree/master/ScreenCaptureEncode
我的程序使用 DirectX9 而不是 GetDIBits,但行为是相同的。使用不同的屏幕分辨率尝试此程序,以确认此行为。
并且我确认,在我的程序中,视频播放速度与视频宽度(以及视频高度)成比例增加。
为什么?
要复制的数据越多,传递的时间就越多。样本 time/sample 持续时间错误。
使用 30 FPS,表示每 33.3333333 毫秒一帧:
- 执行 GetDIBits、MFCopyImage、WriteSample 正好在 33.3333333 毫秒结束...不。
- 你写的每一帧都准确地在 33.3333333 毫秒吗...没有。
所以只执行 rtStart += rtDuration 是错误的,因为此时您没有准确地捕获和写入屏幕。 GetDIBits/DirectX9 无法以 30 FPS 的速度处理,相信我。为什么 Microsoft 提供 Windows 桌面复制(仅限 windows 8/10)?
关键是延迟。
你知道 GetDIBits、MFCopyImage 和 WriteSample 需要多长时间吗?你应该知道,理解问题。通常,它需要超过 33.3333333 毫秒。但它是可变的。 您必须了解它才能为编码器调整正确的 FPS。但是您还需要在适当的时候 WriteSample。
如果您使用 MF_MT_FRAME_RATE 5-10 FPS 而不是 30 FPS,您会发现它更逼真,但不是最优的。
例如,使用 IMFPresentationClock 来处理正确的 WriteSample 时间。