

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

# 整合 Web 應用程式與 WebRTC 重新導向
<a name="webrtc-redirection-integration"></a>

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

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

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

## 先決條件
<a name="webrtc-prereqs"></a>

### 對於最終使用者
<a name="webrtc-prereqs-users"></a>

最終使用者需要下列項目：
+ 使用 DCV 通訊協定的 WorkSpace。如需詳細資訊，請參閱[WorkSpaces Personal 的通訊協定](amazon-workspaces-networking.md#amazon-workspaces-protocols)。
**注意**  
伺服器端的 WebRTC 重新導向目前僅支援 Windows WorkSpaces。
+ 來自 [clients.amazonworkspaces.com](https://clients.amazonworkspaces.com) 的支援 WorkSpaces 用戶端。支援的平台：Windows、macOS、Linux (Ubuntu 22.04 和 Ubuntu 24.04、amd64) 和 Web。
+ 透過群組政策啟用 WebRTC 重新導向。啟用時，政策會透過登錄檔自動在 Chrome 和 Edge 上安裝瀏覽器擴充功能。如需詳細資訊，請參閱[在 WorkSpaces Personal 中管理您的 Windows WorkSpaces](group_policy.md)。如有需要，使用者可以手動安裝擴充功能：[Chrome](https://chromewebstore.google.com/detail/amazon-dcv-webrtc-redirec/diilpfplcnhehakckkpmcmibmhbingnd) 和 [Edge](https://microsoftedge.microsoft.com/addons/detail/amazon-dcv-webrtc-redirec/kjbbkjjiecchbcdoollhgffghfjnbhef)。

### 適用於開發人員
<a name="webrtc-prereqs-devs"></a>

您的 Web 應用程式必須：
+ 使用標準 WebRTC APIs（例如 getUserMedia 和 RTCPeerConnection)
+ 包含本主題中所述的偵測和初始化程式碼

## 如何整合
<a name="webrtc-how-to-integrate"></a>

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

### 步驟 1：偵測重新導向環境
<a name="webrtc-step1-detect"></a>

將此初始化程式碼新增至您的應用程式。回呼會在代理就緒時執行，如果無法使用重新導向，則會在逾時之後執行。

```
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 毫秒
+ 回呼會接收具有布林值和 `proxy`或 `success` 的結果物件 `error`
+ 呼叫 `proxy.overrideWebRTC()`以重新導向標準 WebRTC APIs

### 步驟 2：使用標準 WebRTC APIs
<a name="webrtc-step2-apis"></a>

呼叫 後`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：映射音訊元素
<a name="webrtc-step3-audio"></a>

對於音訊播放，`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：處理重新連線
<a name="webrtc-step4-reconnection"></a>

重新導向服務可能會暫時無法使用，例如因為網路問題或用戶端重新連線。

**重要**  
重新連線後，所有先前建立的代理物件都會變成無效，因為用戶端瀏覽器內容會完全重新載入。您必須關閉現有的連線並建立新的 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
});
```

## 裝置列舉
<a name="webrtc-device-enumeration"></a>

裝置變更 - 例如，使用者插入耳機 - 會自動偵測到。使用 `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 進行音訊處理
<a name="webrtc-advanced-audio"></a>

您可以使用 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);
```

## 完成範例
<a name="webrtc-complete-example"></a>

下列範例示範與初始化、重新連線處理、裝置變更偵測和呼叫設定的完整整合：

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

## 測試您的整合
<a name="webrtc-testing"></a>

### 無重新導向
<a name="webrtc-testing-without"></a>

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

### 使用重新導向
<a name="webrtc-testing-with"></a>

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

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

1. 從 [clients.amazonworkspaces.com](https://clients.amazonworkspaces.com) 下載並安裝 WorkSpaces 用戶端，並連線至 WorkSpace。確認已安裝 Chrome 或 Edge 延伸模組 （設定 GPO 時自動）。

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

## 最佳實務
<a name="webrtc-best-practices"></a>

與 WebRTC 重新導向整合時，請遵循下列建議：
+ 一律檢查重新導向可用性 - 絕不假設代理存在
+ `overrideWebRTC()` 提早呼叫 - 建立任何 WebRTC 物件之前
+ 使用前映射音訊元素 - 設定`mapAudioElement()`前呼叫 `srcObject`
+ 處理重新連線 - 重新連線後，所有代理物件都是無效的；一律建立新的 WebRTC 物件
+ 測試這兩種模式 - 確保您的應用程式在有和沒有重新導向的情況下正常運作
+ 使用標準 WebRTC APIs - 代理會透明地重新導向它們；基本使用不需要自訂 APIs 
+ 清除資源 - 移除事件接聽程式並在完成時關閉連線

## 疑難排解
<a name="webrtc-troubleshooting"></a>

### 未偵測到重新導向
<a name="webrtc-troubleshooting-not-detected"></a>
+ 確認瀏覽器延伸模組已安裝並啟用 （檢查 `chrome://extensions`)
+ 確認 WorkSpace 上已啟用 WebRTC 重新導向群組政策
+ 使用 DCV 通訊協定驗證您在 WorkSpace 內執行
+ 檢查瀏覽器主控台的初始化訊息
+ 確認 WorkSpaces 用戶端版本支援 WebRTC 重新導向 (Windows 5.21.0 或更新版本、macOS 5.31.0 或更新版本、Linux 2026.0 或更新版本，或 Web 用戶端）

### 音訊或視訊無法運作
<a name="webrtc-troubleshooting-audio-video"></a>
+ 在建立任何 WebRTC 物件之前，`overrideWebRTC()`請確認已呼叫
+ 在設定`mapAudioElement()`之前，請確認音訊元素已與 映射 `srcObject`
+ 檢查瀏覽器主控台是否有錯誤
+ 確認已授予裝置許可

### 重新連線問題
<a name="webrtc-troubleshooting-reconnection"></a>
+ 實作 `addStatusChangeEventListener()`以偵測可用性變更
+ 重新連線後，請一律建立新的 RTCPeerConnection 和 MediaStream 物件 - 舊代理無效且會擲回
+ 如果偵測到無法使用太慢或太快，請檢閱活動訊號組態

## API 參考
<a name="webrtc-api-reference"></a>

下列摘要說明代理 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;
}
```