我正在尝试使用Windows Media Foundation将从图像(RGBA)源(桌面/相机)捕获的RGBA缓冲区编码为原始H264,传输它们并实时解码在另一端接收的原始H264帧。我试图达到至少30 fps。编码器工作得很好但不是解码器。
据我所知,Microsoft WMF MFT在发出编码/解码数据之前最多可缓冲30帧。
只有在发生变化而不是连续的RGBA缓冲流时,图像源才会发出帧,因此我的目的是为每个输入缓冲区获取相应MFT的编码/解码数据缓冲区,以便我可以流实时数据并渲染它。
当我使图像源发送连续变化时(通过激发变化),编码器和解码器都能够发射至少10到15 fps。编码器能够利用硬件加速支持。我能够在编码器端实现高达30 fps的速度,而我还没有使用DirectX曲面实现硬件辅助解码。这里的问题不是帧速率,而是MFT缓冲数据。
因此,我尝试通过发送MFT_MESSAGE_COMMAND_DRAIN命令来耗尽解码器MFT,并重复调用ProcessOutput直到解码器返回MF_E_TRANSFORM_NEED_MORE_INPUT。现在发生的是解码器现在每30个输入h264缓冲区只发出一帧,我用连续的数据流测试它,行为相同。看起来解码器丢弃了GOP中的所有中间帧。
如果它只缓冲前几帧,那对我来说没问题,但是我的解码器实现只有在它的缓冲区一直满,甚至在SPS和PPS解析阶段之后才会输出。
我遇到了谷歌的铬源代码(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工作的实时用例或者像英特尔的QuickSync一样。我选择WMF作为我的POC,因为Windows Media Foundation隐含地支持硬件/ GPU /软件回退,以防任何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);
}
//查找并返回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,与之前相比要好得多。当从源到编码器的更多数量的更改时,质量会下降。我还没有实现硬件加速解码。
Update2:我发现如果设置不正确,时间戳和持续时间设置会影响视频质量。问题是,我的图像源不会以恒定速率发射帧,但看起来编码器和解码器期望帧速率恒定。当我将持续时间设置为常数并以恒定步长递增采样时间时,视频质量似乎更好但不是最大。我不认为我正在做的是正确的方法。有没有办法指定关于可变帧率的编码器和解码器?
Update3:在设置CODECAPI_AVEncMPVDefaultBPictureCount(0),CODECAPI_AVEncCommonRealTime和CODECAPI_AVEncCommonLowLatency属性后,我能够从编码器和解码器获得可接受的性能。然而,要探索硬件加速解码。我希望如果实现硬件解码,我将能够达到最佳性能。
视频质量仍然很差,边缘和曲线也不清晰。文字看起来很模糊,这是不可接受的。视频和图像的质量是可以的,但文本和形状的质量没有。
UPDATE4
似乎有些颜色信息在YUV子采样阶段丢失了。我尝试将RGBA缓冲区转换为YUV2然后返回,但颜色丢失是可见的但不错。由于YUV转换造成的损失没有RGBA-> YUV2 - > H264 - > YUV2 - > RGBA转换后渲染的图像质量差。很明显,YUV2转换不仅是质量损失的唯一原因,而且H264编码器还会导致混叠。如果H264编码没有引入混叠效果,我仍然可以获得更好的视频质量。我要去探索WMV编解码器。唯一让我烦恼的是this,代码运行良好,能够捕获屏幕并将文件中的流保存为mp4格式。这里唯一的区别是我在MFVideoFormat_YUY2输入格式中使用Media foundation变换,而在上述代码中使用MFVideoFormat_RGB32作为输入类型的sink writer方法。我仍然希望通过媒体基金会本身实现更好的质量。如果我分别在MFT_REGISTER_TYPE_INFO(MFTEnum)/ SetInputType中指定MFVideoFormat_ARGB32作为输入格式,则MFTEnum / ProcessInput失败。
原版的:
解码后的图像(RGBA后 - > YUV2 - > H264 - > YUV2 - > RGBA转换):
单击以在新选项卡中打开以查看完整图像,以便您可以看到锯齿效果。
大多数消费者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编码器设置:
根据我的结果,我认为质量好/可接受。
您可以使用MF_MT_MPEG2_PROFILE / MF_MT_MPEG2_LEVEL / MF_MT_AVG_BITRATE更改配置文件/级别/比特率。对于CODECAPI_AVEncCommonQuality,似乎你正在尝试使用本地注册的编码器,因为你在Win7上,将该值设置为100,我猜。
但我不认为这会改变事情。
所以。
这是3个带键盘打印屏幕的截图:
最后两张图片来自同一视频编码文件。视频播放器在不以全屏模式播放时会引入混叠。与原始屏幕相比,使用默认编码器设置,以全屏模式播放的相同编码文件并不是那么糟糕。你应该试试这个。我认为我们必须更密切地关注这一点。
我认为别名来自您的视频播放器,因为不能以全屏模式播放。
PS:我使用视频播放器MPC-HC。
PS2:我的程序需要改进:
编辑
你读过这个:
当延迟应该最小化时,低延迟模式对于实时通信或实时捕获非常有用。但是,低延迟模式也可能降低解码或编码质量。
关于为什么我的程序使用MFVideoFormat_RGB32和你的MFVideoFormat_YUY2。默认情况下,SinkWriter启用转换器。 SinkWriter将MFVideoFormat_RGB32转换为兼容的h264编码器格式。对于Microsoft编码器,请阅读:H.264 Video Encoder
输入格式:
所以没有MFVideoFormat_RGB32。我想,SinkWriter使用Color Converter DSP进行转换。
所以当然,问题不是来自在编码之前将rgb转换为yuv。
PS(最后一个)
像马库斯舒曼说的那样;
H.264更多是针对自然内容而非屏幕捕获而设计的。
他应该提到问题与文本捕获特别相关。
您刚刚找到了编码器限制。我只是认为没有编码器针对文本编码进行优化,具有可接受的延伸,就像我提到的视频播放器渲染一样。
您会在最终视频捕获中看到别名,因为它是影片中的固定信息。全屏播放此电影(与拍摄相同)即可。
在Windows上,根据屏幕分辨率计算文本。所以显示总是好的。
这是我的最后一个结论。