10-4. WebRTC 基礎

深入理解點對點即時通訊、NAT 穿透與視訊通話原理

📹 WebRTC 基礎

🎯 什麼是 WebRTC?

💡 比喻:直接打電話 vs 透過總機

傳統方式(Client-Server):
Alice → Server → Bob
就像打電話要透過總機轉接,會有延遲

WebRTC(Peer-to-Peer):
Alice ←→ Bob(直連)
就像直接撥對方號碼,速度更快

WebRTC(Web Real-Time Communication) 是一個開放框架,讓瀏覽器和行動應用可以進行即時的語音、視訊和資料傳輸,而且是點對點(P2P) 連線,不需要透過伺服器中轉。

為什麼需要 WebRTC?

問題場景:視訊通話

傳統方式(經由伺服器):

Alice's 攝影機 → Server → Bob's 螢幕

問題:
1. 延遲高(需經過伺服器)
2. 伺服器頻寬成本高(所有影片都要中轉)
3. 伺服器成為瓶頸(同時 100 人通話 = 100 倍流量)

WebRTC(點對點):

Alice's 攝影機 ←→ Bob's 螢幕(直連)

優點:
1. 延遲極低(直接連線)
2. 伺服器只需處理訊號(不傳輸影音)
3. 可擴展性高(P2P 分散負載)

🏗️ WebRTC 架構

核心組件

┌──────────────────────────────────────┐
│         WebRTC 完整流程              │
└──────────────────────────────────────┘

1. Signaling(訊號交換)
   Alice ←→ Signaling Server ←→ Bob
   交換 SDP(會話描述)和 ICE Candidates(網路路徑)

2. NAT Traversal(NAT 穿透)
   使用 STUN/TURN 找到彼此的真實 IP

3. Peer Connection(點對點連線)
   Alice ←→ Bob(直連!)
   傳輸音訊、視訊、資料

三大 API

  1. getUserMedia:取得攝影機/麥克風
  2. RTCPeerConnection:建立 P2P 連線
  3. RTCDataChannel:傳輸任意資料(非音視訊)

📹 getUserMedia(取得媒體)

💡 比喻:借用攝影機
向瀏覽器請求使用攝影機和麥克風的權限

基本用法:

// 取得視訊和音訊
const stream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
});

// 顯示在 <video> 元素
const videoElement = document.querySelector('#localVideo');
videoElement.srcObject = stream;

進階設定:

// 指定解析度和幀率
const constraints = {
    video: {
        width: { ideal: 1920 },
        height: { ideal: 1080 },
        frameRate: { ideal: 30 }
    },
    audio: {
        echoCancellation: true,  // 回音消除
        noiseSuppression: true,  // 降噪
        autoGainControl: true    // 自動增益
    }
};

const stream = await navigator.mediaDevices.getUserMedia(constraints);

螢幕分享:

// 取得螢幕畫面(用於螢幕分享)
const screenStream = await navigator.mediaDevices.getDisplayMedia({
    video: {
        cursor: "always"  // 顯示滑鼠游標
    },
    audio: true  // 包含系統音訊
});

錯誤處理:

try {
    const stream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
    });

    console.log('取得媒體成功');
    return stream;

} catch (error) {
    if (error.name === 'NotAllowedError') {
        console.error('使用者拒絕權限');
    } else if (error.name === 'NotFoundError') {
        console.error('找不到攝影機/麥克風');
    } else {
        console.error('取得媒體失敗:', error);
    }
}

🔗 RTCPeerConnection(P2P 連線)

💡 比喻:建立電話線路
需要先「撥號」(交換網路資訊)才能通話

完整連線流程

Alice                           Bob
  │                              │
  ├─ 1. createOffer() ──────────>│
  │   (SDP Offer)                │
  │                              │
  │<──────── 2. createAnswer() ──┤
  │         (SDP Answer)          │
  │                              │
  ├─ 3. ICE Candidates ─────────>│
  │<─────── ICE Candidates ───────┤
  │                              │
  │<═══════ 4. P2P Connection ══>│
  │     (直連成功!)            │

