Windows Media Foundation MFT 缓冲和视频质量问题(颜色丢失,曲线不那么平滑,尤其是文本)

Windows Media Foundation MFT buffering and video quality issues (Loss of colors, not so smooth curves, especially text)

我正在尝试使用 Windows Media Foundation 将从图像 (RGBA) 源 (Desktop/Camera) 捕获的 RGBA 缓冲区编码为原始 H264,传输它们并解码接收到的原始 H264 帧实时的另一端。我试图达到至少 30 fps。编码器工作得很好,但解码器不行。

我了解 Microsoft WMF MFT 在发出 encoded/decoded 数据之前最多缓冲 30 帧。

The image source would emit frames only when there is a change occurs and not a continuous stream of RGBA buffers, so my intention is to obtain a buffer of encoded/decoded data for each and every input buffer to the respective MFT so that I can stream the data in real time and also render it.

当我使图像源发送连续变化(通过刺激变化)时,编码器和解码器都能够发出至少 10 到 15 fps。编码器能够利用硬件加速支持。我能够在编码器端实现高达 30 fps,而且我还没有使用 DirectX 表面实现硬件辅助解码。这里的问题不是帧速率,而是 MFT 对数据的缓冲。

因此,我尝试通过发送 MFT_MESSAGE_COMMAND_DRAIN 命令来耗尽解码器 MFT,并重复调用 ProcessOutput 直到解码器 returns MF_E_TRANSFORM_NEED_MORE_INPUT。现在发生的是解码器现在每 30 个输入 h264 缓冲区只发出一帧,我什至用连续的数据流测试它,行为是相同的。 看起来解码器丢弃了 GOP 中的所有中间帧。

如果它只缓冲前几帧对我来说没问题,但我的解码器实现仅在缓冲区已满时才输出,即使在 SPS 和 PPS 解析阶段之后也是如此。

我看到了 Google 的 chromium 源代码 (https://github.com/adobe/chromium/blob/master/content/common/gpu/media/dxva_video_decode_accelerator.cc),他们遵循相同的方法。

mpDecoder->ProcessMessage(MFT_MESSAGE_COMMAND_DRAIN, NULL);

我的实现是基于 https://github.com/GameTechDev/ChatHeads/blob/master/VideoStreaming/EncodeTransform.cpp

https://github.com/GameTechDev/ChatHeads/blob/master/VideoStreaming/DecodeTransform.cpp

我的问题是,我是否遗漏了什么? Windows Media Foundation 适合实时流媒体吗?。耗尽编码器和解码器是否适用于实时用例?

我只有两个选择,让这个 WMF 工作在实时用例中,或者使用像 Intel 的 QuickSync 这样的东西。我为我的 POC 选择了 WMF,因为 Windows Media Foundation 隐式支持 Hardware/GPU/Software 回退,以防任何 MFT 不可用,并且它在内部选择最佳可用的 MFT,无需太多编码。

尽管比特率 属性 设置为 3Mbps,但我遇到了视频质量问题。但与缓冲问题相比,它的优先级最低。几个星期以来,我一直在键盘上敲打自己的脑袋,这很难解决。任何帮助将不胜感激。

代码:

编码器设置:

IMFAttributes* attributes = 0;
    HRESULT  hr = MFCreateAttributes(&attributes, 0);

    if (attributes)
    {
        //attributes->SetUINT32(MF_SINK_WRITER_DISABLE_THROTTLING, TRUE);
        attributes->SetGUID(MF_TRANSCODE_CONTAINERTYPE, MFTranscodeContainerType_MPEG4);
    }//end if (attributes)

    hr = MFCreateMediaType(&pMediaTypeOut);
    // Set the output media type.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateMediaType(&pMediaTypeOut);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetGUID(MF_MT_SUBTYPE, cVideoEncodingFormat); // MFVideoFormat_H264
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_AVG_BITRATE, VIDEO_BIT_RATE); //18000000
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeRatio(pMediaTypeOut, MF_MT_FRAME_RATE, VIDEO_FPS, 1); // 30
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeSize(pMediaTypeOut, MF_MT_FRAME_SIZE, mStreamWidth, mStreamHeight);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_MPEG2_PROFILE, eAVEncH264VProfile_High);
    }

    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeRatio(pMediaTypeOut, MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
    }

    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_MAX_KEYFRAME_SPACING, 16);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(CODECAPI_AVEncCommonRateControlMode, eAVEncCommonRateControlMode_UnconstrainedVBR);//eAVEncCommonRateControlMode_Quality, eAVEncCommonRateControlMode_UnconstrainedCBR);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(CODECAPI_AVEncCommonQuality, 100);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_FIXED_SIZE_SAMPLES, FALSE);
    }
    if (SUCCEEDED(hr))
    {
        BOOL allSamplesIndependent = TRUE;
        hr = pMediaTypeOut->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, allSamplesIndependent);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_COMPRESSED, TRUE);
    }

    if (SUCCEEDED(hr))
    {
        hr = mpEncoder->SetOutputType(0, pMediaTypeOut, 0);
    }

