WebRTC (Web Real-Time Communication) 是一项开放标准 (由 W3C 和 IETF 制定),它允许 Web 应用程序和站点在不需要任何内部或外部插件的情况下,实现浏览器之间的实时语音、视频通信以及数据传输。WebRTC 的核心思想是实现点对点 (P2P) 传输,从而减少服务器负载并降低延迟,提供高质量的实时交互体验。

核心思想:利用浏览器内置的 API,通过一套标准化协议,安全高效地建立客户端之间的直接连接,实现低延迟的实时通信。WebRTC 关注的是客户端之间的数据传输,而连接的协调(如谁与谁连接)则依赖于信令服务器。


一、为什么需要 WebRTC?

在 WebRTC 出现之前,实现浏览器间的实时通信通常需要依赖 Flash、Java Applet 或各种插件,这些方案存在以下问题:

  1. 插件依赖:用户需要安装特定插件,增加了使用门槛和兼容性问题。
  2. 不开放标准:缺乏统一标准,不同方案之间难以互通。
  3. 安全性问题:插件可能引入安全漏洞。
  4. 服务器集中:大部分实时通信方案依赖中心化服务器进行数据传输,导致服务器开销大、延迟高。

WebRTC 旨在解决这些问题,提供一个无需插件、开放标准、安全且高效的实时通信解决方案:

  • 浏览器原生支持:现代浏览器原生集成 WebRTC API,无需任何插件。
  • P2P 通信:在可能的情况下,直接在浏览器之间建立连接,减少服务器开销和网络延迟。
  • 开放标准:基于统一的 W3C 和 IETF 标准,确保不同浏览器和设备之间的互操作性。
  • 安全性:强制使用加密(SRTP/DTLS)保障通信安全。
  • 多媒体支持:提供高质量的音视频采集、编解码和传输能力。
  • 数据通道:除了音视频,还支持任意数据的双向传输。

二、WebRTC 的核心组件与 API

WebRTC 主要由三个核心 API 组成,提供 TypeScript 类型定义,增强开发时的类型安全和代码提示:

2.1 MediaDevices (原 getUserMedia)

navigator.mediaDevices.getUserMedia(constraints) 用于获取用户的音视频输入设备,如摄像头和麦克风。

  • constraints (媒体约束):一个 MediaStreamConstraints 对象,用于指定请求的媒体类型(音频、视频)和质量要求(分辨率、帧率、设备ID等)。
  • 返回值:成功时返回一个 Promise<MediaStream>,解析为一个 MediaStream 对象,其中包含音频和/或视频轨道。

TypeScript 示例:获取本地媒体流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function getLocalMediaStream(videoElement: HTMLVideoElement): Promise<MediaStream | undefined> {
const constraints: MediaStreamConstraints = {
audio: true, // 启用音频
video: {
width: 1280,
height: 720,
frameRate: { ideal: 30, max: 60 } // 指定视频分辨率和帧率
}
};

try {
const stream: MediaStream = await navigator.mediaDevices.getUserMedia(constraints);
videoElement.srcObject = stream; // 将视频流显示在 <video> 元素中
return stream;
} catch (error) {
console.error("无法获取本地媒体流:", error);
alert(`请授权访问麦克风和摄像头: ${error}`);
return undefined;
}
}

// 假设页面中有一个 <video id="localVideo"></video> 元素
const localVideoElement = document.getElementById('localVideo') as HTMLVideoElement;
if (localVideoElement) {
getLocalMediaStream(localVideoElement);
}

2.2 RTCPeerConnection

RTCPeerConnection 是 WebRTC 中最重要的组件,它负责建立、维护和关闭浏览器之间的 P2P 连接,包括:

  • 会话控制:协商媒体能力和连接参数。
  • 编解码器管理:选择合适的音视频编解码器。
  • P2P 数据传输:处理 ICE 协商,建立直接连接。
  • 网络处理:NAT 穿越、带宽管理等。
  • 安全性:处理加密和认证。