實作範例

Alice(發起方):

// 建立 RTCPeerConnection
const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }  // Google 的 STUN 伺服器
    ]
};

const peerConnection = new RTCPeerConnection(config);

// 1. 加入本地媒體流
const localStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
});

localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
});

// 2. 監聽 ICE Candidate
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        // 透過 Signaling Server 發送給 Bob
        sendToSignalingServer({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    }
};

// 3. 監聽遠端媒體流
peerConnection.ontrack = (event) => {
    const remoteVideo = document.querySelector('#remoteVideo');
    remoteVideo.srcObject = event.streams[0];
};

// 4. 建立 Offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

// 5. 發送 Offer 給 Bob
sendToSignalingServer({
    type: 'offer',
    sdp: offer
});

Bob(接收方):

// 建立 RTCPeerConnection(配置相同)
const peerConnection = new RTCPeerConnection(config);

// 加入本地媒體流
const localStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
});

localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
});

// 監聽 ICE Candidate
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        sendToSignalingServer({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    }
};

// 監聽遠端媒體流
peerConnection.ontrack = (event) => {
    const remoteVideo = document.querySelector('#remoteVideo');
    remoteVideo.srcObject = event.streams[0];
};

// 收到 Alice 的 Offer
signalingServer.on('offer', async (offer) => {
    await peerConnection.setRemoteDescription(offer);

    // 建立 Answer
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    // 發送 Answer 給 Alice
    sendToSignalingServer({
        type: 'answer',
        sdp: answer
    });
});

// 收到 ICE Candidate
signalingServer.on('ice-candidate', async (candidate) => {
    await peerConnection.addIceCandidate(candidate);
});

🌐 Signaling(訊號交換)

💡 比喻:紅娘
負責撮合雙方,但不參與之後的通訊
Alice 和 Bob 透過紅娘交換電話號碼,之後就直接聯繫

WebRTC 本身不定義 Signaling 機制,你可以使用任何方式:

  • WebSocket
  • Socket.IO
  • HTTP Long Polling
  • XMPP
  • SIP

使用 Socket.IO 實作 Signaling Server

伺服器端(Node.js):

const express = require('express');
const http = require('http');
const socketIO = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIO(server);

// 儲存房間資訊
const rooms = new Map();

io.on('connection', (socket) => {
    console.log('使用者連線:', socket.id);

    // 加入房間
    socket.on('join-room', (roomId) => {
        socket.join(roomId);

        // 通知房間內的其他人
        socket.to(roomId).emit('user-joined', socket.id);

        console.log(`${socket.id} 加入房間 ${roomId}`);
    });

    // 轉發 Offer
    socket.on('offer', (data) => {
        socket.to(data.to).emit('offer', {
            from: socket.id,
            sdp: data.sdp
        });
    });

    // 轉發 Answer
    socket.on('answer', (data) => {
        socket.to(data.to).emit('answer', {
            from: socket.id,
            sdp: data.sdp
        });
    });

    // 轉發 ICE Candidate
    socket.on('ice-candidate', (data) => {
        socket.to(data.to).emit('ice-candidate', {
            from: socket.id,
            candidate: data.candidate
        });
    });

    // 離開房間
    socket.on('disconnect', () => {
        console.log('使用者離線:', socket.id);
        socket.broadcast.emit('user-left', socket.id);
    });
});

server.listen(3000, () => {
    console.log('Signaling Server 啟動於 port 3000');
});

客戶端:

const socket = io('http://localhost:3000');
const roomId = 'room123';

// 加入房間
socket.emit('join-room', roomId);

// 有人加入房間
socket.on('user-joined', async (userId) => {
    console.log('新使用者加入:', userId);

    // 建立 Offer
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);

    // 發送 Offer
    socket.emit('offer', {
        to: userId,
        sdp: offer
    });
});

// 收到 Offer
socket.on('offer', async (data) => {
    await peerConnection.setRemoteDescription(data.sdp);

    // 建立 Answer
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    // 發送 Answer
    socket.emit('answer', {
        to: data.from,
        sdp: answer
    });
});

