View a markdown version of this page

将您的网络应用程序与 WebRTC 重定向集成 - Amazon WorkSpaces

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

将您的网络应用程序与 WebRTC 重定向集成

WebRTC 重定向允许在会话 WorkSpaces 内运行的 Web 应用程序使用原生客户端音频设备。本主题向Web应用程序开发人员展示了如何检测WebRTC重定向环境并与之集成。

在上启用 WebRTC 重定向后 WorkSpace,Amazon DCV WebRTC 重定向浏览器扩展会在网页中注入代理软件开发工具包。您的应用程序可以检测到此代理,并使用它来将标准的WebRTC API调用(包括UserMedia 获取 RTCPeerConnection 和)重定向到用户的本地设备而不是 WorkSpace远程设备的虚拟设备。与通过 DCV 显示协议的流媒体相比,这提供了明显更好的性能和更低的延迟。

您的应用程序必须处理这两种情况:在启用了WebRTC重定向的情况下在中运行时,以及在未启用WebRTC重定向功能的标准浏览器中运行时。 WorkSpace 代理是可选的,如果不存在,您的应用程序可以回退到标准的WebRTC。

先决条件

面向最终用户

最终用户需要以下内容:

  • A WorkSpace 使用 DCV 协议。有关更多信息,请参阅 WorkSpaces 个人协议

    注意

    目前只有 Windows 支持服务器端的 WebRTC 重定向。 WorkSpaces

  • 来自 clients.amazon workspaces.com 的支持 WorkSpaces 客户端。支持的平台:Windows、macOS、Linux(Ubuntu 22.04 和 Ubuntu 24.04、amd64)和 Web。

  • 通过组策略启用 WebRTC 重定向。启用后,该政策会自动通过注册表在 Chrome 和 Edge 上安装浏览器扩展程序。有关更多信息,请参阅 在 “ WorkSpaces 个人版” WorkSpaces 中管理你的 Windows。如果需要,用户可以手动安装扩展程序:ChromeEdg e。

对于开发者

您的 Web 应用程序必须:

  • 使用标准的 WebRTC API(例如 get 和)UserMedia RTCPeerConnection

  • 包括本主题中所述的检测和初始化代码

如何集成

集成包括四个步骤:检测重定向环境、覆盖 WebRTC API、映射音频元素和处理重新连接。

步骤 1:检测重定向环境

将此初始化代码添加到您的应用程序中。回调将在代理准备就绪时运行,如果重定向不可用,则在超时之后运行。