其主要 API 操作包括:

  • createOffer(): 创建会话描述 (RTCSessionDescriptionInit) “offer”,表示本地端的媒体能力和配置。
  • createAnswer(): 响应收到的 “offer”,创建本地端的 “answer” SDP。
  • setLocalDescription(description): 设置本地的会话描述。
  • setRemoteDescription(description): 设置远程的会话描述。
  • addIceCandidate(candidate): 添加 ICE 候选者,用于 P2P 网络连接的建立。
  • addTrack(track, stream): 将媒体轨道(如摄像头视频、麦克风音频)添加到连接中。
  • ontrack 事件:当远程流添加到连接时触发。
  • onicecandidate 事件:本地 ICE 候选者可用时触发,需要通过信令服务器发送给对方。

TypeScript 示例:RTCPeerConnection 基本设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 假设信令服务器 URL
const SIGNALING_SERVER_URL = "ws://localhost:8080/ws";
const ws = new WebSocket(SIGNALING_SERVER_URL); // WebSocket 信令连接

let peerConnection: RTCPeerConnection | null = null;
let localStream: MediaStream | null = null;
let remoteVideoElement: HTMLVideoElement | null = null;
let isCaller = false; // 标记是否是呼叫方

async function setupPeerConnection(stream: MediaStream, _isCaller: boolean, _remoteVideoElement: HTMLVideoElement) {
localStream = stream;
remoteVideoElement = _remoteVideoElement;
isCaller = _isCaller;

// STUN 服务器配置,用于 NAT 穿越
const configuration: RTCConfiguration = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" }, // 免费 STUN 服务器
// { urls: "turn:your-turn-server.com", username: "user", credential: "password" } // 如果需要 TURN
]
};

peerConnection = new RTCPeerConnection(configuration);

// 将本地媒体流的所有轨道添加到 PeerConnection
localStream.getTracks().forEach(track => {
peerConnection!.addTrack(track, localStream!);
});

// 监听远程轨道添加事件
peerConnection.ontrack = (event: RTCTrackEvent) => {
if (remoteVideoElement) {
remoteVideoElement.srcObject = event.streams[0];
}
};

// 监听 ICE 候选者事件,准备通过信令服务器发送给对方
peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
console.log("发送 ICE 候选者:", event.candidate);
// 通过 WebSocket 信令服务器发送 ICE 候选者
ws.send(JSON.stringify({ type: "ice-candidate", candidate: event.candidate }));
}
};

// 监听 PeerConnection 状态变化
peerConnection.onconnectionstatechange = () => {
console.log("Peer Connection 状态:", peerConnection?.connectionState);
};

if (isCaller) {
// 作为呼叫方,创建 Offer
const offer: RTCSessionDescriptionInit = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
console.log("发送 Offer SDP:", offer);
ws.send(JSON.stringify({ type: "offer", sdp: offer }));
}
}

2.3 RTCDataChannel

RTCDataChannel 允许在两个浏览器之间传输任意的二进制数据。它提供了类似 WebSockets 的 API,但数据传输通过 P2P 进行。

  • 特点
    • 可靠或不可靠:可以配置为可靠传输 (TCP 类似) 或不可靠传输 (UDP 类似),适用于不同场景。
    • 安全性:数据通过 DTLS 协议加密。
    • 低延迟:直接 P2P 传输,延迟极低。
  • 用法
    • 通过 RTCPeerConnection.createDataChannel(label, options) 创建。
    • 监听 onmessageonopenoncloseonerror 事件。

TypeScript 示例:使用 RTCDataChannel 传输数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let dataChannel: RTCDataChannel | null = null;

function setupDataChannel() {
if (!peerConnection) return;

// 作为发起方创建 DataChannel
dataChannel = peerConnection.createDataChannel("chat");

dataChannel.onopen = (event: Event) => {
console.log("DataChannel 已打开");
dataChannel?.send("Hello from DataChannel!");
};
dataChannel.onmessage = (event: MessageEvent) => {
console.log("收到 DataChannel 消息:", event.data);
};
dataChannel.onclose = (event: Event) => {
console.log("DataChannel 已关闭");
};
dataChannel.onerror = (event: RTCDataChannelEvent) => {
console.error("DataChannel 错误:", event);
};

// 监听对方创建的 DataChannel
peerConnection.ondatachannel = (event: RTCDataChannelEvent) => {
dataChannel = event.channel;
console.log("收到对方 DataChannel 请求,已连接");
// 同样设置事件监听
dataChannel!.onopen = ...;
dataChannel!.onmessage = ...;
// ...
};
}

三、WebRTC 的工作流程

WebRTC 的连接建立流程相对复杂,主要包括信令传输、SDP 协商、ICE 候选者交换和数据传输四个阶段。