// 处理传入的样本。忽略时间戳和持续时间参数,我们只是实时渲染数据。

HRESULT ProcessSample(IMFSample **ppSample, LONGLONG& time, LONGLONG& duration, TransformOutput& oDtn)
{
    IMFMediaBuffer *buffer = nullptr;
    DWORD bufferSize;
    HRESULT hr = S_FALSE;

    if (ppSample)
    {
        hr = (*ppSample)->ConvertToContiguousBuffer(&buffer);

        if (SUCCEEDED(hr))
        {
            buffer->GetCurrentLength(&bufferSize);

            hr = ProcessInput(ppSample);

            if (SUCCEEDED(hr))
            {
                //hr = mpDecoder->ProcessMessage(MFT_MESSAGE_COMMAND_DRAIN, NULL);

                //if (SUCCEEDED(hr)) 
                {
                    while (hr != MF_E_TRANSFORM_NEED_MORE_INPUT)
                    {
                        hr = ProcessOutput(time, duration, oDtn);
                    }
                }
            }
            else
            {
                if (hr == MF_E_NOTACCEPTING)
                {
                    while (hr != MF_E_TRANSFORM_NEED_MORE_INPUT)
                    {
                        hr = ProcessOutput(time, duration, oDtn);
                    }

                }
            }
        }

    }

    return (hr == MF_E_TRANSFORM_NEED_MORE_INPUT ? (oDtn.numBytes > 0 ? oDtn.returnCode : hr) : hr);
}

// 查找并 returns h264 MFT(在子类型参数中给出)如果可用...否则失败。

HRESULT FindDecoder(const GUID& subtype)
{
    HRESULT hr = S_OK;
    UINT32 count = 0;

    IMFActivate  **ppActivate = NULL;

    MFT_REGISTER_TYPE_INFO info = { 0 };

    UINT32 unFlags = MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_ASYNCMFT;

    info.guidMajorType = MFMediaType_Video;
    info.guidSubtype = subtype;

    hr = MFTEnumEx(
        MFT_CATEGORY_VIDEO_DECODER,
        unFlags,
        &info,
        NULL,
        &ppActivate,
        &count
    );

    if (SUCCEEDED(hr) && count == 0)
    {
        hr = MF_E_TOPO_CODEC_NOT_FOUND;
    }

    if (SUCCEEDED(hr))
    {
        hr = ppActivate[0]->ActivateObject(IID_PPV_ARGS(&mpDecoder));
    }

    CoTaskMemFree(ppActivate);
    return hr;
}

// 从编码数据重建样本

HRESULT ProcessData(char *ph264Buffer, DWORD bufferLength, LONGLONG& time, LONGLONG& duration, TransformOutput &dtn)
{
    dtn.numBytes = 0;
    dtn.pData = NULL;
    dtn.returnCode = S_FALSE;

    IMFSample *pSample = NULL;
    IMFMediaBuffer *pMBuffer = NULL;

    // Create a new memory buffer.
    HRESULT hr = MFCreateMemoryBuffer(bufferLength, &pMBuffer);

    // Lock the buffer and copy the video frame to the buffer.
    BYTE *pData = NULL;
    if (SUCCEEDED(hr))
        hr = pMBuffer->Lock(&pData, NULL, NULL);

    if (SUCCEEDED(hr))
        memcpy(pData, ph264Buffer, bufferLength);

    pMBuffer->SetCurrentLength(bufferLength);
    pMBuffer->Unlock();

    // Create a media sample and add the buffer to the sample.
    if (SUCCEEDED(hr))
        hr = MFCreateSample(&pSample);

    if (SUCCEEDED(hr))
        hr = pSample->AddBuffer(pMBuffer);

    LONGLONG sampleTime = time - mStartTime;

    // Set the time stamp and the duration.
    if (SUCCEEDED(hr))
        hr = pSample->SetSampleTime(sampleTime);

    if (SUCCEEDED(hr))
        hr = pSample->SetSampleDuration(duration);

    hr = ProcessSample(&pSample, sampleTime, duration, dtn);

    ::Release(&pSample);
    ::Release(&pMBuffer);

    return hr;
}