// 收到 Answer
socket.on('answer', async (data) => {
    await peerConnection.setRemoteDescription(data.sdp);
});

// 收到 ICE Candidate
socket.on('ice-candidate', async (data) => {
    await peerConnection.addIceCandidate(data.candidate);
});

🧭 NAT 穿透(STUN/TURN)

💡 比喻:找到對方的真實地址

你的電腦在公司內網(192.168.1.100)
Bob 的電腦在家裡內網(192.168.0.50)
雙方都不知道對方的「真實門牌號碼」(公網 IP)

STUN:詢問郵局「我的真實地址是什麼?」
TURN:如果直連失敗,郵局幫忙轉信

NAT 問題

Alice 的內網:192.168.1.100
           ↓
      NAT 路由器
           ↓
  公網 IP:203.0.113.5
           ↓
        Internet
           ↓
  公網 IP:198.51.100.8
           ↓
      NAT 路由器
           ↓
Bob 的內網:192.168.0.50

Alice 和 Bob 都不知道對方的公網 IP,無法直接連線。

STUN(Session Traversal Utilities for NAT)

💡 功能:查詢自己的公網 IP 和 Port
Alice → STUN Server:我的公網 IP 是什麼?
STUN Server → Alice:你的公網 IP 是 203.0.113.5:54321

Alice 把這個資訊告訴 Bob
Bob 就可以連線到 203.0.113.5:54321

配置 STUN:

const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' }
    ]
};

const peerConnection = new RTCPeerConnection(config);

TURN(Traversal Using Relays around NAT)

💡 功能:當 P2P 失敗時,透過中繼伺服器轉發

某些嚴格的 NAT(如對稱型 NAT)無法穿透,這時需要 TURN 中繼。

Alice ──> TURN Server ──> Bob

優點:保證連線成功
缺點:佔用伺服器頻寬(與傳統方式類似)

配置 TURN:

const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
            urls: 'turn:turn.example.com:3478',
            username: 'user',
            credential: 'password'
        }
    ]
};

const peerConnection = new RTCPeerConnection(config);

自建 TURN 伺服器(coturn):

# 安裝 coturn
sudo apt-get install coturn

# 設定 /etc/turnserver.conf
listening-port=3478
external-ip=YOUR_PUBLIC_IP
realm=example.com
user=username:password

# 啟動
sudo systemctl start coturn

🧊 ICE(Interactive Connectivity Establishment)

💡 比喻:嘗試所有可能的路徑
就像導航 App 會找出多條路線,選最快的那條

ICE 會嘗試多種連線方式,按優先順序排列:

  1. Host Candidate:本地 IP(內網直連)
  2. Server Reflexive:STUN 查到的公網 IP
  3. Relay Candidate:TURN 中繼
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        console.log('找到 ICE Candidate:', event.candidate);

        // 候選者類型
        const type = event.candidate.type;
        // "host" | "srflx" (Server Reflexive) | "relay"

        // 發送給對方
        sendToSignalingServer({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    } else {
        console.log('ICE 收集完成');
    }
};

📊 SDP(Session Description Protocol)

💡 比喻:通話規格書
就像買手機要看規格(支援 5G?記憶體多大?)
SDP 描述這次連線支援哪些音視訊編碼、解析度等

SDP 範例:

v=0
o=- 1234567890 1234567890 IN IP4 127.0.0.1
s=-
t=0 0

m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:abcd
a=ice-pwd:1234567890abcdefghij
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1

m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000

解析:

  • m=audio:音訊支援 Opus 編碼
  • m=video:視訊支援 VP8、H264 編碼
  • a=ice-ufrag/pwd:ICE 認證資訊

檢視 SDP:

const offer = await peerConnection.createOffer();
console.log('Offer SDP:', offer.sdp);

await peerConnection.setLocalDescription(offer);

💾 RTCDataChannel(資料通道)

💡 比喻:除了語音通話,還能傳簡訊
除了音視訊,還能傳任意資料(文字、檔案、遊戲狀態)

