捕获游戏音频、视频、屏幕截图和元数据
本文介绍如何捕获游戏视频、音频和屏幕截图,以及如何提交元数据。系统将该元数据嵌入到捕获和广播的媒体中,使你的应用和其他人可以创建被同步到游戏事件的动态体验。
有两种不同方法可以在 UWP 应用中捕获游戏。 用户可以使用内置的系统 UI 启动捕获。 使用此技术捕获的媒体将引入到Microsoft游戏生态系统中,可以通过第一方体验(如Xbox 应用)进行查看和共享,并且不能直接提供给应用或用户。 本文的第一部分将向你演示如何启用和禁用由系统实现的应用捕获以及当应用捕获开始或停止时如何接收通知。
捕获媒体的另外一个方法是使用 Windows.Media.AppRecording 命名空间的 API。 如果在设备上启用了捕获,你的应用可以开始捕获游戏,在经过一段时间后,你可以停止捕获,此时媒体被写入文件。 如果用户已启用历史捕获,则还可以通过指定过去开始时间和记录持续时间来记录已发生的游戏。 这两种技术都会生成一个视频文件,可供应用访问,具体取决于用户选择保存文件的位置。 本文的中间部分将指导你完成这些方案的实现。
Windows.Media.Capture 命名空间用于创建元数据的 API,这些元数据描述被捕获或广播的游戏。 它可以包括文本或数字值,并且具有一个标识各数据项的文本标签。 元数据可以表示发生在某一时刻的“事件”,如当用户在赛车游戏中完成一圈时,或者可以表示持续一段时间的“状态”,如用户当前所处的游戏地图。 元数据被写入到缓存中,由系统为你的应用进行分配和管理。 元数据被嵌入到广播流和捕获的视频文件中,包括内置系统捕获或自定义应用捕获方法。 本文的最后部分向你演示如何写入游戏元数据。
注意
游戏元数据可以嵌入到媒体文件中,而且这些媒体文件有可能不受用户控制而通过网络进行共享,因此你不能在元数据中包括个人身份信息或其他潜在的敏感数据。
启用和禁用系统应用捕获
系统应用捕获由用户使用内置系统 UI 启动。 文件由 Windows 游戏生态系统引入,不提供给你的应用或用户,除非通过 Xbox 应用等第一方体验。 你的应用可以禁用和启用由系统启动的应用捕获,这允许你阻止用户捕获某些内容或游戏。
若要启用或禁用系统应用捕获,只需调用静态方法 AppCapture.SetAllowedAsync 并传递 false 禁用捕获或传递 true 启用捕获。
Windows::Media::Capture::AppCapture::SetAllowedAsync(allowed);
当系统应用捕获开始和停止时接收通知
若要在系统应用捕获开始或结束时接收通知,请先通过调用工厂方法 GetForCurrentView 获取 AppCapture 类的一个实例。 接下来,为 CapturingChanged 事件注册处理程序。
Windows::Media::Capture::AppCapture^ appCapture = Windows::Media::Capture::AppCapture::GetForCurrentView();
appCapture->CapturingChanged +=
ref new TypedEventHandler<Windows::Media::Capture::AppCapture^, Platform::Object^>(this, &App::OnCapturingChanged);
在 CapturingChanged 事件的处理程序中,你可以查看 IsCapturingAudio 和 IsCapturingVideo 属性,以确定是否正在分别捕获音频或视频。 你可能想要更新你的应用的 UI,以指示当前的捕获状态。
void App::OnCapturingChanged(Windows::Media::Capture::AppCapture^ sender, Platform::Object^ args)
{
Platform::String^ captureStatusText = "";
if (sender->IsCapturingAudio)
{
captureStatusText += "Capturing audio.";
}
if (sender->IsCapturingVideo)
{
captureStatusText += "Capturing video.";
}
UpdateStatusText(captureStatusText);
}
将适用于 UWP 的 Windows 桌面扩展添加到你的应用
录制音频和视频以及直接从你的应用捕获屏幕截图的 API 位于 Windows.Media.AppRecording 命名空间中,不包括在通用 API 协定中。 若要访问 API,你必须按照以下步骤将适用于 UWP 的 Windows 桌面扩展的引用添加到你的应用。
- 在 Visual Studio 的解决方案资源管理器中,展开你的 UWP 项目并右键单击引用,然后选择添加引用...。
- 展开通用 Windows 节点并选择扩展。
- 在扩展列表中,选中与你的项目的目标版本匹配的适用于 UWP 的 Windows 桌面扩展条目旁边的复选框。 对于应用广播功能,版本必须为 1709 或更高版本。
- 单击 “确定” 。
获取 AppRecordingManager 的一个实例
AppRecordingManager 类是用于管理应用录制的中央 API。 通过调用工厂方法 GetDefault 来获取此类的一个实例。 使用 Windows.Media.AppRecording 命名空间中的任何 API 之前,应检查该 API 在当前设备上是否存在。 在操作系统版本早于 Windows 10 版本 1709 的设备上,这些 API 不可用。 也可以不检查具体的操作系统版本,改用 ApiInformation.IsApiContractPresent 方法查询 Windows.Media.AppBroadcasting.AppRecordingContract 版本 1.0。 如果此协定存在,则录制 API 在该设备上可用。 本文中的示例代码检查 API 一次,然后在执行后续操作前检查 AppRecordingManager 是否为空。
if (Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
"Windows.Media.AppRecording.AppRecordingContract", 1, 0))
{
m_appRecordingManager = AppRecordingManager::GetDefault();
}
确定你的应用当前是否可以录制
有几种原因导致你的应用当前可能无法捕获音频或视频,包括当前设备不满足录制所需的硬件要求,或另一个应用当前正在广播。 在开始录制前,你可以检查你的应用当前是否能够录制。 调用 AppRecordingManager 对象的 GetStatus 方法,然后检查返回的 AppRecordingStatus 对象的 CanRecord 属性。 如果 CanRecord 返回 false,这意味着你的应用当前无法录制,你可以查看详细信息属性确定原因。 根据具体原因,你可能要向用户显示状态或显示启用应用录制的说明。
bool App::CanRecord()
{
if (m_appRecordingManager == nullptr)
{
return false;
}
AppRecordingStatus^ recordingStatus = m_appRecordingManager->GetStatus();
if (!recordingStatus->CanRecord)
{
AppRecordingStatusDetails^ details = recordingStatus->Details;
if (details->IsAnyAppBroadcasting)
{
UpdateStatusText("Another app is currently broadcasting.");
return false;
}
if (details->IsCaptureResourceUnavailable)
{
UpdateStatusText("The capture resource is currently unavailable.");
return false;
}
if (details->IsGameStreamInProgress)
{
UpdateStatusText("A game stream is currently in progress.");
return false;
}
if (details->IsGpuConstrained)
{
// Typically, this means that the GPU software does not include an H264 encoder
UpdateStatusText("The GPU does not support app recording.");
return false;
}
if (details->IsAppInactive)
{
// Broadcasting can only be started when the application's window is the active window.
UpdateStatusText("The app window to be recorded is not active.");
return false;
}
if (details->IsBlockedForApp)
{
UpdateStatusText("Recording is blocked for this app.");
return false;
}
if (details->IsDisabledByUser)
{
UpdateStatusText("The user has disabled GameBar in Windows Settings.");
return false;
}
if (details->IsDisabledBySystem)
{
UpdateStatusText("Recording is disabled by the system.");
return false;
}
return false;
}
return true;
}
手动开始和停止将你的应用录制到文件
验证你的应用能够录制后,你可以通过调用 AppRecordingManager 对象的 StartRecordingToFileAsync 方法开始新的录制。
在以下示例中,当异步任务失败时,执行第一个然后块。 第二个然后块尝试访问任务结果,如果结果为空,则表示任务已完成。 在这两种情况下,调用下方显示的 OnRecordingComplete 帮助程序方法处理结果。
void App::StartRecordToFile(Windows::Storage::StorageFile^ file)
{
if (m_appRecordingManager == nullptr)
{
return;
}
if (!CanRecord())
{
return;
}
// Start a recording operation to record starting from
// now until the operation fails or is cancelled.
m_recordOperation = m_appRecordingManager->StartRecordingToFileAsync(file);
create_task(m_recordOperation).then(
[this](AppRecordingResult^ result)
{
OnRecordingComplete();
}).then([this](task<void> t)
{
try
{
t.get();
}
catch (const task_canceled&)
{
OnRecordingComplete();
}
});
}
录制操作完成后,检查返回的 AppRecordingResult 对象的已成功属性以确定录制操作是否成功。 如果成功,你可以检查 IsFileTruncated 属性以确定系统是否因为存储原因而被强制截断捕获的文件。 你可以检查持续时间属性,如果文件被截断,你会发现录制文件的实际持续时间可能短于录制操作的持续时间。
void App::OnRecordingComplete()
{
if (m_recordOperation)
{
auto result = m_recordOperation->GetResults();
if (result->Succeeded)
{
Windows::Foundation::TimeSpan duration = result->Duration;
boolean isTruncated = result->IsFileTruncated;
UpdateStatusText("Recording completed.");
}
else
{
// If the recording failed, ExtendedError
// can be retrieved and used for diagnostic purposes
HResult extendedError = result->ExtendedError;
LogTelemetryMessage("Error during recording: " + extendedError);
}
m_recordOperation = nullptr;
}
}
下面的示例显示了一些基本代码,这些代码用于开始和停止在上一示例中显示的录制操作。
StorageFolder^ storageFolder = ApplicationData::Current->LocalFolder;
concurrency::create_task(storageFolder->CreateFileAsync("recordtofile_example.mp4", CreationCollisionOption::ReplaceExisting)).then(
[this](StorageFile^ file)
{
StartRecordToFile(file);
});
void App::FinishRecordToFile()
{
m_recordOperation->Cancel();
}
将历史时间跨度录制到文件
如果用户已经在系统设置中为你的应用启用历史录制,你可以录制之前已经发生的游戏的时间跨度。 本文的上一个示例演示了如何确认你的应用当前可以录制游戏。 你还可以通过其他检查确定是否已启用历史捕获。 再一次调用 GetStatus 并检查返回的 AppRecordingStatus 对象的 CanRecordTimeSpan 属性。 此示例也返回 AppRecordingStatus 的 HistoricalBufferDuration 属性,它将用于确定录制操作的有效开始时间。
bool App::CanRecordTimeSpan(TimeSpan &historicalDurationBuffer)
{
if (m_appRecordingManager == nullptr)
{
return false;
}
AppRecordingStatus^ recordingStatus = m_appRecordingManager->GetStatus();
if (recordingStatus->Details->IsTimeSpanRecordingDisabled)
{
UpdateStatusText("Historical time span recording is disabled by the system.");
return false;
}
historicalDurationBuffer = recordingStatus->HistoricalBufferDuration;
return true;
}
若要捕获历史时间跨度,你必须指定录制的开始时间和持续时间。 开始时间作为 DateTime 结构提供。 开始时间必须早于当前时间,且在历史录制缓冲区的长度内。 在此示例中,检查是否已启用历史录制时要检索缓冲区长度,如上一个代码示例中所示。 历史录制的持续时间作为 TimeSpan 结构提供,它也应等于或小于历史缓冲区的持续时间。 一旦你确定了所需的开始时间和持续时间,则调用 RecordTimeSpanToFileAsync 开始录制操作。
同手动开始和停止录制一样,当历史录制完成后,你可以检查返回的 AppRecordingResult 对象的已成功属性以确定录制操作是否成功,还可以检查 IsFileTruncated 和持续时间属性,如果文件被截断,你会发现录制文件的实际持续时间可能短于请求的时间窗口的持续时间。
void App::RecordTimeSpanToFile(Windows::Storage::StorageFile^ file)
{
if (m_appRecordingManager == nullptr)
{
return;
}
if (!CanRecord())
{
return;
}
Windows::Foundation::TimeSpan historicalBufferDuration;
if (!CanRecordTimeSpan(historicalBufferDuration))
{
return;
}
AppRecordingStatus^ recordingStatus = m_appRecordingManager->GetStatus();
Windows::Globalization::Calendar^ calendar = ref new Windows::Globalization::Calendar();
calendar->SetToNow();
Windows::Foundation::DateTime nowTime = calendar->GetDateTime();
int secondsToRecord = min(30, historicalBufferDuration.Duration / 10000000);
calendar->AddSeconds(-1 * secondsToRecord);
Windows::Foundation::DateTime startTime = calendar->GetDateTime();
Windows::Foundation::TimeSpan duration;
duration.Duration = nowTime.UniversalTime - startTime.UniversalTime;
create_task(m_appRecordingManager->RecordTimeSpanToFileAsync(startTime, duration, file)).then(
[this](AppRecordingResult^ result)
{
if (result->Succeeded)
{
Windows::Foundation::TimeSpan duration = result->Duration;
boolean isTruncated = result->IsFileTruncated;
UpdateStatusText("Recording completed.");
}
else
{
// If the recording failed, ExtendedError
// can be retrieved and used for diagnostic purposes
HResult extendedError = result->ExtendedError;
LogTelemetryMessage("Error during recording: " + extendedError);
}
});
}
下面的示例显示了一些基本代码,这些代码用于启动在上一示例中显示的历史录制操作。
StorageFolder^ storageFolder = ApplicationData::Current->LocalFolder;
concurrency::create_task(storageFolder->CreateFileAsync("recordtimespantofile_example.mp4", CreationCollisionOption::ReplaceExisting)).then(
[this](StorageFile^ file)
{
RecordTimeSpanToFile(file);
});
将屏幕截图图像保存到文件
你的应用可以启动屏幕截图捕获,将应用窗口的当前内容保存到一个图像文件或使用不同的图像编码保存到多个图像文件。 若要指定你希望使用的图像编码,可以创建一个字符串列表,其中每个字符串表示一个图像类型。 ImageEncodingSubtypes 的属性为每个受支持的图像类型提供正确的字符串,如 MediaEncodingSubtypes.Png 或 MediaEncodingSubtypes.JpegXr。
通过调用 AppRecordingManager 对象的 SaveScreenshotToFilesAsync 方法启动屏幕捕获。 此方法的第一个参数是 StorageFolder,即图像文件的保存位置。 第二个参数是文件名前缀,系统会将保存的每个图像类型的扩展名(如“.png”)附加到该前缀后面。
SaveScreenshotToFilesAsync 的第三个参数是当要捕获的当前窗口显示 HDR 内容时,系统能够执行正确的颜色空间转换所需的必要参数。 如果存在 HDR 内容,此参数应设置为 AppRecordingSaveScreenshotOption.HdrContentVisible。 否则,请使用 AppRecordingSaveScreenshotOption.None。 此方法的最后一个参数是应将屏幕捕获到的图像格式的列表。
当异步调用 SaveScreenshotToFilesAsync 完成时,它将返回 AppRecordingSavedScreenshotInfo 对象,该对象提供 StorageFile 和关联的 MediaEncodingSubtypes 值,指示每个保存的图像的图像类型。
void App::SaveScreenShotToFiles(Windows::Storage::StorageFolder^ folder, Platform::String^ filenamePrefix)
{
if (m_appRecordingManager == nullptr)
{
return;
}
Windows::Foundation::Collections::IVectorView<Platform::String^>^ supportedFormats =
m_appRecordingManager->SupportedScreenshotMediaEncodingSubtypes;
Platform::Collections::Vector<Platform::String^>^ requestedFormats =
ref new Platform::Collections::Vector<Platform::String^>();
for (Platform::String^ format : requestedFormats)
{
if (format == Windows::Media::MediaProperties::MediaEncodingSubtypes::Png)
{
requestedFormats->Append(format);
}
else if (format == Windows::Media::MediaProperties::MediaEncodingSubtypes::JpegXr)
{
requestedFormats->Append(format);
}
}
create_task(m_appRecordingManager->SaveScreenshotToFilesAsync(folder, filenamePrefix, AppRecordingSaveScreenshotOption::None,
requestedFormats->GetView())).then(
[this](AppRecordingSaveScreenshotResult^ result)
{
if (result->Succeeded)
{
Windows::Foundation::Collections::IVectorView<AppRecordingSavedScreenshotInfo^>^ returnedScreenshots = result->SavedScreenshotInfos;
for (AppRecordingSavedScreenshotInfo^ screenshotInfo : returnedScreenshots)
{
Windows::Storage::StorageFile^ file = screenshotInfo->File;
Platform::String^ type = screenshotInfo->MediaEncodingSubtype;
}
}
else
{
// If the recording failed, ExtendedError
// can be retrieved and used for diagnostic purposes
HResult extendedError = result->ExtendedError;
LogTelemetryMessage("Error during screenshot: " + extendedError);
}
});
}
下面的示例显示了一些基本代码,这些代码用于启动在上一示例中显示的屏幕截图操作。
StorageFolder^ storageFolder = ApplicationData::Current->LocalFolder;
SaveScreenShotToFiles(storageFolder, "screen_capture");
为系统和应用启动的捕获添加游戏元数据
本文的以下部分介绍如何提供系统将嵌入到已捕获游戏或已广播游戏的 MP4 流中的元数据。 元数据可以嵌入到使用内置系统 UI 捕获的媒体中或由应用使用 AppRecordingManager 捕获的媒体中。 你的应用和其他应用在媒体播放过程中可以提取此元数据,以提供与捕获或广播的游戏同步的依赖于上下文的体验。
获取 AppCaptureMetadataWriter 的一个实例
用于管理应用捕获元数据的主要类是 AppCaptureMetadataWriter。 在初始化此类的实例时,使用 ApiInformation.IsApiContractPresent 方法查询 Windows.Media.Capture.AppCaptureMetadataContract 1.0,以验证 API 在当前设备上可用。
if (Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent("Windows.Media.Capture.AppCaptureMetadataContract", 1, 0))
{
m_appCaptureMetadataWriter = ref new AppCaptureMetadataWriter();
}
将元数据写入到你的应用的系统缓存
每个元数据项都有一个字符串标签,用于标识元数据项、关联的数据值(可能是字符串、整数或双值),以及一个来自 AppCaptureMetadataPriority 枚举的值,该值指示数据项的相对优先级。 元数据项可以被视为发生在单个时间点的“事件”或在一个时间窗口内维持一个值的“状态”。 元数据被写入到内存缓存中,由系统为你的应用进行分配和管理。 系统对元数据的内存缓存强制执行大小限制,并且当达到此限制时,系统将基于每个元数据项被写入时的优先级清除数据。 本文的下一部分演示如何管理你的应用的元数据内存分配。
一个典型的应用可以选择在捕获会话开始时写入一些元数据,以便为后续数据提供一些上下文。 在此方案中,建议你使用即时“事件”数据。 此示例调用 AddStringEvent、AddDoubleEvent 和 AddInt32Event 设置每个数据类型的即时值。
void App::StartSession(Platform::String^ sessionId, double averageFps, int resolutionWidth, int resolutionHeight)
{
if (m_appCaptureMetadataWriter != nullptr)
{
m_appCaptureMetadataWriter->AddStringEvent("sessionId", sessionId, AppCaptureMetadataPriority::Informational);
m_appCaptureMetadataWriter->AddDoubleEvent("averageFps", averageFps, AppCaptureMetadataPriority::Informational);
m_appCaptureMetadataWriter->AddInt32Event("resolutionWidth", resolutionWidth, AppCaptureMetadataPriority::Informational);
m_appCaptureMetadataWriter->AddInt32Event("resolutionHeight", resolutionHeight, AppCaptureMetadataPriority::Informational);
}
}
使用在一段时间内持续存在的“状态”数据的一个常见方案是跟踪玩家目前所在的游戏地图。 此示例调用 StartStringState 设置状态值。
void App::StartMap(Platform::String^ mapName)
{
m_appCaptureMetadataWriter->StartStringState("map", mapName, AppCaptureMetadataPriority::Important);
}
调用 StopState 记录特定状态已结束。
void App::EndMap(Platform::String^ mapName)
{
m_appCaptureMetadataWriter->StopState("map");
}
你可以通过使用现有状态标签设置新值的方式覆盖状态。
void App::LevelUp(int newLevel)
{
m_appCaptureMetadataWriter->StartInt32State("currentLevel", newLevel, AppCaptureMetadataPriority::Important);
}
你可以通过调用 StopAllStates 的方式结束所有当前打开的状态。
void App::RaceComplete()
{
m_appCaptureMetadataWriter->StopAllStates();
}
管理元数据缓存存储限制
你使用 AppCaptureMetadataWriter 写入的元数据由系统进行缓存,直到该元数据被写入到关联的媒体流。 系统定义每个应用的元数据缓存的大小限制。 一旦达到缓存大小限制,系统将开始清除缓存的元数据。 系统将先删除使用 AppCaptureMetadataPriority.Informational 优先级值写入的元数据,然后再删除优先级为 AppCaptureMetadataPriority.Important 的元数据。
在任何时候,你都可以通过调用 RemainingStorageBytesAvailable 的方式检查在你的应用的元数据缓存中可用的字节数。 你可以选择设置你自己的应用定义的阈值,之后可以选择减少你写入到缓存的元数据量。 下面的示例演示了此模式的简单实现。
void App::CheckMetadataStorage()
{
INT64 storageRemaining = m_appCaptureMetadataWriter->RemainingStorageBytesAvailable;
if (storageRemaining < m_myLowStorageLevelInBytes)
{
m_writeLowPriorityMetadata = false;
}
}
void App::ComboExecuted(Platform::String^ comboName)
{
if (m_writeLowPriorityMetadata)
{
m_appCaptureMetadataWriter->AddStringEvent("combo", comboName, AppCaptureMetadataPriority::Informational);
}
}
当系统清除元数据时收到通知
你可以为 MetadataPurged 事件注册一个处理程序,以便当系统开始清除应用的元数据时收到通知。
if (m_appCaptureMetadataWriter != nullptr)
{
m_appCaptureMetadataWriter->MetadataPurged +=
ref new TypedEventHandler<AppCaptureMetadataWriter^, Platform::Object^>(this, &App::OnMetadataPurged);
}
在 MetadataPurged 事件的处理程序中,你可以通过结束低优先级状态的方式清除元数据缓存中的一些空间,可以实施由应用定义的减少写入到缓存的元数据量的逻辑,或者不执行任何操作,让系统继续基于写入的优先级清除缓存。
void App::OnMetadataPurged(Windows::Media::Capture::AppCaptureMetadataWriter^ sender, Platform::Object^ args)
{
// Reduce metadata by stopping a low-priority state.
//m_appCaptureMetadataWriter->StopState("map");
// Reduce metadata by stopping all states.
//m_appCaptureMetadataWriter->StopAllStates();
// Change app-specific behavior to write less metadata.
//m_writeLowPriorityMetadata = false;
// Take no action. Let the system purge data as needed. Record event for telemetry.
OutputDebugString(TEXT("Low-priority metadata purged."));
}
相关主题