3.1 信令 (Signaling)

WebRTC 本身不提供信令机制,信令服务器用于协调双方发起和建立连接所需的信息交换。这些信息包括:

  • 会话描述 (SDP - Session Description Protocol):用于描述本地媒体会话能力,如 IP 地址、端口、支持的编解码器、传输协议等。
    • Offer (提议):连接发起方创建的 SDP。
    • Answer (应答):连接接收方对 Offer 的响应。
  • ICE 候选者 (ICE Candidates):描述本地网络的连接信息,如 IP 地址、端口、传输协议(UDP/TCP)。

信令服务器可以是任何能进行双向通信的技术,如 WebSocket、AJAX 长轮询或自定义的 HTTP/S 服务。通常使用 WebSocket,因为其双向、持久连接特性非常适合信令交换。

3.2 NAT 穿越与 ICE (Interactive Connectivity Establishment)

大多数设备位于防火墙或 NAT (Network Address Translation) 之后,无法直接进行 P2P 连接。ICE 协议用于解决 NAT 穿越问题,它会尝试多种连接方式来找到最佳路径:

  1. 收集候选者:客户端首先收集所有可能的 IP 地址和端口对:
    • Host Candidate (主机候选者):设备的本地 IP 地址。
    • Server Reflexive Candidate (服务器反射候选者):通过 STUN (Session Traversal Utilities for NAT) 服务器 发现的公网 IP 和端口。STUN 服务器帮助客户端“看到”其外部网络地址。
    • Relay Candidate (中继候选者):当 STUN 无法直接建立连接时,数据通过 TURN (Traversal Using Relays around NAT) 服务器 中继传输。TURN 服务器作为中继,会产生带宽消耗。
  2. 交换候选者:通过信令服务器将这些候选者交换给对方。
  3. 连接检查:双方同时尝试连接所有可能的候选者组合,找到最有效和可用的连接路径。

3.3 SDP 协商 (Session Description Protocol)

SDP 是一种文本协议,用于描述多媒体会话。在 WebRTC 中,它被用来在两个 Peer 之间协商:

  • 双方的 IP 地址和端口。
  • 支持的音视频编解码器列表和优先级。
  • 媒体传输协议(如 RTP/RTCP)。
  • 安全参数(如 DTLS/SRTP 的指纹)。

一方创建 Offer SDP,另一方创建 Answer SDP 进行响应,通过信令服务器交换。

3.4 数据传输层

一旦 ICE 协商完成并建立了连接,数据传输通过以下协议进行:

  • RTP (Real-time Transport Protocol):用于传输实时音视频流。
  • RTCP (RTP Control Protocol):用于监控 RTP 传输的质量,提供反馈。
  • SRTP (Secure Real-time Transport Protocol):RTP 的加密版本,确保音视频传输安全。
  • DTLS (Datagram Transport Layer Security):用于建立安全的数据通道,为 SRTP 交换密钥,也用于 RTCDataChannel 的加密。

四、安全考量

WebRTC 从设计之初就优先考虑安全性:

  1. 强制加密:所有 WebRTC 通信(包括数据通道、音视频)都强制使用 DTLS 和 SRTP 进行加密,防止窃听和篡改。
  2. 权限机制getUserMedia 必须经过用户明确授权才能访问摄像头和麦克风。
  3. 同源策略:WebRTC API 遵循浏览器的同源安全策略。
  4. IP 地址泄露:虽然 P2P 连接方便,但也可能暴露本地 IP 地址。在某些浏览器中,例如 Firefox 和 Chrome,提供了配置选项来限制 WebRTC 泄露本地 IP 地址(如使用 mDNS 候选者)。
  5. 信令安全:信令服务器本身需要确保传输的 SDP 和 ICE 候选者不被篡改,通常通过 HTTPS 和 TLS 加密进行。

五、WebRTC 应用场景

WebRTC 广泛应用于需要低延迟实时交互的场景:

  • 视频会议和语音通话:Zoom、Google Meet、Microsoft Teams 等。
  • 在线教育:师生实时互动、白板共享。
  • 直播和点播:低延迟直播推流 (如游戏直播、活动直播)。
  • 客服系统:在网页中直接与客服进行视频/语音沟通。
  • 社交应用:实时的视频聊天功能。
  • 游戏:多玩家实时互动、云游戏流媒体。
  • IoT 和设备控制:远程摄像头监控、机器人控制。
  • 文件共享:通过数据通道实现 P2P 文件传输。