建立 Data Channel:

// Alice 建立 Data Channel
const dataChannel = peerConnection.createDataChannel('chat');

dataChannel.onopen = () => {
    console.log('Data Channel 已開啟');
    dataChannel.send('Hello, Bob!');
};

dataChannel.onmessage = (event) => {
    console.log('收到訊息:', event.data);
};

dataChannel.onerror = (error) => {
    console.error('Data Channel 錯誤:', error);
};

Bob 接收 Data Channel:

peerConnection.ondatachannel = (event) => {
    const dataChannel = event.channel;

    dataChannel.onopen = () => {
        console.log('Data Channel 已開啟');
    };

    dataChannel.onmessage = (event) => {
        console.log('收到訊息:', event.data);

        // 回覆
        dataChannel.send('Hi, Alice!');
    };
};

傳輸檔案:

// 讀取檔案
const fileInput = document.querySelector('#fileInput');
fileInput.addEventListener('change', async (event) => {
    const file = event.target.files[0];

    // 分塊傳輸(避免超過 buffer 大小)
    const chunkSize = 16384;  // 16 KB
    const fileReader = new FileReader();

    let offset = 0;

    fileReader.onload = (e) => {
        dataChannel.send(e.target.result);
        offset += chunkSize;

        if (offset < file.size) {
            readSlice(offset);
        } else {
            console.log('檔案傳輸完成');
        }
    };

    const readSlice = (offset) => {
        const slice = file.slice(offset, offset + chunkSize);
        fileReader.readAsArrayBuffer(slice);
    };

    readSlice(0);
});

應用場景:

  • 💬 文字聊天(不需要伺服器中轉)
  • 📁 檔案分享(P2P 傳輸)
  • 🎮 遊戲同步(低延遲)
  • 📊 協作編輯(共享狀態)

🎓 常見面試題

Q1:WebRTC 的連線流程是什麼?

答案:

完整流程(7 步驟):

1. Alice 取得本地媒體(getUserMedia)
2. Alice 建立 RTCPeerConnection
3. Alice 建立 Offer(createOffer)→ 發送給 Bob
4. Bob 收到 Offer,建立 Answer(createAnswer)→ 發送給 Alice
5. 雙方交換 ICE Candidates(透過 Signaling Server)
6. ICE 建立 P2P 連線(嘗試 Host → STUN → TURN)
7. 連線成功,開始傳輸音視訊!

記憶口訣:「媒建議答冰連傳」

  • 媒(媒體)
  • 建(建立連線)
  • 議(Offer)
  • 答(Answer)
  • 冰(ICE)
  • 連(P2P 連線)
  • 傳(傳輸)

程式碼濃縮版:

// 1-2. 取得媒體 + 建立連線
const stream = await getUserMedia({video: true, audio: true});
const pc = new RTCPeerConnection(config);
stream.getTracks().forEach(t => pc.addTrack(t, stream));

// 3-4. Offer/Answer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToSignalingServer(offer);  // → Bob

// Bob:
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToSignalingServer(answer);  // → Alice

// 5-6. ICE
pc.onicecandidate = e => sendToSignalingServer(e.candidate);

// 7. 傳輸
pc.ontrack = e => remoteVideo.srcObject = e.streams[0];

Q2:STUN 和 TURN 的差異?

答案:

比喻:
STUN = 問路人「我家地址是什麼?」(查詢服務)
TURN = 請快遞幫忙轉交(中繼服務)
特性STUNTURN
功能查詢公網 IP/Port中繼轉發流量
流量不經過 STUN(只查詢)所有流量經過 TURN
成本低(幾乎不用頻寬)高(需要大頻寬)
成功率~80-90%100%
延遲較高(多一跳)

何時使用:

// ICE 自動選擇最佳路徑:
1. 嘗試直連Host
    失敗
2. 嘗試 STUNServer Reflexive
    失敗
3. 使用 TURNRelay

配置建議:

