使用 IMFSinkWriter 编码的视频的播放速度根据宽度变化

Posted

技术标签:

【中文标题】使用 IMFSinkWriter 编码的视频的播放速度根据宽度变化【英文标题】:Playback speed of video encoded with IMFSinkWriter changes based on width 【发布时间】:2020-01-19 10:39:23 【问题描述】:

我正在使用Win32s Sink Writer 将一系列位图编码为 MP4 文件来制作屏幕录像机(无音频)。

由于某种原因,视频播放速度(似乎)与视频宽度成比例增加。

从this post 得知,这很可能是因为我计算的缓冲区大小不正确。这里的不同之处在于,一旦音频缓冲区大小的计算正确,他们的视频播放问题就得到了解决,但由于我根本不编码任何音频,我不确定从中获取什么。

我还尝试阅读有关 how the buffer works 的信息,但我真的不知道 缓冲区大小究竟是如何导致播放速度不同的。

Here is a pastebin 对于整个代码,除了缓冲区大小和/或帧索引/持续时间之外,我真的无法找到问题所在。

即: 根据成员变量m_width 的宽度(以像素为单位),播放速度会发生变化。那是;宽度越大,视频播放速度越快,反之亦然。

以下是两个视频示例: 3840x1080 和 640x1080,注意系统时钟。 Imugr 不保留文件的原始分辨率,但我在上传前仔细检查了,该程序确实创建了声称分辨率的文件。

rtStart 和 rtDuration 是这样定义的,并且都是 MP4File 类的私有成员。

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 比特率:15 000 000 输出编码格式:H264 (MP4)

【问题讨论】:

很高兴知道为什么这个问题被否决了。我以为我提供了所有相关信息和链接到我看过的内容。如果缺少我没有想到的任何内容,请发表评论,以便我可以编辑问题.. 我猜你可以改进 Q。首先,您可以链接您获得的录音。其次,不清楚什么是有效的名声指数/持续时间参数。我想它们比假设的与m_width 的相关性更重要,我认为这没有直接影响。 @RomanR。谢谢你澄清这一点。我现在添加了两个视频示例,清楚地展示了调整宽度的剧烈效果,并添加了使用rtStartrtDuration 的每一段代码。希望这能让我的问题更容易理解。 呈现的文件有不同的分辨率,854x720 和 1280x528 @RomanR。抱歉,我认为示例不必是相应的解决方案,以为只是为了演示问题。链接现在已修复,应该有正确的分辨率。 【参考方案1】:

对我来说,这种行为是有道理的。

见https://github.com/mofo7777/***/tree/master/ScreenCaptureEncode

我的程序使用 DirectX9 而不是 GetDIBits,但行为是相同的。用不同的屏幕分辨率尝试这个程序,以确认这种行为。

我确认,在我的程序中,视频播放速度与视频宽度(以及视频高度)成正比。

为什么?

要复制更多数据,需要更多时间。以及错误的采样时间/采样持续时间。

使用 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。

如果您使用 5-10 FPS 而不是 30 FPS 的 MF_MT_FRAME_RATE,您会发现它更真实,但不是最佳的。

例如,使用 IMFPresentationClock 来处理正确的 WriteSample 时间。

【讨论】:

就是这样,丑陋的事实。感谢您指出这一点,我被多个这样做的例子分心了,我真的没有考虑到这一点。我肯定会对此进行更多研究,因为我现在至少知道要寻找什么了。 MSDN 声明应用程序通常不会创建这个时钟,它应该从IMFMediaSession 获取它,但我不使用这样的会话。那我应该创建一个吗?或者尝试实现一个媒体会话? 你能再发一个问题吗? 我会先试一试,如果我遇到困难,最终会发布一个帖子,如果我这样做会在这里告诉你。感谢您的帮助:) 欢迎您。如果你在我所有的 github 项目中搜索,你会找到答案。但我认为这是一个很好的问题,可以帮助人们进行不同类型的项目。

以上是关于使用 IMFSinkWriter 编码的视频的播放速度根据宽度变化的主要内容,如果未能解决你的问题,请参考以下文章

通过 ICodecAPI 为 H.264 IMFSinkWriter 编码器设置属性

IMFSinkWriter 中的错误?

IMFSinkWriter :请求无效,因为已调用 Shutdown()

Media Foundation 将音频流添加到视频文件

网页中加入视频播放组件,并使用ffmpeg对视频转编码

vue在网页上播放h265编码的视频(内附github不能访问的解决办法)