六、TypeScript 前端与信令服务器交互示例

本节将展示一个简化的 TypeScript 前端代码片段,它如何通过 WebSocket 信令服务器协调两个 WebRTC Peer 之间的连接。

首先,一个简单的 Go 语言信令服务器 示例(与之前提供的一致,它负责将收到的消息广播给所有连接的客户端)。
要运行此服务器,你需要安装 github.com/gorilla/websocket 库:go get github.com/gorilla/websocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main

import (
"log"
"net/http"
"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { // 允许所有来源,生产环境请严格限制
return true
},
}

type Hub struct {
clients map[*websocket.Conn]bool
broadcast chan []byte
register chan *websocket.Conn
unregister chan *websocket.Conn
}

func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
clients: make(map[*websocket.Conn]bool),
}
}

func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
log.Printf("Client registered: %s, total clients: %d", client.RemoteAddr().String(), len(h.clients))
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
client.Close()
log.Printf("Client unregistered: %s, total clients: %d", client.RemoteAddr().String(), len(h.clients))
}
case message := <-h.broadcast:
for client := range h.clients {
err := client.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Error writing message to client %s: %v", client.RemoteAddr().String(), err)
h.unregister <- client
}
}
}
}
}

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrade error: %v", err)
return
}

hub.register <- conn

go func() {
defer func() {
hub.unregister <- conn
}()
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("Read error for client %s: %v", conn.RemoteAddr().String(), err)
}
break
}
log.Printf("Received msg from %s: %s", conn.RemoteAddr().String(), message)
hub.broadcast <- message
}
}()
}

func main() {
hub := newHub()
go hub.run()

http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})

log.Println("Signaling server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

TypeScript 前端代码示例 (HTML + JS/TS)

假设你的 index.html 页面结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC TypeScript Demo</title>
<style>
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; }
.video-container { display: flex; gap: 20px; margin-top: 20px; }
video { width: 480px; height: 360px; border: 1px solid #ccc; background-color: #eee; }
.controls { margin-top: 20px; }
button { padding: 10px 20px; font-size: 16px; margin: 5px; cursor: pointer; }
</style>
</head>
<body>
<h1>WebRTC TypeScript Demo</h1>
<div class="video-container">
<div>
<h2>Local Video</h2>
<video id="localVideo" autoplay muted playsinline></video>
</div>
<div>
<h2>Remote Video</h2>
<video id="remoteVideo" autoplay playsinline></video>
</div>
</div>
<div class="controls">
<button id="startCall">开始呼叫</button>
<button id="hangUp">挂断</button>
</div>

<!-- 编译后的 TypeScript 文件 -->
<script src="dist/main.js"></script>
</body>
</html>

src/main.ts (前端逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// 定义信令消息类型
interface SignalingMessage {
type: "offer" | "answer" | "ice-candidate";
sdp?: RTCSessionDescriptionInit;
candidate?: RTCIceCandidateInit;
}

// 获取页面元素
const localVideo = document.getElementById('localVideo') as HTMLVideoElement;
const remoteVideo = document.getElementById('remoteVideo') as HTMLVideoElement;
const startCallButton = document.getElementById('startCall') as HTMLButtonElement;
const hangUpButton = document.getElementById('hangUp') as HTMLButtonElement;

const SIGNALING_SERVER_URL = "ws://localhost:8080/ws"; // 你的信令服务器地址
const ws = new WebSocket(SIGNALING_SERVER_URL);

let peerConnection: RTCPeerConnection | null = null;
let localStream: MediaStream | null = null;

// ---------- WebSocket 信令处理 ----------
ws.onopen = () => {
console.log("WebSocket 连接成功!等待其他客户端加入...");
};

ws.onmessage = async (event: MessageEvent) => {
const message: SignalingMessage = JSON.parse(event.data);
console.log("收到信令消息:", message);

if (!peerConnection) {
// 如果没有 PeerConnection,且收到 offer,则创建并响应
if (message.type === "offer") {
try {
// 如果是第一次收到 offer,则需要先获取本地媒体流
if (!localStream) {
localStream = await getLocalMediaStream(localVideo);
if (!localStream) {
console.error("无法获取本地媒体流,无法建立连接");
return;
}
}
peerConnection = createPeerConnection();
localStream.getTracks().forEach(track => peerConnection!.addTrack(track, localStream!));

await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp!));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
ws.send(JSON.stringify({ type: "answer", sdp: answer }));
} catch (error) {
console.error("处理 Offer 失败:", error);
}
}
} else { // PeerConnection 已经存在
if (message.type === "offer") {
await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp!));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
ws.send(JSON.stringify({ type: "answer", sdp: answer }));
} else if (message.type === "answer") {
await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp!));
} else if (message.type === "ice-candidate") {
try {
// 确保远程描述已设置
if (peerConnection.remoteDescription) {
await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate!));
} else {
console.warn("远程描述未设置,ICE 候选者暂存或延迟添加");
// 在实际应用中,你可能需要将这些候选者暂存起来,待远程描述设置后再添加
}
} catch (error) {
console.error("添加 ICE 候选者失败:", error);
}
}
}
};