const config = {
    iceServers: [
        // 免費 STUN(Google 提供)
        { urls: 'stun:stun.l.google.com:19302' },

        // 自建 TURN(需要認證,避免濫用)
        {
            urls: 'turn:turn.myserver.com:3478',
            username: 'user',
            credential: 'pass',
            credentialType: 'password'
        }
    ],

    // ICE 策略
    iceTransportPolicy: 'all'  // 'all' | 'relay'(強制使用 TURN)
};

成本估算:

STUN 伺服器:
- 流量:幾乎為 0
- 費用:免費(使用 Google STUN)

TURN 伺服器:
- 流量:與視訊通話相當(每小時約 500 MB - 1 GB)
- 費用:需要自建或租用
- 使用率:約 10-20% 的連線需要 TURN

Q3:如何優化 WebRTC 的視訊品質?

答案:

多種優化策略:

1. 自適應位元率(Adaptive Bitrate):

const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');

const parameters = sender.getParameters();

// 設定最大位元率(1 Mbps)
if (!parameters.encodings) {
    parameters.encodings = [{}];
}

parameters.encodings[0].maxBitrate = 1000000;  // 1 Mbps

await sender.setParameters(parameters);

2. 根據網路狀況調整解析度:

peerConnection.getStats().then(stats => {
    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'video') {
            const packetsLost = report.packetsLost;
            const packetsReceived = report.packetsReceived;
            const lossRate = packetsLost / (packetsLost + packetsReceived);

            if (lossRate > 0.1) {  // 丟包率 > 10%
                // 降低解析度
                adjustVideoQuality('low');
            }
        }
    });
});

function adjustVideoQuality(quality) {
    const constraints = {
        low: { width: 640, height: 360, frameRate: 15 },
        medium: { width: 1280, height: 720, frameRate: 24 },
        high: { width: 1920, height: 1080, frameRate: 30 }
    };

    const videoTrack = localStream.getVideoTracks()[0];
    videoTrack.applyConstraints(constraints[quality]);
}

3. 使用 Simulcast(同時多碼率):

// 發送者同時傳輸多種解析度,接收者選擇適合的
const sender = peerConnection.addTransceiver('video', {
    direction: 'sendonly',
    sendEncodings: [
        { rid: 'h', maxBitrate: 900000 },   // High
        { rid: 'm', maxBitrate: 300000, scaleResolutionDownBy: 2 },  // Medium
        { rid: 'l', maxBitrate: 100000, scaleResolutionDownBy: 4 }   // Low
    ]
});

4. 選擇合適的編碼器:

// 檢查支援的編碼器
const capabilities = RTCRtpSender.getCapabilities('video');
console.log('支援的編碼器:', capabilities.codecs);

// 優先使用 VP9(比 VP8 更高效)
const preferredCodec = capabilities.codecs.find(codec =>
    codec.mimeType === 'video/VP9'
);

if (preferredCodec) {
    const transceivers = peerConnection.getTransceivers();
    transceivers.forEach(transceiver => {
        if (transceiver.sender.track?.kind === 'video') {
            const params = transceiver.sender.getParameters();
            params.codecs = [preferredCodec];
            transceiver.sender.setParameters(params);
        }
    });
}

Q4:WebRTC 如何處理多人通話?

答案:

有兩種架構:

1. Mesh(網狀)- 每個人連線到所有人:

3 人通話:

Alice ←→ Bob
  ↓  ╲  ↗
  ↓   Charlie
  ↓  ↗
  ↓↗

每個人需要 (n-1) 個連線
3 人 = 2 個連線
10 人 = 9 個連線(頻寬吃不消!)

優點:

  • 延遲低(P2P)
  • 不需要伺服器

缺點:

  • 頻寬需求高(上傳 n-1 份影片)
  • 只適合 2-4 人

實作:

// 維護多個 PeerConnection
const peerConnections = new Map();

function createPeerConnection(userId) {
    const pc = new RTCPeerConnection(config);

    // 加入本地媒體
    localStream.getTracks().forEach(track => {
        pc.addTrack(track, localStream);
    });

    // 監聽遠端媒體
    pc.ontrack = (event) => {
        displayRemoteVideo(userId, event.streams[0]);
    };

    peerConnections.set(userId, pc);
    return pc;
}