// 处理解码器的输出样本

HRESULT ProcessOutput(LONGLONG& time, LONGLONG& duration, TransformOutput& oDtn/*output*/)
{
    IMFMediaBuffer *pBuffer = NULL;
    DWORD mftOutFlags;
    MFT_OUTPUT_DATA_BUFFER outputDataBuffer;
    IMFSample *pMftOutSample = NULL;
    MFT_OUTPUT_STREAM_INFO streamInfo;

    memset(&outputDataBuffer, 0, sizeof outputDataBuffer);

    HRESULT hr = mpDecoder->GetOutputStatus(&mftOutFlags);
    if (SUCCEEDED(hr))
    {
        hr = mpDecoder->GetOutputStreamInfo(0, &streamInfo);
    }

    
    if (SUCCEEDED(hr))
    {
        hr = MFCreateSample(&pMftOutSample);
    }

    
    if (SUCCEEDED(hr))
    {
        hr = MFCreateMemoryBuffer(streamInfo.cbSize, &pBuffer);
    }

    
    if (SUCCEEDED(hr))
    {   
        hr = pMftOutSample->AddBuffer(pBuffer);
    }
    
    if (SUCCEEDED(hr))
    {
        DWORD dwStatus = 0;

        outputDataBuffer.dwStreamID = 0;
        outputDataBuffer.dwStatus = 0;
        outputDataBuffer.pEvents = NULL;
        outputDataBuffer.pSample = pMftOutSample;

        hr = mpDecoder->ProcessOutput(0, 1, &outputDataBuffer, &dwStatus);
    }

    if (SUCCEEDED(hr))
    {
        hr = GetDecodedBuffer(outputDataBuffer.pSample, outputDataBuffer, time, duration, oDtn);
    }

    if (pBuffer)
    {
        ::Release(&pBuffer);
    }

    if (pMftOutSample)
    {
        ::Release(&pMftOutSample);
    }

    return hr;
}

// 将解码后的样本写出来

HRESULT GetDecodedBuffer(IMFSample *pMftOutSample, MFT_OUTPUT_DATA_BUFFER& outputDataBuffer, LONGLONG& time, LONGLONG& duration, TransformOutput& oDtn/*output*/)
{
    // ToDo: These two lines are not right. Need to work out where to get timestamp and duration from the H264 decoder MFT.
    HRESULT hr = outputDataBuffer.pSample->SetSampleTime(time);
    
    if (SUCCEEDED(hr))
    {
        hr = outputDataBuffer.pSample->SetSampleDuration(duration);
    }
    

    if (SUCCEEDED(hr))
    {
        hr = pMftOutSample->ConvertToContiguousBuffer(&pDecodedBuffer);
    }
    
    if (SUCCEEDED(hr))
    {
        DWORD bufLength;
        hr = pDecodedBuffer->GetCurrentLength(&bufLength);
    }
    
    if (SUCCEEDED(hr))
    {
        byte *pEncodedYUVBuffer;
        DWORD buffCurrLen = 0;
        DWORD buffMaxLen = 0;
        pDecodedBuffer->GetCurrentLength(&buffCurrLen);
        pDecodedBuffer->Lock(&pEncodedYUVBuffer, &buffMaxLen, &buffCurrLen);
        ColorConversion::YUY2toRGBBuffer(pEncodedYUVBuffer, 
                                        buffCurrLen, 
                                        mpRGBABuffer,
                                        mStreamWidth,
                                        mStreamHeight,
                                        mbEncodeBackgroundPixels,
                                        mChannelThreshold);

        pDecodedBuffer->Unlock();       
        ::Release(&pDecodedBuffer);

        oDtn.pData = mpRGBABuffer;
        oDtn.numBytes = mStreamWidth * mStreamHeight * 4;
        oDtn.returnCode = hr; // will be S_OK..
    }
        
    return hr;
}

