View a markdown version of this page

整合 Web 應用程式與 WebRTC 重新導向 - Amazon WorkSpaces

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

整合 Web 應用程式與 WebRTC 重新導向

WebRTC 重新導向可讓在 WorkSpaces 工作階段內執行的 Web 應用程式使用原生用戶端音訊裝置。本主題顯示 Web 應用程式開發人員如何偵測 WebRTC 重新導向環境並與其整合。

在 WorkSpace 上啟用 WebRTC 重新導向時,Amazon DCV WebRTC 重新導向瀏覽器延伸模組會將代理 SDK 注入網頁。您的應用程式可以偵測此代理,並使用它將標準 WebRTC API 呼叫 - 包括 getUserMedia 和 RTCPeerConnection - 重新導向至使用者的本機裝置,而不是遠端 WorkSpace 的虛擬裝置。相較於透過 DCV 顯示通訊協定串流媒體,這提供了顯著更好的效能和更低的延遲。

您的應用程式必須處理兩種情況:當在已啟用 WebRTC 重新導向的 WorkSpace 內執行時,以及在標準瀏覽器中執行而不執行時。代理是選用的 - 如果不存在,您的應用程式可能會回到標準 WebRTC。

先決條件

對於最終使用者

最終使用者需要下列項目:

  • 使用 DCV 通訊協定的 WorkSpace。如需詳細資訊,請參閱WorkSpaces Personal 的通訊協定

    注意

    伺服器端的 WebRTC 重新導向目前僅支援 Windows WorkSpaces。

  • 來自 clients.amazonworkspaces.com 的支援 WorkSpaces 用戶端。支援的平台:Windows、macOS、Linux (Ubuntu 22.04 和 Ubuntu 24.04、amd64) 和 Web。

  • 透過群組政策啟用 WebRTC 重新導向。啟用時,政策會透過登錄檔自動在 Chrome 和 Edge 上安裝瀏覽器擴充功能。如需詳細資訊,請參閱在 WorkSpaces Personal 中管理您的 Windows WorkSpaces。如有需要,使用者可以手動安裝擴充功能:ChromeEdge

適用於開發人員

您的 Web 應用程式必須:

  • 使用標準 WebRTC APIs(例如 getUserMedia 和 RTCPeerConnection)

  • 包含本主題中所述的偵測和初始化程式碼

如何整合

整合包含四個步驟:偵測重新導向環境、覆寫 WebRTC APIs、映射音訊元素,以及處理重新連線。

步驟 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 毫秒

  • 回呼會接收具有布林值和 proxysuccess 的結果物件 error

  • 呼叫 proxy.overrideWebRTC()以重新導向標準 WebRTC APIs

步驟 2:使用標準 WebRTC APIs

呼叫 後overrideWebRTC(),請正常使用 WebRTC APIs。代理會透明地將其重新導向至本機用戶端。

注意

目前不支援視訊重新導向。在getUserMedia請求video: false中設定 。

// 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);

如果您需要存取原始瀏覽器 APIs (例如,從遠端瀏覽器而非本機用戶端擷取視訊),它們會保留在 中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:映射音訊元素

對於音訊播放,srcObject請在設定任何音訊元素mapAudioElement()之前呼叫 。

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 Audio 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();

測試您的整合

無重新導向

您的應用程式在未安裝延伸模組、在 WorkSpace 外部執行,或未透過群組政策啟用 WebRTC 重新導向時正常運作。在一般瀏覽器中開啟應用程式進行測試。

使用重新導向

若要在啟用重新導向的情況下進行測試:

  1. 使用 DCV 通訊協定建立 WorkSpace,並啟用 WebRTC 重新導向群組政策設定。請參閱 在 WorkSpaces Personal 中管理您的 Windows WorkSpaces

  2. clients.amazonworkspaces.com 下載並安裝 WorkSpaces 用戶端,並連線至 WorkSpace。確認已安裝 Chrome 或 Edge 延伸模組 (設定 GPO 時自動)。

  3. 在 WorkSpace 瀏覽器中開啟 Web 應用程式。檢查主控台是否有「已啟用 WebRTC 重新導向」訊息。使用本機裝置驗證音訊功能、測試裝置列舉和切換,以及驗證重新連線處理。

最佳實務

與 WebRTC 重新導向整合時,請遵循下列建議:

  • 一律檢查重新導向可用性 - 絕不假設代理存在

  • overrideWebRTC() 提早呼叫 - 建立任何 WebRTC 物件之前

  • 使用前映射音訊元素 - 設定mapAudioElement()前呼叫 srcObject

  • 處理重新連線 - 重新連線後,所有代理物件都是無效的;一律建立新的 WebRTC 物件

  • 測試這兩種模式 - 確保您的應用程式在有和沒有重新導向的情況下正常運作

  • 使用標準 WebRTC APIs - 代理會透明地重新導向它們;基本使用不需要自訂 APIs

  • 清除資源 - 移除事件接聽程式並在完成時關閉連線

疑難排解

未偵測到重新導向

  • 確認瀏覽器延伸模組已安裝並啟用 (檢查 chrome://extensions)

  • 確認 WorkSpace 上已啟用 WebRTC 重新導向群組政策

  • 使用 DCV 通訊協定驗證您在 WorkSpace 內執行

  • 檢查瀏覽器主控台的初始化訊息

  • 確認 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; }