View a markdown version of this page

将您的 Web 应用程序与 WebRTC 重定向集成 - 亚马逊 WorkSpaces

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

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

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

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

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

先决条件

对于最终用户

最终用户需要以下内容:

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

    注意

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

  • client s.amazonworkspaces.com 支持的 WorkSpaces 客户端。支持的平台:Windows、macOS、Linux(Ubuntu 22.04 和 Ubuntu 24.04、amd64)和 Web。

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

对于开发者来说

您的 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 API。代理透明地将它们重定向到本地客户端。

注意

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

// 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 音频 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. 从 clients.amazonworkspaces.com 下载并安装 WorkSpaces 客户端,然后连接到。 WorkSpace验证是否安装了 Chrome 或 Edge 扩展程序(设置 GPO 时自动安装)。

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

最佳实践

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

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

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

  • 使用前映射音频元素-设置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; }