let proxy = null; function handleInitCallback(result) { if (result.success) { // Redirection is available proxy = result.proxy; console.log('WebRTC redirection enabled, version:', proxy.getVersion()); console.log('Client info:', proxy.clientInfo); // Override WebRTC APIs to use redirection proxy.overrideWebRTC(); // Set up reconnection handling setupReconnectionHandling(); } else { // Not in a redirection environment - use standard WebRTC console.log('WebRTC redirection not available:', result.error); } } // Register the initialization callback if (globalThis.DCVWebRTCPeerConnectionProxyV2) { globalThis.DCVWebRTCPeerConnectionProxyV2.setInitCallback(handleInitCallback, 5000); }

关键点:

  • 检查globalThis.DCVWebRTCPeerConnectionProxyV2以检测扩展

  • setInitCallback()用你的处理器调用然后超时——建议超时 5000 毫秒

  • 回调接收一个带有success布尔值和proxy或的结果对象 error

  • 调用重定proxy.overrideWebRTC()向标准 WebRTC API

第 2 步:使用标准的 WebRTC API

调用后overrideWebRTC(),请正常使用 WebRTC 接口。代理会透明地将它们重定向到本地客户端。

注意

目前不支持视频重定向。video: falsegetUserMedia请求中设置。

// Get user media - automatically redirected const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false // Video redirection not yet supported }); // Create peer connection - automatically redirected const pc = new RTCPeerConnection(configuration); // Everything else works as standard WebRTC pc.addTrack(stream.getAudioTracks()[0], stream); const offer = await pc.createOffer(); await pc.setLocalDescription(offer);

如果您需要访问原始浏览器 API(例如,从远程浏览器而不是本地客户端捕获视频),它们将保留在proxy.overridenApis

// Get the original getUserMedia (runs in remote browser, not redirected) const originalGetUserMedia = proxy.overridenApis.get('navigator.mediaDevices.getUserMedia'); // Available original APIs: // 'navigator.mediaDevices.getUserMedia' // 'navigator.mediaDevices.addEventListener' // 'navigator.mediaDevices.removeEventListener' // 'navigator.mediaDevices.enumerateDevices' // 'navigator.mediaDevices.getDisplayMedia' // 'window.RTCPeerConnection' // 'window.RTCPeerConnection.generateCertificate' // 'window.AudioContext' // 'window.Worker' (experimental) const remoteStream = await originalGetUserMedia.call(navigator.mediaDevices, { audio: false, video: true });

第 3 步:映射音频元素

要播放音频,mapAudioElement()请在设置srcObject任何音频元素之前先调用。

const audioElement = document.querySelector('audio#remote-audio'); // Map the audio element before setting srcObject proxy.mapAudioElement(audioElement); // Now use the audio element normally audioElement.srcObject = remoteStream; await audioElement.play(); // Control playback audioElement.pause(); audioElement.volume = 0.8; audioElement.muted = false; // Change output device await audioElement.setSinkId(deviceId);

步骤 4:处理重新连接

重定向服务可能会暂时不可用,例如由于网络问题或客户端重新连接。

重要

重新连接后,所有先前创建的代理对象都将失效,因为客户端浏览器上下文已完全重新加载。您必须关闭现有连接并创建新的 WebRTC 对象。

function setupReconnectionHandling() { proxy.addStatusChangeEventListener((event) => { switch (event.status) { case 'unavailable': console.error('Redirection lost'); handleRedirectionLost(); break; case 'available': console.info('Redirection restored'); handleRedirectionRestored(); break; } }); } function handleRedirectionLost() { // Try to close cleanly - proxies may throw exceptions after reconnection try { if (peerConnection) { peerConnection.close(); } } catch (error) { console.warn('Error closing peer connection (proxy may be invalid):', error); } showReconnectingMessage(); } function handleRedirectionRestored() { // IMPORTANT: All existing proxy objects are invalid after reconnection. // Create new RTCPeerConnection, MediaStream, and other WebRTC objects. hideReconnectingMessage(); restartCall(); // Must create fresh WebRTC objects }

您可以自定义心跳时间:

proxy.resetHeartbeat({ heartbeatTimeoutMs: 5000, // Time before marking unavailable heartbeatIntervalPeriodMs: 500 // How often to check });

设备枚举

设备更改(例如,用户插入头戴式耳机)会被自动检测到。makeMediaDevicesProxy()用于监听本地客户端上的设备更改:

const mediaDevices = proxy.makeMediaDevicesProxy(); mediaDevices.ondevicechange = async () => { const devices = await navigator.mediaDevices.enumerateDevices(); updateDeviceList(devices); };

用于proxy.clientInfo确定用户从哪个客户端平台进行连接:

// clientInfo contains: // - platform: 'web' | 'windows' | 'macOS' | 'linux' // - version: SDK version string // - userAgent: browser user agent string // - browserDetails: { browser: string, version: string } switch (proxy.clientInfo.platform) { case 'web': console.log('Connecting from web client'); break; case 'windows': console.log('Connecting from Windows native client'); break; case 'macOS': console.log('Connecting from macOS native client'); break; case 'linux': console.log('Connecting from Linux native client'); break; }

高级:使用 Web 音频 API 进行音频处理

在通过对等连接发送音频流之前,您可以使用 Web Audio API 来处理音频流:

const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(micStream); const gainNode = audioContext.createGain(); const destination = audioContext.createMediaStreamDestination(); source.connect(gainNode); gainNode.connect(destination); // Control microphone volume gainNode.gain.value = 0.5; // 50% volume // Use the processed stream pc.addTrack(destination.stream.getAudioTracks()[0], destination.stream);

完整示例

以下示例演示了与初始化、重新连接处理、设备更改检测和呼叫设置的完整集成:

let proxy = null; let peerConnection = null; let localStream = null; function initializeRedirection() { if (globalThis.DCVWebRTCPeerConnectionProxyV2) { globalThis.DCVWebRTCPeerConnectionProxyV2.setInitCallback((result) => { if (result.success) { proxy = result.proxy; proxy.overrideWebRTC(); proxy.addStatusChangeEventListener(handleStatusChange); proxy.resetHeartbeat({ heartbeatTimeoutMs: 5000, heartbeatIntervalPeriodMs: 500 }); setupDeviceChangeListener(); } else { console.log('Redirection not available:', result.error); } }, 5000); } } function handleStatusChange(event) { if (event.status === 'unavailable') { try { if (peerConnection) { peerConnection.close(); peerConnection = null; } if (localStream) { localStream.getTracks().forEach(t => t.stop()); localStream = null; } } catch (error) { console.warn('Error cleaning up (proxies invalid):', error); peerConnection = null; localStream = null; } } else if (event.status === 'available') { startCall(); // Create fresh WebRTC objects } } function setupDeviceChangeListener() { const mediaDevices = proxy.makeMediaDevicesProxy(); mediaDevices.ondevicechange = async () => { const devices = await navigator.mediaDevices.enumerateDevices(); updateDeviceUI(devices); }; } async function startCall() { try { localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); peerConnection.ontrack = (event) => { const remoteAudio = document.querySelector('audio#remote'); if (proxy) { proxy.mapAudioElement(remoteAudio); } remoteAudio.srcObject = event.streams[0]; remoteAudio.play(); }; peerConnection.onicecandidate = (event) => { if (event.candidate) { sendToSignalingServer({ type: 'ice-candidate', candidate: event.candidate }); } }; const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); sendToSignalingServer({ type: 'offer', sdp: offer }); } catch (error) { console.error('Failed to start call:', error); } } initializeRedirection();

测试您的集成

没有重定向

当未安装扩展程序、在外部运行或未通过组策略启用 WebRTC 重定向时 WorkSpace,您的应用程序可以正常运行。通过在普通浏览器中打开应用程序进行测试。

使用重定向

要在启用重定向的情况下进行测试,请执行以下操作:

  1. 创建 WorkSpace 使用 DCV 协议并启用 WebRTC 重定向组策略设置。请参阅在 “ WorkSpaces 个人版” WorkSpaces 中管理你的 Windows

  2. 从 c lients.amazonworkspaces.com 下载并安装 WorkSpaces 客户端,然后连接到。 WorkSpace验证是否安装了 Chrome 或 Edge 扩展程序(设置 GPO 后自动安装)。

  3. 在 WorkSpace 浏览器中打开您的 Web 应用程序。在控制台上查看 “WebRTC重定向已启用” 消息。验证本地设备的音频功能,测试设备枚举和切换,并验证重新连接的处理方式。

最佳实践

在与 WebRTC 重定向集成时,请遵循以下建议:

  • 务必检查重定向的可用性-永远不要假设代理存在

  • 在创建任何 WebRTC 对象之前,请overrideWebRTC()尽早致电

  • 使用前映射音频元素-设置mapAudioElement()前调用 srcObject

  • 处理重新连接-重新连接后,所有代理对象均无效;请务必创建新的 WebRTC 对象

  • 测试这两种模式-确保您的应用程序在重定向和不使用重定向的情况下都能正常运行

  • 使用标准的 WebRTC API-代理会透明地重定向它们;基本用途不需要自定义 API

  • 清理资源-移除事件侦听器并在完成后关闭连接

问题排查

未检测到重定向

  • 验证浏览器扩展程序是否已安装并启用(选中chrome://extensions

  • 确认WebRTC重定向组策略已在上启用 WorkSpace

  • 验证您是否在 WorkSpace 使用 DCV 协议内运行

  • 检查浏览器控制台是否有初始化消息

  • 验证 WorkSpaces 客户端版本是否支持 WebRTC 重定向(Windows 5.21.0 或更高版本、macOS 5.31.0 或更高版本、Linux 2026.0 或更高版本或 Web 客户端)

音频或视频无法正常工作

  • 在创建任何 WebRTC 对象之前overrideWebRTC()已调用验证

  • 在设置mapAudioElement()之前,请验证音频元素是否与之映射 srcObject

  • 检查浏览器控制台是否有错误

  • 验证是否已授予设备权限

重新连接问题

  • 实施addStatusChangeEventListener()以检测可用性变化

  • 重新连接后,请务必创建新的 RTCPeerConnection 和 MediaStream对象-旧的代理无效并且会抛出

  • 如果检测到不可用的速度太慢或太快,请查看心跳配置

API 参考

以下总结了代理 API:

interface DCVWebRTCProxy { // Version and client info getVersion(): string; clientInfo: { platform: 'web' | 'windows' | 'macOS' | 'linux'; version: string; userAgent: string; browserDetails: { browser: string; version: string; }; }; // Core methods overrideWebRTC(): void; mapAudioElement(element: HTMLAudioElement): void; // Device management makeMediaDevicesProxy(): MediaDevices; // Status monitoring addStatusChangeEventListener(callback: (event: StatusChangeEvent) => void): void; resetHeartbeat(config: { heartbeatTimeoutMs: number, heartbeatIntervalPeriodMs: number }): void; // Access original (non-redirected) browser APIs overridenApis: Map<string, Function>; } interface StatusChangeEvent { status: 'available' | 'unavailable'; lastHeartbeat?: number; }