更新: 在启用 CODECAPI_AVLowLatency 模式后,解码器的输出现在令人满意,但是与发送方相比,流中有 2 秒的延迟,我能够达到 15 到 20fps,这比以前好很多。当从源推送到编码器的更改数量更多时,质量会下降。我还没有实现硬件加速解码。

更新2: 我发现如果设置不当,时间戳和持续时间设置会影响视频质量。问题是,我的图像源不以恒定速率发射帧,但看起来编码器和解码器期望恒定帧速率。当我将持续时间设置为常数并以恒定的步长增加采样时间时,视频质量似乎更好但不是最好的。我不认为我正在做的是正确的方法。有没有办法指定编码器和解码器关于可变帧率?

更新3: 在设置 CODECAPI_AVEncMPVDefaultBPictureCount (0) 和 CODECAPI_AVEncCommonLowLatency 属性后,我能够从编码器和解码器获得可接受的性能。尚未探索硬件加速解码。我希望如果实现硬件解码,我能达到最好的性能。

视频质量仍然很差,边缘和曲线不清晰。文字看起来很模糊,这是不可接受的。视频和图像的质量还可以,但文本和形状的质量不行。

更新4

似乎在 YUV 子采样阶段丢失了一些颜色信息。我尝试将 RGBA 缓冲区转换为 YUV2,然后再转换回来,颜色损失是可见的,但还不错。 YUV 转换造成的损失没有 RGBA-> YUV2 -> H264 -> YUV2 -> RGBA 转换后渲染的图像质量差。很明显,不仅 YUV2 转换是质量损失的唯一原因,而且 H264 编码器进一步导致混叠。如果 H264 编码不引入混叠效果,我仍然可以获得更好的视频质量。我将探索 WMV 编解码器。唯一仍然困扰我的是 this,代码运行良好,能够捕获屏幕并将流以 mp4 格式保存在文件中。这里唯一的区别是,与上述代码中使用 MFVideoFormat_RGB32 作为输入类型的接收器编写器方法相比,我使用的是具有 MFVideoFormat_YUY2 输入格式的媒体基础转换。我仍然希望有可能通过 Media Foundation 本身获得更好的质量。如果我分别在 MFT_REGISTER_TYPE_INFO (MFTEnum)/SetInputType 中指定 MFVideoFormat_ARGB32 作为输入格式,那么 MFTEnum/ProcessInput 就会失败。

原文:

解码图像(RGBA -> YUV2 -> H264 -> YUV2 -> RGBA转换后):

Click to open in the new tab to view the full image so that you can see the aliasing effect.

大多数消费者 H.264 编码器将颜色信息子采样到 4:2:0。 (RGB 到 YUV) 这意味着甚至在编码过程开始之前,您的 RGB 位图就会丢失 75% 的颜色信息。 H.264 更适合自然内容而不是屏幕捕获。 但是有些编解码器是专门为实现屏幕内容的良好压缩而设计的。例如:https://docs.microsoft.com/en-us/windows/desktop/medfound/usingthewindowsmediavideo9screencodec 即使您增加 H.264 编码的比特率,您也只能使用原始颜色信息的 25%。

因此您的格式更改如下所示:

您从 1920x1080 红色、绿色和蓝色像素开始。你转换成YUV。现在您拥有 1920x1080 亮度、Cb 和 Cr。其中 Cb 和 Cr 是色差分量。这只是表示颜色的一种不同方式。现在,您 将 Cb 和 Cr 平面 缩放 到其原始大小的 1/4。因此,您生成的 Cb 和 Cr 通道约为 960x540,而您的亮度平面仍为 1920x1080。通过将您的颜色信息从 1920x1080 缩放到 960x540 - 您可以缩小到原始尺寸的 25%。 然后将全尺寸亮度平面和 25% 色差通道传入编码器。这种减少颜色信息的级别称为子采样到 4:2:0。编码器需要二次采样输入,并由媒体框架自动完成。除了选择不同的格式之外,您无能为力。

R = red
G = green
B = blue

Y = luminescence
U = blue difference  (Cb)
V = red difference  (Cr)

YUV用于分离出可以高分辨率存储或高带宽传输的亮度信号(Y), 和两个色度分量(U 和 V),可以减少带宽,二次采样, 压缩,或以其他方式单独处理以提高系统效率。 (维基百科)

