媒体基础转换解码
Xbox 游戏开发工具包 (GDK) 支持通过媒体基础源阅读器接口对 H264 和 HEVC 文件/数据流进行硬件和软件解码。 与源阅读器的桌面版本相比,Xbox GDK 版本仅支持桌面功能集的一个子集。
支持的编解码器
硬件解码
编解码器 | 最大分辨率/帧速率 (fps) | 档案 | 主机类型 |
---|---|---|---|
H264 | 高达 1080p/30 | 基线/主要/高 | 全部 Xbox 主机 |
HEVC | 高达 1080p/30 | Main/Main10 | Xbox One S、Xbox One X |
HEVC | 高达 4k/30 | Main/Main10 | Xbox Series S、Xbox Series X |
软件解码
H264 软件解码最高支持 4096x2160@30 fps(5.2 级)。 但是,请注意,性能将取决于主机类型,Xbox Series S/X 能够轻松解码 > 1080p 的分辨率。
还支持对高达 1920x1080@30 fps 的 HEVC 软件解码,其性能取决于主机类型。 但是,我们建议对 HEVC 使用硬件解码,因为与 H264 软件解码相比,它对 CPU 的占用要高得多。
解码媒体数据
使用源阅读器解码媒体数据涉及以下步骤。
请参阅随 GDK 示例提供的 Mp4Reader 示例以获取参考代码(此处的代码片段来自 Mp4reader,以便更易于理解)。
步骤 1:创建源阅读器
要创建源阅读器实例,请调用 MFCreateSourceReaderFromByteStream 函数,该函数采用指向字节流的指针。 此函数还使用源解析程序创建媒体源。
MFCreateSourceReaderFromByteStream
函数的 pByteStream
参数采用指向 IMFAttributes 接口的指针,该接口用于在源阅读器上设置多个选项(如 IMFAttributes::Set
方法的参考主题中所述)。 将此参数设置为 nullptr
即使用默认行为,但不建议这样做。 要启用硬件加速解码,源阅读器至少应始终指定 MF_SOURCE_READER_D3D_MANAGER
属性。
MFCreateSourceReaderFromByteStream
函数会输出一个指向 IMFSourceReader 接口的指针。
请注意,桌面的源读取器同时支持同步模式和异步模式。 Xbox 游戏开发工具包 (GDK) 仅支持同步模式,这是默认设置。
可以指定 m3u8 或 Smoothstreaming 清单 URL 和用于创建源阅读器的输入以及基本 HLS/Smoothstreaming 支持。 对于 Mp4 文件输入,有一些限制:
- AAC-LC 音频和 AC3 是唯一支持的音频格式。
- 对于 HEVC 数据,格式标签必须是“hvc1”(不支持“hev1”)。
C++
// Initialize the Media Foundation platform.
DX::ThrowIfFailed(MFStartup(MF_VERSION));
// Call the MFCreateDXGIDeviceManager function to create the Direct3D device manager
DX::ThrowIfFailed(MFCreateDXGIDeviceManager(&uResetToken, &pDXVAManager));
// Call the MFResetDXGIDeviceManagerX function with a pointer to the Direct3D device
DX::ThrowIfFailed(MFResetDXGIDeviceManagerX(pDXVAManager.Get(), device, uResetToken));
// Create an attribute store
DX::ThrowIfFailed(pDXVAManager.AsIID(__uuidof(IUnknown), &punkDeviceMgr));
DX::ThrowIfFailed(MFCreateAttributes(&pMFAttributes, 3));
DX::ThrowIfFailed(pMFAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, punkDeviceMgr.Get()));
DX::ThrowIfFailed(pMFAttributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE));
DX::ThrowIfFailed(pMFAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE));
// Don't set the MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING MFAttribute to TRUE. It is too slow.
// Create the source reader.
DX::ThrowIfFailed(MFCreateSourceReaderFromURL(INPUT_FILE_PATH, pMFAttributes.Get(), &m_pReader));
步骤2:使用硬件(或软件)加速
源阅读器与 Microsoft DirectX 视频加速 (DXVA) 2.0 兼容,以支持硬件加速视频解码。 要将 DXVA 与源阅读器一起使用,请执行以下步骤。
- 调用 MFCreateDXGIDeviceManager 函数以创建 Direct3D 设备管理器。 此函数采用指向 IMFDXGIDeviceManager 接口的指针。
- 使用指向 Direct3D 设备的指针调用
MFResetDXGIDeviceManagerX
方法。 - 通过调用 MFCreateAttributes 函数创建属性存储。
- 调用 SetUnknown,将
MF_SOURCE_READER_D3D_MANAGER
属性设置为 IMFDXGIDeviceManager 接口。 - 同时启用
MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS
并将MF_SOURCE_READER_DISABLE_DXVA
设置为 FALSE。 - 创建源阅读器。 传递创建函数的
pAttributes
参数中的属性存储。
软件解码
要启用软件解码,只需将 MF_SOURCE_READER_DISABLE_DXVA
设置为 TRUE。 通过软件解码,可以配置一系列其他参数,如下所示,用于视频解码器上的细粒度线程控制,例如线程关联掩码、工作线程数量和线程优先级别。
C++
// Set some threading parameters for software decode
Microsoft::WRL::ComPtr<IMFTransform> transform;
Microsoft::WRL::ComPtr<IMFAttributes> attributes;
DX::ThrowIfFailed(m_pReader->GetServiceForStream(MF_SOURCE_READER_FIRST_VIDEO_STREAM, GUID_NULL, IID_PPV_ARGS(transform.GetAddressOf())));
transform->GetAttributes(attributes.GetAddressOf());
attributes->SetUINT32(CODECAPI_AVDecVideoThreadAffinityMask, 0x0F); // Use the first 4 cores. Input value is the same as Win32 SetThreadAffinityMask
attributes->SetUINT32(CODECAPI_AVDecNumWorkerThreads, 0x4); // 4 threads
attributes->SetUINT32(CODECAPI_AVPriorityControl, THREAD_PRIORITY_HIGHEST); // Set thread priority. Same as Win32 SetThreadPriority for priorty levels
步骤 3:枚举输出格式
每个媒体源都至少有一个流。 例如,视频文件可能包含视频流和音频流。 每个流的格式用媒体类型描述,后者用 IMFMediaType 接口表示。 有关媒体类型的详细信息,请参阅媒体类型。 您必须检查媒体类型以了解您从源阅读器获得的数据的格式。
最初,每个流都有一种默认格式,您可以通过调用 IMFSourceReader::GetCurrentMediaType 方法找到该格式。
对于每个流,媒体源为该流提供可能媒体类型的列表。 类型数目取决于源。 如果源表示一个媒体文件,则每个流通常只有一种类型。 另一方面,网络摄像头可能使用几种不同的格式对视频进行流式处理。 在这种情况下,应用可以从媒体类型列表中选择要使用的格式。
要获取流的某一种媒体类型,请调用 IMFSourceReader::GetNativeMediaType 方法。 此方法有两个索引参数:流的索引和该流的媒体类型列表的索引。 要枚举某个流的所有类型,请在保持流索引不变的同时递增列表索引。 当列表索引超出界限时,GetNativeMediaType
会返回 MF_E_NO_MORE_TYPES
,如以下代码所示。
C++
HRESULT EnumerateTypesForStream(IMFSourceReader *pReader, DWORD dwStreamIndex)
{
HRESULT hr = S_OK;
DWORD dwMediaTypeIndex = 0;
while (SUCCEEDED(hr))
{
IMFMediaType *pType = NULL;
hr = pReader->GetNativeMediaType(dwStreamIndex, dwMediaTypeIndex, &pType);
if (hr == MF_E_NO_MORE_TYPES)
{
hr = S_OK;
break;
}
else if (SUCCEEDED(hr))
{
// Examine the media type here.
pType->Release();
}
++dwMediaTypeIndex;
}
return hr;
}
要枚举每个流的媒体类型,请递增流索引。 当流索引超出边界时,IMFSourceReader::GetNativeMediaType 会返回 MF_E_INVALIDSTREAMNUMBER
。
步骤 4:设置输出格式
要对流进行解码,请创建一个新的媒体类型用来描述所需未压缩格式。 对于解码器,请按如下方式创建媒体类型。
- 调用 MFCreateMediaType 以创建新的媒体类型。
- 设置 MF_MT_MAJOR_TYPE 属性以指定音频或视频。
- 设置 MF_MT_SUBTYPE 属性以指定解码格式的子类型。 若要了解可用的子类型,请参阅音频子类型 GUID 和视频子类型 GUID。 在 Xbox 上,我们仅支持用于 8 位视频解码的 NV12 和用于 10 位视频解码的 P010。
- 调用 SetCurrentMediaType。
源阅读器将自动加载解码器。 若要获取解码格式的完整详细信息,请在调用 SetCurrentMediaType 后调用 IMFMediaTypeHandler::GetCurrentMediaType。 以下代码为 NV12 配置视频流,为浮点音频配置音频流。
C++
HRESULT ConfigureDecoder(IMFSourceReader *pReader, DWORD dwStreamIndex)
{
ComPtr<IMFMediaType> pNativeType;
ComPtr<IMFMediaType> pType;
GUID majorType, subtype;
// Find the native format of the stream.
DX::ThrowIfFailed(pReader->GetNativeMediaType(dwStreamIndex, 0, &pNativeType));
// Find the major type.
DX::ThrowIfFailed(pNativeType->GetGUID(MF_MT_MAJOR_TYPE, &majorType));
// Define the output type.
DX::ThrowIfFailed(MFCreateMediaType(&pType));
DX::ThrowIfFailed(pType->SetGUID(MF_MT_MAJOR_TYPE, majorType));
// Select a subtype.
if (majorType == MFMediaType_Video)
{
// NV12 for 8 bit (or P010 for 10 bit)are the only supported output types of Xbox One HW decoders
subtype = MFVideoFormat_NV12;
}
else if (majorType == MFMediaType_Audio)
{
subtype = MFAudioFormat_Float;
}
else
{
// Unrecognized type. Skip.
return;
}
DX::ThrowIfFailed(pType->SetGUID(MF_MT_SUBTYPE, subtype));
// Set the uncompressed format.
DX::ThrowIfFailed(pReader->SetCurrentMediaType(dwStreamIndex, nullptr, pType.Get()));
}
步骤 5:处理媒体数据
要从源获取媒体数据,请调用 IMFSourceReader::ReadSample 方法,如以下代码所示。
C++
DWORD streamIndex, flags;
LONGLONG llTimeStamp;
hr = pReader->ReadSample(
MF_SOURCE_READER_ANY_STREAM, // Stream index.
0, // Flags.
&streamIndex, // Receives the actual stream index.
&flags, // Receives status flags.
&llTimeStamp, // Receives the time stamp.
&pSample // Receives the sample or nullptr.
);
第一个参数是你想要获取其数据的流的索引。 你还可指定 MF_SOURCE_READER_ANY_STREAM
,从任何流获取下一可用数据。 第二个参数包含可选标志。 有关这些标志的列表,请参阅 MF_SOURCE_READER_CONTROL_FLAG。 第三个参数接收实际生成数据的流的索引。 如果将第一个参数设置为 MF_SOURCE_READER_ANY_STREAM
,则需要此信息。 第四个参数接收状态标志,它们指示在读取数据时可能出现的各种事件,比如流中格式出现更改。 有关状态标志的列表,请参阅 MF_SOURCE_READER_FLAG。
如果媒体源能够为请求的流生成数据,则 ReadSample 的最后一个参数将收到指向媒体样本对象的 IMFSample 接口的指针。 使用媒体样本:
- 获取指向媒体数据的指针。
- 获取演示时间和样本持续时间。
- 获取描述样本的交错、场控制和其他方面的属性。
媒体数据的内容取决于流的格式。 对于未压缩的视频流,每个媒体样本都包含一个视频帧。 对于未压缩的音频流,每个媒体样本都包含一个音频帧序列。
ReadSample 方法可返回 S_OK
,但尚不返回 pSample
参数中的媒体样本。 例如,当到达文件末尾时,ReadSample
会在 dwFlags
中设置 MF_SOURCE_READERF_ENDOFSTREAM
标志,并将 pSample
设置为 nullptr
。 在此情况下,即使 pSample
参数设置为 nullptr
,ReadSample
方法也会返回 S_OK
,因为未发生错误。 因此,在取消引用 pSample
的值之前,始终检查该值。
在调用 ReadSample 之后,视频示例需要一些额外处理。 这包括根据 MF_MT_FRAME_SIZE 或 MF_MT_MINIMUM_DISPLAY_APERTURE 属性确定视频宽度和高度以及处理同步。
以下代码片段调用 ReadSample
,然后检查第一个视频流的方法返回的信息。 有关如何处理音频流数据,请参阅 Mp4reader 示例。 下一节将介绍硬件视频解码操作所需的同步。
C++
// Retreive sample from source reader
ComPtr<IMFSample> pOutputSample;
hr = m_pReader->ReadSample(
uint32_t(MF_SOURCE_READER_FIRST_VIDEO_STREAM), // Stream index.
0, // Flags.
&streamIndex, // Receives the actual stream index.
&dwStreamFlags, // Receives status flags.
&llTimestamp, // Receives the time stamp.
&pOutputSample // Receives the sample or nullptr. If this parameter receives a non-NULL pointer, the caller must release the
// interface.
);
if (SUCCEEDED(hr))
{
if (dwStreamFlags & MF_SOURCE_READERF_ENDOFSTREAM)
{
m_videoDone = true;
}
if (dwStreamFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED)
{
// The format changed. Reconfigure the decoder.
ConfigureSourceReaderOutput(m_pReader.Get(), streamIndex);
}
if (pOutputSample)
{
if (m_videoWidth == 0 || m_videoHeight == 0
|| (dwStreamFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) || (dwStreamFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED))
{
// Update video width and height
ComPtr<IMFMediaType> pMediaType;
if (SUCCEEDED(m_pReader->GetCurrentMediaType(uint32_t(MF_SOURCE_READER_FIRST_VIDEO_STREAM), &pMediaType)))
{
MFVideoArea videoArea = {};
if (SUCCEEDED(pMediaType->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (uint8_t*)&videoArea, sizeof(MFVideoArea), nullptr)))
{
m_videoWidth = UINT(videoArea.Area.cx);
m_videoHeight = UINT(videoArea.Area.cy);
}
else
{
DX::ThrowIfFailed(MFGetAttributeSize(pMediaType.Get(), MF_MT_FRAME_SIZE, &m_videoWidth, &m_videoHeight));
}
}
}
if (m_pOutputVideoSample)
{
DX::ThrowIfFailed(MFD3D12GpuSignalSampleFree(m_pVideoRender->GetVideoProcessCommandQueue(), m_pOutputVideoSample.Get()));
m_pOutputVideoSample.Reset();
}
// The output buffer may still used by decoding ( although decode returns the buffer from CPU), put a wait single on the GPU to wait to the decoding to complete
DX::ThrowIfFailed(MFD3D12GpuWaitForSampleReady(m_pVideoRender->GetVideoProcessCommandQueue(), pOutputSample.Get()));
m_pOutputVideoSample = pOutputSample;
++m_numberOfFramesDecoded;
}
}
通过 IMFSourceReader::SetCurrentPosition API 支持搜索。
C++
PROPVARIANT position;
position.vt = VT_I8;
position.hVal.QuadPart = hnsPosition;
m_pReader->SetCurrentPosition(GUID_NULL, position);
步骤 6:与图形(或其他 D3D12 命令队列)同步
在 Xbox GDK 中,由调用者确保来自硬件解码器的视频示例与图形或其他 D3D12 队列正确同步。 有 2 个新的 API 可以帮助解决这个问题:
-
EnqueueResourceReadyWait
将在传入的命令队列中添加一个等待,以确保该操作在处理示例之前等待解码完成。 这必须在ReadSample
之后和其他队列开始处理视频示例之前立即调用。 -
EnqueueResourceRelease
将确保解码器等待传入命令队列中的任何挂起操作(它将要求命令队列发出内部围栏的信号)。 必须在其他命令队列上的所有处理完成后调用此命令,以将示例返回到池中以供重复使用。
C++
// For D3D12, application must call MFD3D12GpuSignalForSampleFree after it has finished processing the video sample
HRESULT MFD3D12GpuSignalSampleFree(
_In_ ID3D12CommandQueue* pCmdQueue,
_In_ IMFSample* pVideoSample)
{
ComPtr<IMFMediaBuffer> pBuffer;
HRESULT hr = pVideoSample->GetBufferByIndex(0, &pBuffer);
if (SUCCEEDED(hr))
{
ComPtr<IMFDXGIBuffer> pDXGIBuffer;
hr = pBuffer->QueryInterface<IMFDXGIBuffer>(&pDXGIBuffer);
if (SUCCEEDED(hr))
{
ComPtr<IMFD3D12SynchronizationObjectCommands> pMFSyncObj;
hr = pDXGIBuffer->GetUnknown(MF_D3D12_SYNCHRONIZATION_OBJECT, IID_PPV_ARGS(&pMFSyncObj));
if (SUCCEEDED(hr))
{
//GPU signal the sample can be freed for decoding
hr = pMFSyncObj->EnqueueResourceRelease(pCmdQueue);
}
}
}
return hr;
}
// For D3D12, application must call MFD3D12GpuWaitForSampleReady to make sure the GPU waits for decode to complete before process the sample using GPU code
HRESULT MFD3D12GpuWaitForSampleReady(
_In_ ID3D12CommandQueue* pCmdQueue,
_In_ IMFSample* pVideoSample)
{
ComPtr<IMFMediaBuffer> pBuffer;
HRESULT hr = pVideoSample->GetBufferByIndex(0, &pBuffer);
if (SUCCEEDED(hr))
{
ComPtr<IMFDXGIBuffer> pDXGIBuffer;
hr = pBuffer->QueryInterface<IMFDXGIBuffer>(&pDXGIBuffer);
if (SUCCEEDED(hr))
{
ComPtr<IMFD3D12SynchronizationObjectCommands> pMFSyncObj;
hr = pDXGIBuffer->GetUnknown(MF_D3D12_SYNCHRONIZATION_OBJECT, IID_PPV_ARGS(&pMFSyncObj));
if (SUCCEEDED(hr))
{
// GPU wait until the decoding completed
hr = pMFSyncObj->EnqueueResourceReadyWait(pCmdQueue);
}
}
}
return hr;
}
软件解码不需要这些同步调用。 从 ReadSample
返回的示例将始终具有完整的解码帧。
第 7 步:格式和颜色空间转换
现在可以使用 D3D12 视频处理器 API 执行解码输出的颜色空间转换和格式转换。 Xbox GDK 仅支持 YUV (DXGI_FORMAT_NV12
/DXGI_FORMAT_P010
) 到 RGB (DXGI_FORMAT_R10G10B10A2_UNORM
/DXGI_FORMAT_R8G8B8A8_UNORM
包括其他 8 位 RGB/无类型变体) 的格式转换。
除了 YUV-RGB 格式转换外,如果需要,还可以使用视频处理器同时进行颜色空间转换。 支持的转换包括:
- BT.2020 至 BT.709
- BT.709 至 BT.2020
有关适当的颜色空间类型,请参阅 DXGI_COLOR_SPACE_TYPE 枚举。
创建视频处理器
调用 ID3D12VideoDevice::CreateVideoProcessor 创建 ID3D12VideoProcessor
的实例。 视频处理器保持视频处理会话的状态,包括所需的中间内存、缓存的处理数据或其他临时工作空间。 视频处理器创建参数指定在 ID3D12VideoProcessCommandList1::ProcessFrames
时间执行或可用的操作。
C++
D3D12_VIDEO_PROCESS_INPUT_STREAM_DESC inputStreamDesc{};
inputStreamDesc.Format = DXGI_FORMAT_NV12;
inputStreamDesc.ColorSpace = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
inputStreamDesc.SourceSizeRange = D3D12_VIDEO_SIZE_RANGE{ g_MaxVideoWidth, g_MaxVideoHeight, 1, 1 };
inputStreamDesc.DestinationSizeRange = D3D12_VIDEO_SIZE_RANGE{ g_MaxVideoWidth, g_MaxVideoHeight, 1, 1 };
D3D12_VIDEO_PROCESS_OUTPUT_STREAM_DESC outputStreamDesc{};
outputStreamDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
outputStreamDesc.ColorSpace = DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709;
hr = pVideoDevice->CreateVideoProcessor(0, &outputStreamDesc, 1, &inputStreamDesc, IID_GRAPHICS_PPV_ARGS(m_pVideoProcessor.GetAddressOf()));
视频处理器可用于记录来自多个命令列表的命令,但一次只能与一个命令列表关联。 应用程序负责同步访问视频处理器。 应用程序还必须按照在 GPU 上执行的顺序来记录针对视频处理器的视频处理命令。
从 IMFSample 查询输出纹理
以下代码片段显示了如何从 IMFSample 查询解码的 D3D12 输出纹理以用于视频处理或其他图形操作
C++
IMFSample* pSample = pOutputDecodedSample;
HRESULT hr = pSample->GetBufferCount(&bufferCount);
if (FAILED(hr))
return hr;
assert(bufferCount == 1);
ComPtr<IMFMediaBuffer> spBuffer;
hr = pSample->GetBufferByIndex(0, spBuffer.GetAddressOf());
if (FAILED(hr))
return hr;
ComPtr<IMFDXGIBuffer> spDXGIBuffer;
hr = spBuffer.Get()->QueryInterface(spDXGIBuffer.GetAddressOf());
if (FAILED(hr))
return hr;
ComPtr<ID3D12Resource> spResourceTexture;
hr = spDXGIBuffer->GetResource(IID_GRAPHICS_PPV_ARGS(spResourceTexture.GetAddressOf()));
if (FAILED(hr))
return hr;
UINT32 uiIndexSrc;
hr = spDXGIBuffer->GetSubresourceIndex(&uiIndexSrc);
if (FAILED(hr))
return hr;
视频处理器命令执行
创建和执行视频处理器命令使用标准 D3D12 工作提交模型,使用 D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS
命令列表/队列/分配器。 以下示例代码显示了如何创建它们:
C++
HRESULT hr = pDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS, IID_GRAPHICS_PPV_ARGS(m_pVpCommandAllocator.GetAddressOf()));
if (FAILED(hr))
return hr;
D3D12_COMMAND_QUEUE_DESC descQueue{};
descQueue.Type = D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS;
descQueue.Priority = 0;
descQueue.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
hr = pDevice->CreateCommandQueue(&descQueue, IID_GRAPHICS_PPV_ARGS(m_pVpCommandQueue.GetAddressOf()));
if (FAILED(hr))
return hr;
hr = pDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS, m_pVpCommandAllocator.Get(), nullptr, __uuidof(ID3D12CommandList), reinterpret_cast<void**>(m_pVpCommandList.GetAddressOf()));
if (FAILED(hr))
return hr;
视频处理操作的所有输入和输出参数都组织成一个输入参数结构 D3D12_VIDEO_PROCESS_INPUT_STREAM_ARGUMENTS 和一个输出参数结构 D3D12_VIDEO_PROCESS_OUTPUT_STREAM_ARGUMENTS。 应用程序必须调用 ID3D12VideoProcessCommandList::ProcessFrames 来记录它要执行的视频处理操作。
记录命令列表后,在视频处理器命令队列上调用 ID3D12CommandQueue::ExecuteCommandLists
,将帧处理提交给 GPU。 注意:在 Xbox Series S/X 上,需要同步对视频处理器命令队列和图形命令队列的访问,因为多线程访问可能会导致意外行为。