ws.onclose = () => {
console.log("WebSocket 连接已关闭");
};

ws.onerror = (error) => {
console.error("WebSocket 错误:", error);
};

// ---------- WebRTC 核心逻辑 ----------

// 封装创建 RTCPeerConnection 的过程
function createPeerConnection(): RTCPeerConnection {
const configuration: RTCConfiguration = {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
};
const pc = new RTCPeerConnection(configuration);

pc.ontrack = (event: RTCTrackEvent) => {
if (remoteVideo.srcObject !== event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
console.log("收到远程流");
}
};

pc.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
console.log("发送本地 ICE 候选者:", event.candidate);
ws.send(JSON.stringify({ type: "ice-candidate", candidate: event.candidate }));
}
};

pc.onconnectionstatechange = () => {
console.log("Peer Connection 状态:", pc.connectionState);
};

return pc;
}

// 获取本地媒体流
async function getLocalMediaStream(videoElement: HTMLVideoElement): Promise<MediaStream | undefined> {
const constraints: MediaStreamConstraints = { audio: true, video: true };
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
videoElement.srcObject = stream;
return stream;
} catch (error) {
console.error("无法获取本地媒体流:", error);
alert(`请授权访问麦克风和摄像头: ${error}`);
return undefined;
}
}

// 开始呼叫
startCallButton.onclick = async () => {
startCallButton.disabled = true;
hangUpButton.disabled = false;

localStream = await getLocalMediaStream(localVideo);
if (!localStream) return;

peerConnection = createPeerConnection();
localStream.getTracks().forEach(track => peerConnection!.addTrack(track, localStream!));

try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
console.log("发送 Offer SDP:", offer);
ws.send(JSON.stringify({ type: "offer", sdp: offer }));
} catch (error) {
console.error("创建 Offer 失败:", error);
}
};

// 挂断
hangUpButton.onclick = () => {
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
localVideo.srcObject = null;
remoteVideo.srcObject = null;
startCallButton.disabled = false;
hangUpButton.disabled = true;
console.log("呼叫已挂断");
};

// 初始化按钮状态
hangUpButton.disabled = true;

编译 TypeScript

你需要一个 tsconfig.json 文件来编译 TypeScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"compilerOptions": {
"target": "ES2020", // 目标 JavaScript 版本
"module": "commonjs", // 模块系统
"strict": true, // 启用所有严格类型检查
"esModuleInterop": true, // 允许默认导入 CommonJS 模块
"skipLibCheck": true, // 跳过声明文件检查
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 根目录
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

然后,你可以使用 tsc 命令编译:tsc

运行 Go 信令服务器:go run your_signaling_server.go
然后在浏览器中打开 index.html。同时打开两个浏览器标签页(或者在不同的设备上打开),点击“开始呼叫”,它们将尝试建立 WebRTC 连接。

七、总结

WebRTC 是一项革命性的技术,它将实时通信能力内建到 Web 浏览器中,为开发者提供了强大的工具来构建丰富、交互性强的在线应用。通过利用点对点连接、强制加密以及对音视频和数据通道的全面支持,WebRTC 降低了实时通信的门槛,并推动了视频会议、在线教育、协作工具等领域的发展。尽管其内部机制复杂,但其标准化的 API 结合 TypeScript 的类型安全优势使得开发者能够相对便捷地在 Web 平台实现高性能的实时交互。