Original format

RGB (4:4:4) 3 bytes per pixel

R  R  R  R   R  R  R  R    R  R  R  R   R  R  R  R
G  G  G  G   G  G  G  G    G  G  G  G   G  G  G  G
B  B  B  B   B  B  B  B    B  B  B  B   B  B  B  B

Encoder input format - before H.264 compression

YUV (4:2:0) 1.5 bytes per pixel (6 bytes per 4 pixel)

Y  Y  Y  Y   Y  Y  Y  Y   Y  Y  Y  Y   Y  Y  Y  Y
    UV           UV           UV           UV

我正在尝试了解您的问题。

我的程序 ScreenCaptureEncode 使用默认的 Microsoft 编码器设置 :

  • 个人资料:基线
  • 等级:40
  • CODECAPI_AVEncCommonQuality : 70
  • 比特率:2000000

根据我的结果,我认为质量是 good/acceptable。

您可以将 profile/level/bitrate 更改为 MF_MT_MPEG2_PROFILE/MF_MT_MPEG2_LEVEL/MF_MT_AVG_BITRATE。 对于 CODECAPI_AVEncCommonQuality,您似乎正在尝试使用本地注册的编码器, 因为您使用的是 Win7,所以我想将该值设置为 100。

但我认为这不会显着改变事情。

所以。

这是 3 个带有键盘打印屏幕的屏幕截图:

  • 屏幕
  • 编码屏幕,由视频播放器以全屏模式播放
  • 编码屏幕,由视频播放器以非全屏模式播放

最后两张图片来自同一个视频编码文件。 视频播放器在不以全屏模式播放时会引入锯齿。 同一个编码的文件全屏播放还不错,对比原屏, 和默认的编码器设置。 你应该试试这个。我认为我们必须更仔细地研究一下。

我认为锯齿是由您的视频播放器造成的,因为不是在全屏模式下播放。

PS : 我用的是MPC-HC视频播放器

PS2:我的程序需要改进:

  • (不确定)使用 IDirect3D9Ex 改进缓冲机制。在 Windows7 上,对于渲染,IDirect3D9Ex 更好(无交换缓冲区)。 也许捕获屏幕(待办事项列表)也是如此。
  • 我应该使用两个线程,一个用于捕获屏幕,一个用于编码。

编辑

你读过这个吗:

CODECAPI_AVLowLatencyMode

Low-latency mode is useful for real-time communications or live capture, when latency should be minimized. However, low-latency mode might also reduce the decoding or encoding quality.

关于为什么我的程序使用 MFVideoFormat_RGB32 而你的程序使用 MFVideoFormat_YUY2。默认情况下,SinkWriter 启用转换器。 SinkWriter 将 MFVideoFormat_RGB32 转换为兼容的 h264 编码器格式。对于 Microsoft 编码器,请阅读:H.264 Video Encoder

输入格式:

  • MFVideoFormat_I420
  • MFVideoFormat_IYUV
  • MFVideoFormat_NV12
  • MFVideoFormat_YUY2
  • MFVideoFormat_YV12

所以没有MFVideoFormat_RGB32。我认为 SinkWriter 使用 Color Converter DSP 进行转换。

所以肯定,问题不在于在编码之前将 rgb 转换为 yuv。

PS(最后一个)

就像马库斯舒曼说的那样;

H.264 was more designed for natural content rather than screen capture.

他应该提到这个问题特别与文本捕获有关。

您刚刚发现了编码器的限制。我只是认为没有编码器针对文本编码进行优化,具有可接受的拉伸,就像我提到的视频播放器渲染一样。

您会在最终视频捕获中看到混叠,因为它是电影中的固定信息。全屏播放这部电影(与捕获相同)是可以的。

在Windows上,文本是根据屏幕分辨率计算的。所以显示总是好的。

这是我最后的结论。

经过这么多的研究和努力,问题已经解决了。颜色质量问题是由于基于软件的颜色转换导致混叠(编码器中的 RGB 到 YUV,然后在解码器中返回)。使用 hardware-accelerated color convertor 解决了锯齿和图像质量问题。

将最佳值设置为 CODECAPI_AVEncMPVGOPSize、CODECAPI_AVEncMPVDefaultBPictureCount 和 CODECAPI_AVEncCommonLowLatency 属性解决了缓冲问题。