// 有新使用者加入
signalingServer.on('user-joined', (userId) => {
    const pc = createPeerConnection(userId);
    // 建立 Offer...
});

2. SFU(Selective Forwarding Unit)- 伺服器轉發:

10 人通話:

Alice ──┐
Bob ────┼──> SFU Server
Charlie─┤    (只轉發,不編碼)
...     │
10th ───┘

每個人只需 1 個上傳連線(傳給 SFU)
SFU 轉發給其他 9 人

優點:

  • 頻寬需求低(只上傳 1 份)
  • 支援大規模通話(100+ 人)

缺點:

  • 需要 SFU 伺服器(mediasoup、Janus、Kurento)

使用 mediasoup(SFU):

// 客戶端
import { Device } from 'mediasoup-client';

const device = new Device();

// 載入伺服器的 RTP capabilities
await device.load({ routerRtpCapabilities });

// 建立 Producer(發送)
const transport = await device.createSendTransport(transportOptions);
const producer = await transport.produce({
    track: localStream.getVideoTracks()[0]
});

// 建立 Consumer(接收)
const consumer = await device.createRecvTransport(transportOptions).consume({
    id: consumerId,
    producerId: producerId,
    kind: 'video',
    rtpParameters: rtpParameters
});

3. MCU(Multipoint Control Unit)- 伺服器混流:

SFU 轉發 9 個影片(客戶端需解碼 9 次)
MCU 混合成 1 個影片(客戶端只解碼 1 次)

  Alice ──┐
  Bob ────┼──> MCU Server
  Charlie─┘    (混合成一個畫面)
                 ↓
         [九宮格合成影片]
                 ↓
          傳給所有人(1 個流)

優點:

  • 客戶端負擔最低(只收 1 個流)

缺點:

  • 伺服器運算量高(需要編碼/解碼)
  • 彈性較低(無法自由調整布局)

Q5:WebRTC 的安全性如何?

答案:

WebRTC 內建強制加密,非常安全!

1. DTLS(Datagram TLS):

WebRTC 強制使用 DTLS 加密所有資料
就像 HTTPS,但用於 UDP

2. SRTP(Secure RTP):

音視訊流使用 SRTP 加密
每個封包都加密,無法竊聽

3. 瀏覽器安全限制:

// 只能在 HTTPS 頁面使用 getUserMedia
// HTTP 會被拒絕(Localhost 除外)

if (location.protocol !== 'https:') {
    console.error('WebRTC 需要 HTTPS');
}

4. 權限控制:

// 使用者必須明確允許存取攝影機/麥克風
// 瀏覽器會顯示權限提示

const stream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
});
// 彈出權限提示:「example.com 想要使用您的攝影機和麥克風」

潛在風險:

⚠️ Signaling Server 可能被竊聽
解決:使用 WSS(WebSocket Secure)

⚠️ TURN 伺服器可能記錄資料
解決:使用可信任的 TURN 伺服器

⚠️ 惡意網站可能竊取媒體
解決:只在可信任的網站允許權限

📝 總結

WebRTC 是現代即時通訊的基石:

  • P2P:點對點連線,延遲極低 ⚡
  • 無插件:瀏覽器原生支援 🌐
  • 加密:強制 DTLS/SRTP 加密 🔐
  • 免費:開放標準,無需授權 💰

記憶口訣:「點(P2P)、原(原生)、密(加密)、免(免費)」

適用場景:

  • 📞 視訊通話(Zoom、Google Meet)
  • 🎮 雲端遊戲(Google Stadia)
  • 📡 直播(Twitch、YouTube Live)
  • 🏥 遠端醫療
  • 🎓 線上教育

完整堆疊:

應用層:getUserMedia, RTCPeerConnection, RTCDataChannel
訊號層:WebSocket, Socket.IO(自訂)
媒體層:SRTP(加密音視訊)
傳輸層:UDP(即時傳輸)
穿透層:ICE, STUN, TURN

🔗 延伸閱讀

0%