webrtc-tree 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,314 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ RTCTreeClient: () => RTCTreeClient,
34
+ RTCTreeCoordinator: () => RTCTreeCoordinator
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/server/RTCTreeCoordinator.ts
39
+ var RTCTreeCoordinator = class {
40
+ constructor() {
41
+ // roomId -> peerId -> Node Data
42
+ this.trees = {};
43
+ this.configs = {};
44
+ }
45
+ /**
46
+ * 建立一個新的直播房間拓撲結構
47
+ * @param roomId 房間 ID
48
+ * @param streamerPeerId 直播主的 Peer ID
49
+ * @param config 拓撲設定,例如 maxNodesPerLayer: [1, 4, 8, 16, 64]
50
+ */
51
+ createRoom(roomId, streamerPeerId, config) {
52
+ this.configs[roomId] = config;
53
+ this.trees[roomId] = {};
54
+ this.trees[roomId][streamerPeerId] = { children: [], parent: null, layer: 0 };
55
+ }
56
+ /**
57
+ * 新節點加入,透過 BFS 分配最合適的父節點
58
+ * @param roomId 房間 ID
59
+ * @param newPeerId 新節點的 Peer ID
60
+ * @returns 分配到的父節點 Peer ID,若無法分配則回傳 null
61
+ */
62
+ joinNode(roomId, newPeerId) {
63
+ const tree = this.trees[roomId];
64
+ const config = this.configs[roomId];
65
+ if (!tree || !config) return null;
66
+ let rootId = null;
67
+ for (const [id, node] of Object.entries(tree)) {
68
+ if (node.layer === 0) {
69
+ rootId = id;
70
+ break;
71
+ }
72
+ }
73
+ if (!rootId) return null;
74
+ if (tree[newPeerId]) {
75
+ this.removeNode(roomId, newPeerId);
76
+ }
77
+ const layerCounts = {};
78
+ for (const node of Object.values(tree)) {
79
+ layerCounts[node.layer] = (layerCounts[node.layer] || 0) + 1;
80
+ }
81
+ const queue = [rootId];
82
+ while (queue.length > 0) {
83
+ const currentId = queue.shift();
84
+ const currentNode = tree[currentId];
85
+ const currentLayer = currentNode.layer;
86
+ const nextLayer = currentLayer + 1;
87
+ if (nextLayer >= config.maxNodesPerLayer.length) {
88
+ queue.push(...currentNode.children);
89
+ continue;
90
+ }
91
+ const nextLayerMax = config.maxNodesPerLayer[nextLayer];
92
+ const currentNextLayerCount = layerCounts[nextLayer] || 0;
93
+ if (currentNextLayerCount >= nextLayerMax) {
94
+ queue.push(...currentNode.children);
95
+ continue;
96
+ }
97
+ const currentLayerMax = config.maxNodesPerLayer[currentLayer];
98
+ const maxChildrenPerNode = Math.floor(nextLayerMax / currentLayerMax) || 1;
99
+ if (currentNode.children.length < maxChildrenPerNode) {
100
+ currentNode.children.push(newPeerId);
101
+ tree[newPeerId] = {
102
+ children: [],
103
+ parent: currentId,
104
+ layer: nextLayer
105
+ };
106
+ return currentId;
107
+ }
108
+ queue.push(...currentNode.children);
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * 節點斷線,將其從樹中移除,並讓其子節點變成孤兒 (等待重新 join)
114
+ */
115
+ removeNode(roomId, deadPeerId) {
116
+ const tree = this.trees[roomId];
117
+ if (!tree) return;
118
+ const deadNode = tree[deadPeerId];
119
+ if (!deadNode) return;
120
+ if (deadNode.parent && tree[deadNode.parent]) {
121
+ const parentNode = tree[deadNode.parent];
122
+ parentNode.children = parentNode.children.filter((id) => id !== deadPeerId);
123
+ }
124
+ for (const childId of deadNode.children) {
125
+ if (tree[childId]) {
126
+ tree[childId].parent = null;
127
+ }
128
+ }
129
+ delete tree[deadPeerId];
130
+ }
131
+ /**
132
+ * 報告節點失效 (Self-Healing 觸發點)
133
+ */
134
+ reportDeadNode(roomId, deadPeerId) {
135
+ this.removeNode(roomId, deadPeerId);
136
+ }
137
+ /**
138
+ * 取得房間目前的樹狀結構 (For Debug/Monitor)
139
+ */
140
+ getTree(roomId) {
141
+ return this.trees[roomId] || null;
142
+ }
143
+ };
144
+
145
+ // src/client/RTCTreeClient.ts
146
+ var import_peerjs = __toESM(require("peerjs"));
147
+ var RTCTreeClient = class {
148
+ constructor(options) {
149
+ this.options = options;
150
+ this.peer = null;
151
+ this.myPeerId = null;
152
+ this.myStream = null;
153
+ this.activeCalls = {};
154
+ this.parentConnection = null;
155
+ // State
156
+ this.isStreamer = false;
157
+ this.isReconnecting = false;
158
+ }
159
+ /**
160
+ * 初始化為直播主 (Streamer)
161
+ * @param stream 本地攝影機/麥克風的 MediaStream
162
+ * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)
163
+ * @returns 建立完成後的 Peer ID
164
+ */
165
+ initStreamer(stream, maxChildren = 4) {
166
+ this.isStreamer = true;
167
+ this.myStream = stream;
168
+ return new Promise((resolve, reject) => {
169
+ this.peer = new import_peerjs.default();
170
+ this.peer.on("open", (id) => {
171
+ this.myPeerId = id;
172
+ this.options.onStatusChange?.("\u76F4\u64AD\u5DF2\u958B\u59CB");
173
+ resolve(id);
174
+ });
175
+ this.peer.on("error", (err) => {
176
+ this.options.onError?.(err);
177
+ reject(err);
178
+ });
179
+ this.peer.on("connection", (conn) => {
180
+ conn.on("data", (data) => {
181
+ if (data === "VIEWER_READY") {
182
+ if (Object.keys(this.activeCalls).length < maxChildren) {
183
+ const call = this.peer.call(conn.peer, this.myStream);
184
+ this.activeCalls[conn.peer] = call;
185
+ } else {
186
+ conn.send({ type: "REJECT_FULL" });
187
+ }
188
+ }
189
+ });
190
+ conn.on("close", () => {
191
+ if (this.activeCalls[conn.peer]) {
192
+ this.activeCalls[conn.peer].close();
193
+ delete this.activeCalls[conn.peer];
194
+ }
195
+ });
196
+ });
197
+ });
198
+ }
199
+ /**
200
+ * 初始化為觀眾 (Viewer)
201
+ * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)
202
+ */
203
+ initViewer(maxChildren = 4) {
204
+ this.isStreamer = false;
205
+ return new Promise((resolve, reject) => {
206
+ this.peer = new import_peerjs.default();
207
+ this.peer.on("open", async (id) => {
208
+ this.myPeerId = id;
209
+ this.options.onStatusChange?.("\u6B63\u5728\u5206\u914D\u7BC0\u9EDE...");
210
+ resolve(id);
211
+ await this.connectToMesh();
212
+ });
213
+ this.peer.on("error", (err) => {
214
+ this.options.onError?.(err);
215
+ reject(err);
216
+ });
217
+ this.peer.on("connection", (conn) => {
218
+ conn.on("data", (data) => {
219
+ if (data === "VIEWER_READY") {
220
+ if (Object.keys(this.activeCalls).length < maxChildren && this.myStream) {
221
+ const call = this.peer.call(conn.peer, this.myStream);
222
+ this.activeCalls[conn.peer] = call;
223
+ } else {
224
+ conn.send({ type: "REJECT_FULL" });
225
+ }
226
+ }
227
+ });
228
+ conn.on("close", () => {
229
+ if (this.activeCalls[conn.peer]) {
230
+ this.activeCalls[conn.peer].close();
231
+ delete this.activeCalls[conn.peer];
232
+ }
233
+ });
234
+ });
235
+ this.peer.on("call", (call) => {
236
+ this.options.onStatusChange?.("\u63A5\u6536\u5F71\u50CF\u4E2D...");
237
+ this.parentConnection = call;
238
+ call.answer();
239
+ call.on("stream", (remoteStream) => {
240
+ this.myStream = remoteStream;
241
+ this.options.onStatusChange?.("");
242
+ this.options.onStreamReceived?.(remoteStream);
243
+ });
244
+ call.on("close", () => {
245
+ this.handleParentDisconnect(call.peer);
246
+ });
247
+ });
248
+ });
249
+ }
250
+ async connectToMesh() {
251
+ if (!this.options.fetchParentIdFn) {
252
+ throw new Error("fetchParentIdFn is required for viewers");
253
+ }
254
+ try {
255
+ const targetPeerId = await this.options.fetchParentIdFn();
256
+ if (!targetPeerId) {
257
+ this.options.onStatusChange?.("\u66AB\u7121\u53EF\u7528\u7BC0\u9EDE\uFF0C\u8ACB\u7A0D\u5F8C\u91CD\u8A66");
258
+ return;
259
+ }
260
+ this.options.onStatusChange?.("\u9023\u7DDA\u4E2D...");
261
+ const conn = this.peer.connect(targetPeerId);
262
+ const timeoutId = setTimeout(() => {
263
+ conn.close();
264
+ this.handleParentDisconnect(targetPeerId);
265
+ }, 5e3);
266
+ conn.on("open", () => {
267
+ clearTimeout(timeoutId);
268
+ conn.send("VIEWER_READY");
269
+ });
270
+ conn.on("data", (data) => {
271
+ if (data && data.type === "REJECT_FULL") {
272
+ clearTimeout(timeoutId);
273
+ conn.close();
274
+ this.handleParentDisconnect(targetPeerId);
275
+ }
276
+ });
277
+ conn.on("close", () => {
278
+ clearTimeout(timeoutId);
279
+ this.handleParentDisconnect(targetPeerId);
280
+ });
281
+ conn.on("error", () => {
282
+ clearTimeout(timeoutId);
283
+ this.handleParentDisconnect(targetPeerId);
284
+ });
285
+ } catch (e) {
286
+ this.options.onStatusChange?.("\u9023\u7DDA\u5931\u6557");
287
+ console.error(e);
288
+ }
289
+ }
290
+ async handleParentDisconnect(deadPeerId) {
291
+ if (this.isReconnecting) return;
292
+ this.isReconnecting = true;
293
+ this.options.onStatusChange?.("\u4E0A\u5C64\u7BC0\u9EDE\u65B7\u7DDA\uFF0C\u91CD\u65B0\u5C0B\u627E\u8DEF\u5F91...");
294
+ if (this.options.reportDeadFn) {
295
+ await this.options.reportDeadFn(deadPeerId).catch(console.error);
296
+ }
297
+ setTimeout(async () => {
298
+ this.isReconnecting = false;
299
+ await this.connectToMesh();
300
+ }, 2e3);
301
+ }
302
+ destroy() {
303
+ if (this.peer) {
304
+ this.peer.destroy();
305
+ this.peer = null;
306
+ }
307
+ }
308
+ };
309
+ // Annotate the CommonJS export names for ESM import in node:
310
+ 0 && (module.exports = {
311
+ RTCTreeClient,
312
+ RTCTreeCoordinator
313
+ });
314
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/server/RTCTreeCoordinator.ts","../src/client/RTCTreeClient.ts"],"sourcesContent":["export * from './server/index';\nexport * from './client/index';\n","export interface RTCTreeNode {\n children: string[];\n parent: string | null;\n layer: number;\n}\n\nexport interface RoomConfig {\n maxNodesPerLayer: number[]; // e.g. [1, 4, 8, 16, 64]\n}\n\nexport class RTCTreeCoordinator {\n // roomId -> peerId -> Node Data\n private trees: Record<string, Record<string, RTCTreeNode>> = {};\n private configs: Record<string, RoomConfig> = {};\n\n /**\n * 建立一個新的直播房間拓撲結構\n * @param roomId 房間 ID\n * @param streamerPeerId 直播主的 Peer ID\n * @param config 拓撲設定,例如 maxNodesPerLayer: [1, 4, 8, 16, 64]\n */\n public createRoom(roomId: string, streamerPeerId: string, config: RoomConfig): void {\n this.configs[roomId] = config;\n this.trees[roomId] = {};\n // Streamer is at layer 0\n this.trees[roomId][streamerPeerId] = { children: [], parent: null, layer: 0 };\n }\n\n /**\n * 新節點加入,透過 BFS 分配最合適的父節點\n * @param roomId 房間 ID\n * @param newPeerId 新節點的 Peer ID\n * @returns 分配到的父節點 Peer ID,若無法分配則回傳 null\n */\n public joinNode(roomId: string, newPeerId: string): string | null {\n const tree = this.trees[roomId];\n const config = this.configs[roomId];\n if (!tree || !config) return null;\n\n // 尋找樹根 (layer 0 的節點,通常是 streamer)\n let rootId: string | null = null;\n for (const [id, node] of Object.entries(tree)) {\n if (node.layer === 0) {\n rootId = id;\n break;\n }\n }\n\n if (!rootId) return null;\n\n // 如果該節點已經在樹中,先將其從原本的位置移除 (防止重複加入或狀態不一致)\n if (tree[newPeerId]) {\n this.removeNode(roomId, newPeerId);\n }\n\n // 計算目前每一層的總節點數\n const layerCounts: Record<number, number> = {};\n for (const node of Object.values(tree)) {\n layerCounts[node.layer] = (layerCounts[node.layer] || 0) + 1;\n }\n\n // BFS 尋找未滿載的節點\n const queue: string[] = [rootId];\n \n while (queue.length > 0) {\n const currentId = queue.shift()!;\n const currentNode = tree[currentId];\n const currentLayer = currentNode.layer;\n \n // 下一層的索引\n const nextLayer = currentLayer + 1;\n\n // 如果已經達到最大層數設定,該節點不能再有子節點\n if (nextLayer >= config.maxNodesPerLayer.length) {\n // Continue searching in queue\n queue.push(...currentNode.children);\n continue;\n }\n\n // 檢查下一層是否已經達到整體上限\n const nextLayerMax = config.maxNodesPerLayer[nextLayer];\n const currentNextLayerCount = layerCounts[nextLayer] || 0;\n\n if (currentNextLayerCount >= nextLayerMax) {\n // 下一層已經滿了,把目前節點的子節點加入 queue 繼續尋找更下層\n queue.push(...currentNode.children);\n continue;\n }\n\n // 如果下一層還沒滿,那我們要決定「目前這個節點」還能不能接客\n // 算法:該層平均每個節點可以接的子節點數\n // 例如 layer 1 最大 4 人,layer 2 最大 8 人,代表 layer 1 每個節點最多接 8/4 = 2 人\n const currentLayerMax = config.maxNodesPerLayer[currentLayer];\n const maxChildrenPerNode = Math.floor(nextLayerMax / currentLayerMax) || 1;\n\n if (currentNode.children.length < maxChildrenPerNode) {\n // 找到可以接客的節點了!\n currentNode.children.push(newPeerId);\n tree[newPeerId] = {\n children: [],\n parent: currentId,\n layer: nextLayer\n };\n return currentId;\n }\n\n // 目前節點滿了,把它的小孩加進 queue\n queue.push(...currentNode.children);\n }\n\n return null; // 樹已滿或無法分配\n }\n\n /**\n * 節點斷線,將其從樹中移除,並讓其子節點變成孤兒 (等待重新 join)\n */\n public removeNode(roomId: string, deadPeerId: string): void {\n const tree = this.trees[roomId];\n if (!tree) return;\n\n const deadNode = tree[deadPeerId];\n if (!deadNode) return;\n\n // 將自己從父節點的 children 中移除\n if (deadNode.parent && tree[deadNode.parent]) {\n const parentNode = tree[deadNode.parent];\n parentNode.children = parentNode.children.filter(id => id !== deadPeerId);\n }\n\n // 將子節點的 parent 設為 null (它們需要重新連線)\n for (const childId of deadNode.children) {\n if (tree[childId]) {\n tree[childId].parent = null;\n }\n }\n\n delete tree[deadPeerId];\n }\n\n /**\n * 報告節點失效 (Self-Healing 觸發點)\n */\n public reportDeadNode(roomId: string, deadPeerId: string): void {\n this.removeNode(roomId, deadPeerId);\n }\n\n /**\n * 取得房間目前的樹狀結構 (For Debug/Monitor)\n */\n public getTree(roomId: string): Record<string, RTCTreeNode> | null {\n return this.trees[roomId] || null;\n }\n}\n","import Peer, { MediaConnection } from 'peerjs';\n\nexport interface RTCTreeClientOptions {\n onStreamReceived?: (stream: MediaStream) => void;\n onStatusChange?: (status: string) => void;\n onError?: (error: any) => void;\n fetchParentIdFn?: () => Promise<string | null>; // Client uses this to ask Server for a parent\n reportDeadFn?: (deadPeerId: string) => Promise<void>; // Client uses this to tell Server a node is dead\n}\n\nexport class RTCTreeClient {\n private peer: Peer | null = null;\n private myPeerId: string | null = null;\n private myStream: MediaStream | null = null;\n private activeCalls: Record<string, MediaConnection> = {};\n private parentConnection: MediaConnection | null = null;\n \n // State\n private isStreamer: boolean = false;\n private isReconnecting: boolean = false;\n\n constructor(private options: RTCTreeClientOptions) {}\n\n /**\n * 初始化為直播主 (Streamer)\n * @param stream 本地攝影機/麥克風的 MediaStream\n * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)\n * @returns 建立完成後的 Peer ID\n */\n public initStreamer(stream: MediaStream, maxChildren: number = 4): Promise<string> {\n this.isStreamer = true;\n this.myStream = stream;\n\n return new Promise((resolve, reject) => {\n this.peer = new Peer();\n\n this.peer.on('open', (id) => {\n this.myPeerId = id;\n this.options.onStatusChange?.(\"直播已開始\");\n resolve(id);\n });\n\n this.peer.on('error', (err) => {\n this.options.onError?.(err);\n reject(err);\n });\n\n // 監聽觀眾連線請求\n this.peer.on('connection', (conn) => {\n conn.on('data', (data: any) => {\n if (data === 'VIEWER_READY') {\n if (Object.keys(this.activeCalls).length < maxChildren) {\n const call = this.peer!.call(conn.peer, this.myStream!);\n this.activeCalls[conn.peer] = call;\n } else {\n conn.send({ type: 'REJECT_FULL' });\n }\n }\n });\n\n conn.on('close', () => {\n if (this.activeCalls[conn.peer]) {\n this.activeCalls[conn.peer].close();\n delete this.activeCalls[conn.peer];\n }\n });\n });\n });\n }\n\n /**\n * 初始化為觀眾 (Viewer)\n * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)\n */\n public initViewer(maxChildren: number = 4): Promise<string> {\n this.isStreamer = false;\n\n return new Promise((resolve, reject) => {\n this.peer = new Peer();\n\n this.peer.on('open', async (id) => {\n this.myPeerId = id;\n this.options.onStatusChange?.(\"正在分配節點...\");\n resolve(id);\n \n // 開始加入流程\n await this.connectToMesh();\n });\n\n this.peer.on('error', (err) => {\n this.options.onError?.(err);\n reject(err);\n });\n\n // 當我們自己也是別人的 parent 時,處理下層觀眾連線\n this.peer.on('connection', (conn) => {\n conn.on('data', (data: any) => {\n if (data === 'VIEWER_READY') {\n if (Object.keys(this.activeCalls).length < maxChildren && this.myStream) {\n const call = this.peer!.call(conn.peer, this.myStream);\n this.activeCalls[conn.peer] = call;\n } else {\n conn.send({ type: 'REJECT_FULL' });\n }\n }\n });\n\n conn.on('close', () => {\n if (this.activeCalls[conn.peer]) {\n this.activeCalls[conn.peer].close();\n delete this.activeCalls[conn.peer];\n }\n });\n });\n\n // 接收上層傳來的影像\n this.peer.on('call', (call) => {\n this.options.onStatusChange?.(\"接收影像中...\");\n this.parentConnection = call;\n call.answer(); \n \n call.on('stream', (remoteStream) => {\n this.myStream = remoteStream;\n this.options.onStatusChange?.(\"\"); \n this.options.onStreamReceived?.(remoteStream);\n });\n\n call.on('close', () => {\n // 上層斷線!啟動 Self-Healing\n this.handleParentDisconnect(call.peer);\n });\n });\n });\n }\n\n private async connectToMesh(): Promise<void> {\n if (!this.options.fetchParentIdFn) {\n throw new Error(\"fetchParentIdFn is required for viewers\");\n }\n\n try {\n const targetPeerId = await this.options.fetchParentIdFn();\n if (!targetPeerId) {\n this.options.onStatusChange?.(\"暫無可用節點,請稍後重試\");\n // 可以加上 retry 機制\n return;\n }\n\n this.options.onStatusChange?.(\"連線中...\");\n const conn = this.peer!.connect(targetPeerId);\n\n const timeoutId = setTimeout(() => {\n conn.close();\n this.handleParentDisconnect(targetPeerId);\n }, 5000);\n\n conn.on('open', () => {\n clearTimeout(timeoutId);\n conn.send('VIEWER_READY');\n });\n\n conn.on('data', (data: any) => {\n if (data && data.type === 'REJECT_FULL') {\n clearTimeout(timeoutId);\n conn.close();\n this.handleParentDisconnect(targetPeerId);\n }\n });\n\n conn.on('close', () => {\n clearTimeout(timeoutId);\n this.handleParentDisconnect(targetPeerId);\n });\n\n conn.on('error', () => {\n clearTimeout(timeoutId);\n this.handleParentDisconnect(targetPeerId);\n });\n\n } catch (e) {\n this.options.onStatusChange?.(\"連線失敗\");\n console.error(e);\n }\n }\n\n private async handleParentDisconnect(deadPeerId: string) {\n if (this.isReconnecting) return;\n this.isReconnecting = true;\n this.options.onStatusChange?.(\"上層節點斷線,重新尋找路徑...\");\n\n if (this.options.reportDeadFn) {\n await this.options.reportDeadFn(deadPeerId).catch(console.error);\n }\n\n // 延遲一下避免大量 request\n setTimeout(async () => {\n this.isReconnecting = false;\n await this.connectToMesh();\n }, 2000);\n }\n\n public destroy() {\n if (this.peer) {\n this.peer.destroy();\n this.peer = null;\n }\n }\n}\n"],"mappings":"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","names":["Peer"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,276 @@
1
+ // src/server/RTCTreeCoordinator.ts
2
+ var RTCTreeCoordinator = class {
3
+ constructor() {
4
+ // roomId -> peerId -> Node Data
5
+ this.trees = {};
6
+ this.configs = {};
7
+ }
8
+ /**
9
+ * 建立一個新的直播房間拓撲結構
10
+ * @param roomId 房間 ID
11
+ * @param streamerPeerId 直播主的 Peer ID
12
+ * @param config 拓撲設定,例如 maxNodesPerLayer: [1, 4, 8, 16, 64]
13
+ */
14
+ createRoom(roomId, streamerPeerId, config) {
15
+ this.configs[roomId] = config;
16
+ this.trees[roomId] = {};
17
+ this.trees[roomId][streamerPeerId] = { children: [], parent: null, layer: 0 };
18
+ }
19
+ /**
20
+ * 新節點加入,透過 BFS 分配最合適的父節點
21
+ * @param roomId 房間 ID
22
+ * @param newPeerId 新節點的 Peer ID
23
+ * @returns 分配到的父節點 Peer ID,若無法分配則回傳 null
24
+ */
25
+ joinNode(roomId, newPeerId) {
26
+ const tree = this.trees[roomId];
27
+ const config = this.configs[roomId];
28
+ if (!tree || !config) return null;
29
+ let rootId = null;
30
+ for (const [id, node] of Object.entries(tree)) {
31
+ if (node.layer === 0) {
32
+ rootId = id;
33
+ break;
34
+ }
35
+ }
36
+ if (!rootId) return null;
37
+ if (tree[newPeerId]) {
38
+ this.removeNode(roomId, newPeerId);
39
+ }
40
+ const layerCounts = {};
41
+ for (const node of Object.values(tree)) {
42
+ layerCounts[node.layer] = (layerCounts[node.layer] || 0) + 1;
43
+ }
44
+ const queue = [rootId];
45
+ while (queue.length > 0) {
46
+ const currentId = queue.shift();
47
+ const currentNode = tree[currentId];
48
+ const currentLayer = currentNode.layer;
49
+ const nextLayer = currentLayer + 1;
50
+ if (nextLayer >= config.maxNodesPerLayer.length) {
51
+ queue.push(...currentNode.children);
52
+ continue;
53
+ }
54
+ const nextLayerMax = config.maxNodesPerLayer[nextLayer];
55
+ const currentNextLayerCount = layerCounts[nextLayer] || 0;
56
+ if (currentNextLayerCount >= nextLayerMax) {
57
+ queue.push(...currentNode.children);
58
+ continue;
59
+ }
60
+ const currentLayerMax = config.maxNodesPerLayer[currentLayer];
61
+ const maxChildrenPerNode = Math.floor(nextLayerMax / currentLayerMax) || 1;
62
+ if (currentNode.children.length < maxChildrenPerNode) {
63
+ currentNode.children.push(newPeerId);
64
+ tree[newPeerId] = {
65
+ children: [],
66
+ parent: currentId,
67
+ layer: nextLayer
68
+ };
69
+ return currentId;
70
+ }
71
+ queue.push(...currentNode.children);
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * 節點斷線,將其從樹中移除,並讓其子節點變成孤兒 (等待重新 join)
77
+ */
78
+ removeNode(roomId, deadPeerId) {
79
+ const tree = this.trees[roomId];
80
+ if (!tree) return;
81
+ const deadNode = tree[deadPeerId];
82
+ if (!deadNode) return;
83
+ if (deadNode.parent && tree[deadNode.parent]) {
84
+ const parentNode = tree[deadNode.parent];
85
+ parentNode.children = parentNode.children.filter((id) => id !== deadPeerId);
86
+ }
87
+ for (const childId of deadNode.children) {
88
+ if (tree[childId]) {
89
+ tree[childId].parent = null;
90
+ }
91
+ }
92
+ delete tree[deadPeerId];
93
+ }
94
+ /**
95
+ * 報告節點失效 (Self-Healing 觸發點)
96
+ */
97
+ reportDeadNode(roomId, deadPeerId) {
98
+ this.removeNode(roomId, deadPeerId);
99
+ }
100
+ /**
101
+ * 取得房間目前的樹狀結構 (For Debug/Monitor)
102
+ */
103
+ getTree(roomId) {
104
+ return this.trees[roomId] || null;
105
+ }
106
+ };
107
+
108
+ // src/client/RTCTreeClient.ts
109
+ import Peer from "peerjs";
110
+ var RTCTreeClient = class {
111
+ constructor(options) {
112
+ this.options = options;
113
+ this.peer = null;
114
+ this.myPeerId = null;
115
+ this.myStream = null;
116
+ this.activeCalls = {};
117
+ this.parentConnection = null;
118
+ // State
119
+ this.isStreamer = false;
120
+ this.isReconnecting = false;
121
+ }
122
+ /**
123
+ * 初始化為直播主 (Streamer)
124
+ * @param stream 本地攝影機/麥克風的 MediaStream
125
+ * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)
126
+ * @returns 建立完成後的 Peer ID
127
+ */
128
+ initStreamer(stream, maxChildren = 4) {
129
+ this.isStreamer = true;
130
+ this.myStream = stream;
131
+ return new Promise((resolve, reject) => {
132
+ this.peer = new Peer();
133
+ this.peer.on("open", (id) => {
134
+ this.myPeerId = id;
135
+ this.options.onStatusChange?.("\u76F4\u64AD\u5DF2\u958B\u59CB");
136
+ resolve(id);
137
+ });
138
+ this.peer.on("error", (err) => {
139
+ this.options.onError?.(err);
140
+ reject(err);
141
+ });
142
+ this.peer.on("connection", (conn) => {
143
+ conn.on("data", (data) => {
144
+ if (data === "VIEWER_READY") {
145
+ if (Object.keys(this.activeCalls).length < maxChildren) {
146
+ const call = this.peer.call(conn.peer, this.myStream);
147
+ this.activeCalls[conn.peer] = call;
148
+ } else {
149
+ conn.send({ type: "REJECT_FULL" });
150
+ }
151
+ }
152
+ });
153
+ conn.on("close", () => {
154
+ if (this.activeCalls[conn.peer]) {
155
+ this.activeCalls[conn.peer].close();
156
+ delete this.activeCalls[conn.peer];
157
+ }
158
+ });
159
+ });
160
+ });
161
+ }
162
+ /**
163
+ * 初始化為觀眾 (Viewer)
164
+ * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)
165
+ */
166
+ initViewer(maxChildren = 4) {
167
+ this.isStreamer = false;
168
+ return new Promise((resolve, reject) => {
169
+ this.peer = new Peer();
170
+ this.peer.on("open", async (id) => {
171
+ this.myPeerId = id;
172
+ this.options.onStatusChange?.("\u6B63\u5728\u5206\u914D\u7BC0\u9EDE...");
173
+ resolve(id);
174
+ await this.connectToMesh();
175
+ });
176
+ this.peer.on("error", (err) => {
177
+ this.options.onError?.(err);
178
+ reject(err);
179
+ });
180
+ this.peer.on("connection", (conn) => {
181
+ conn.on("data", (data) => {
182
+ if (data === "VIEWER_READY") {
183
+ if (Object.keys(this.activeCalls).length < maxChildren && this.myStream) {
184
+ const call = this.peer.call(conn.peer, this.myStream);
185
+ this.activeCalls[conn.peer] = call;
186
+ } else {
187
+ conn.send({ type: "REJECT_FULL" });
188
+ }
189
+ }
190
+ });
191
+ conn.on("close", () => {
192
+ if (this.activeCalls[conn.peer]) {
193
+ this.activeCalls[conn.peer].close();
194
+ delete this.activeCalls[conn.peer];
195
+ }
196
+ });
197
+ });
198
+ this.peer.on("call", (call) => {
199
+ this.options.onStatusChange?.("\u63A5\u6536\u5F71\u50CF\u4E2D...");
200
+ this.parentConnection = call;
201
+ call.answer();
202
+ call.on("stream", (remoteStream) => {
203
+ this.myStream = remoteStream;
204
+ this.options.onStatusChange?.("");
205
+ this.options.onStreamReceived?.(remoteStream);
206
+ });
207
+ call.on("close", () => {
208
+ this.handleParentDisconnect(call.peer);
209
+ });
210
+ });
211
+ });
212
+ }
213
+ async connectToMesh() {
214
+ if (!this.options.fetchParentIdFn) {
215
+ throw new Error("fetchParentIdFn is required for viewers");
216
+ }
217
+ try {
218
+ const targetPeerId = await this.options.fetchParentIdFn();
219
+ if (!targetPeerId) {
220
+ this.options.onStatusChange?.("\u66AB\u7121\u53EF\u7528\u7BC0\u9EDE\uFF0C\u8ACB\u7A0D\u5F8C\u91CD\u8A66");
221
+ return;
222
+ }
223
+ this.options.onStatusChange?.("\u9023\u7DDA\u4E2D...");
224
+ const conn = this.peer.connect(targetPeerId);
225
+ const timeoutId = setTimeout(() => {
226
+ conn.close();
227
+ this.handleParentDisconnect(targetPeerId);
228
+ }, 5e3);
229
+ conn.on("open", () => {
230
+ clearTimeout(timeoutId);
231
+ conn.send("VIEWER_READY");
232
+ });
233
+ conn.on("data", (data) => {
234
+ if (data && data.type === "REJECT_FULL") {
235
+ clearTimeout(timeoutId);
236
+ conn.close();
237
+ this.handleParentDisconnect(targetPeerId);
238
+ }
239
+ });
240
+ conn.on("close", () => {
241
+ clearTimeout(timeoutId);
242
+ this.handleParentDisconnect(targetPeerId);
243
+ });
244
+ conn.on("error", () => {
245
+ clearTimeout(timeoutId);
246
+ this.handleParentDisconnect(targetPeerId);
247
+ });
248
+ } catch (e) {
249
+ this.options.onStatusChange?.("\u9023\u7DDA\u5931\u6557");
250
+ console.error(e);
251
+ }
252
+ }
253
+ async handleParentDisconnect(deadPeerId) {
254
+ if (this.isReconnecting) return;
255
+ this.isReconnecting = true;
256
+ this.options.onStatusChange?.("\u4E0A\u5C64\u7BC0\u9EDE\u65B7\u7DDA\uFF0C\u91CD\u65B0\u5C0B\u627E\u8DEF\u5F91...");
257
+ if (this.options.reportDeadFn) {
258
+ await this.options.reportDeadFn(deadPeerId).catch(console.error);
259
+ }
260
+ setTimeout(async () => {
261
+ this.isReconnecting = false;
262
+ await this.connectToMesh();
263
+ }, 2e3);
264
+ }
265
+ destroy() {
266
+ if (this.peer) {
267
+ this.peer.destroy();
268
+ this.peer = null;
269
+ }
270
+ }
271
+ };
272
+ export {
273
+ RTCTreeClient,
274
+ RTCTreeCoordinator
275
+ };
276
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/RTCTreeCoordinator.ts","../src/client/RTCTreeClient.ts"],"sourcesContent":["export interface RTCTreeNode {\n children: string[];\n parent: string | null;\n layer: number;\n}\n\nexport interface RoomConfig {\n maxNodesPerLayer: number[]; // e.g. [1, 4, 8, 16, 64]\n}\n\nexport class RTCTreeCoordinator {\n // roomId -> peerId -> Node Data\n private trees: Record<string, Record<string, RTCTreeNode>> = {};\n private configs: Record<string, RoomConfig> = {};\n\n /**\n * 建立一個新的直播房間拓撲結構\n * @param roomId 房間 ID\n * @param streamerPeerId 直播主的 Peer ID\n * @param config 拓撲設定,例如 maxNodesPerLayer: [1, 4, 8, 16, 64]\n */\n public createRoom(roomId: string, streamerPeerId: string, config: RoomConfig): void {\n this.configs[roomId] = config;\n this.trees[roomId] = {};\n // Streamer is at layer 0\n this.trees[roomId][streamerPeerId] = { children: [], parent: null, layer: 0 };\n }\n\n /**\n * 新節點加入,透過 BFS 分配最合適的父節點\n * @param roomId 房間 ID\n * @param newPeerId 新節點的 Peer ID\n * @returns 分配到的父節點 Peer ID,若無法分配則回傳 null\n */\n public joinNode(roomId: string, newPeerId: string): string | null {\n const tree = this.trees[roomId];\n const config = this.configs[roomId];\n if (!tree || !config) return null;\n\n // 尋找樹根 (layer 0 的節點,通常是 streamer)\n let rootId: string | null = null;\n for (const [id, node] of Object.entries(tree)) {\n if (node.layer === 0) {\n rootId = id;\n break;\n }\n }\n\n if (!rootId) return null;\n\n // 如果該節點已經在樹中,先將其從原本的位置移除 (防止重複加入或狀態不一致)\n if (tree[newPeerId]) {\n this.removeNode(roomId, newPeerId);\n }\n\n // 計算目前每一層的總節點數\n const layerCounts: Record<number, number> = {};\n for (const node of Object.values(tree)) {\n layerCounts[node.layer] = (layerCounts[node.layer] || 0) + 1;\n }\n\n // BFS 尋找未滿載的節點\n const queue: string[] = [rootId];\n \n while (queue.length > 0) {\n const currentId = queue.shift()!;\n const currentNode = tree[currentId];\n const currentLayer = currentNode.layer;\n \n // 下一層的索引\n const nextLayer = currentLayer + 1;\n\n // 如果已經達到最大層數設定,該節點不能再有子節點\n if (nextLayer >= config.maxNodesPerLayer.length) {\n // Continue searching in queue\n queue.push(...currentNode.children);\n continue;\n }\n\n // 檢查下一層是否已經達到整體上限\n const nextLayerMax = config.maxNodesPerLayer[nextLayer];\n const currentNextLayerCount = layerCounts[nextLayer] || 0;\n\n if (currentNextLayerCount >= nextLayerMax) {\n // 下一層已經滿了,把目前節點的子節點加入 queue 繼續尋找更下層\n queue.push(...currentNode.children);\n continue;\n }\n\n // 如果下一層還沒滿,那我們要決定「目前這個節點」還能不能接客\n // 算法:該層平均每個節點可以接的子節點數\n // 例如 layer 1 最大 4 人,layer 2 最大 8 人,代表 layer 1 每個節點最多接 8/4 = 2 人\n const currentLayerMax = config.maxNodesPerLayer[currentLayer];\n const maxChildrenPerNode = Math.floor(nextLayerMax / currentLayerMax) || 1;\n\n if (currentNode.children.length < maxChildrenPerNode) {\n // 找到可以接客的節點了!\n currentNode.children.push(newPeerId);\n tree[newPeerId] = {\n children: [],\n parent: currentId,\n layer: nextLayer\n };\n return currentId;\n }\n\n // 目前節點滿了,把它的小孩加進 queue\n queue.push(...currentNode.children);\n }\n\n return null; // 樹已滿或無法分配\n }\n\n /**\n * 節點斷線,將其從樹中移除,並讓其子節點變成孤兒 (等待重新 join)\n */\n public removeNode(roomId: string, deadPeerId: string): void {\n const tree = this.trees[roomId];\n if (!tree) return;\n\n const deadNode = tree[deadPeerId];\n if (!deadNode) return;\n\n // 將自己從父節點的 children 中移除\n if (deadNode.parent && tree[deadNode.parent]) {\n const parentNode = tree[deadNode.parent];\n parentNode.children = parentNode.children.filter(id => id !== deadPeerId);\n }\n\n // 將子節點的 parent 設為 null (它們需要重新連線)\n for (const childId of deadNode.children) {\n if (tree[childId]) {\n tree[childId].parent = null;\n }\n }\n\n delete tree[deadPeerId];\n }\n\n /**\n * 報告節點失效 (Self-Healing 觸發點)\n */\n public reportDeadNode(roomId: string, deadPeerId: string): void {\n this.removeNode(roomId, deadPeerId);\n }\n\n /**\n * 取得房間目前的樹狀結構 (For Debug/Monitor)\n */\n public getTree(roomId: string): Record<string, RTCTreeNode> | null {\n return this.trees[roomId] || null;\n }\n}\n","import Peer, { MediaConnection } from 'peerjs';\n\nexport interface RTCTreeClientOptions {\n onStreamReceived?: (stream: MediaStream) => void;\n onStatusChange?: (status: string) => void;\n onError?: (error: any) => void;\n fetchParentIdFn?: () => Promise<string | null>; // Client uses this to ask Server for a parent\n reportDeadFn?: (deadPeerId: string) => Promise<void>; // Client uses this to tell Server a node is dead\n}\n\nexport class RTCTreeClient {\n private peer: Peer | null = null;\n private myPeerId: string | null = null;\n private myStream: MediaStream | null = null;\n private activeCalls: Record<string, MediaConnection> = {};\n private parentConnection: MediaConnection | null = null;\n \n // State\n private isStreamer: boolean = false;\n private isReconnecting: boolean = false;\n\n constructor(private options: RTCTreeClientOptions) {}\n\n /**\n * 初始化為直播主 (Streamer)\n * @param stream 本地攝影機/麥克風的 MediaStream\n * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)\n * @returns 建立完成後的 Peer ID\n */\n public initStreamer(stream: MediaStream, maxChildren: number = 4): Promise<string> {\n this.isStreamer = true;\n this.myStream = stream;\n\n return new Promise((resolve, reject) => {\n this.peer = new Peer();\n\n this.peer.on('open', (id) => {\n this.myPeerId = id;\n this.options.onStatusChange?.(\"直播已開始\");\n resolve(id);\n });\n\n this.peer.on('error', (err) => {\n this.options.onError?.(err);\n reject(err);\n });\n\n // 監聽觀眾連線請求\n this.peer.on('connection', (conn) => {\n conn.on('data', (data: any) => {\n if (data === 'VIEWER_READY') {\n if (Object.keys(this.activeCalls).length < maxChildren) {\n const call = this.peer!.call(conn.peer, this.myStream!);\n this.activeCalls[conn.peer] = call;\n } else {\n conn.send({ type: 'REJECT_FULL' });\n }\n }\n });\n\n conn.on('close', () => {\n if (this.activeCalls[conn.peer]) {\n this.activeCalls[conn.peer].close();\n delete this.activeCalls[conn.peer];\n }\n });\n });\n });\n }\n\n /**\n * 初始化為觀眾 (Viewer)\n * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)\n */\n public initViewer(maxChildren: number = 4): Promise<string> {\n this.isStreamer = false;\n\n return new Promise((resolve, reject) => {\n this.peer = new Peer();\n\n this.peer.on('open', async (id) => {\n this.myPeerId = id;\n this.options.onStatusChange?.(\"正在分配節點...\");\n resolve(id);\n \n // 開始加入流程\n await this.connectToMesh();\n });\n\n this.peer.on('error', (err) => {\n this.options.onError?.(err);\n reject(err);\n });\n\n // 當我們自己也是別人的 parent 時,處理下層觀眾連線\n this.peer.on('connection', (conn) => {\n conn.on('data', (data: any) => {\n if (data === 'VIEWER_READY') {\n if (Object.keys(this.activeCalls).length < maxChildren && this.myStream) {\n const call = this.peer!.call(conn.peer, this.myStream);\n this.activeCalls[conn.peer] = call;\n } else {\n conn.send({ type: 'REJECT_FULL' });\n }\n }\n });\n\n conn.on('close', () => {\n if (this.activeCalls[conn.peer]) {\n this.activeCalls[conn.peer].close();\n delete this.activeCalls[conn.peer];\n }\n });\n });\n\n // 接收上層傳來的影像\n this.peer.on('call', (call) => {\n this.options.onStatusChange?.(\"接收影像中...\");\n this.parentConnection = call;\n call.answer(); \n \n call.on('stream', (remoteStream) => {\n this.myStream = remoteStream;\n this.options.onStatusChange?.(\"\"); \n this.options.onStreamReceived?.(remoteStream);\n });\n\n call.on('close', () => {\n // 上層斷線!啟動 Self-Healing\n this.handleParentDisconnect(call.peer);\n });\n });\n });\n }\n\n private async connectToMesh(): Promise<void> {\n if (!this.options.fetchParentIdFn) {\n throw new Error(\"fetchParentIdFn is required for viewers\");\n }\n\n try {\n const targetPeerId = await this.options.fetchParentIdFn();\n if (!targetPeerId) {\n this.options.onStatusChange?.(\"暫無可用節點,請稍後重試\");\n // 可以加上 retry 機制\n return;\n }\n\n this.options.onStatusChange?.(\"連線中...\");\n const conn = this.peer!.connect(targetPeerId);\n\n const timeoutId = setTimeout(() => {\n conn.close();\n this.handleParentDisconnect(targetPeerId);\n }, 5000);\n\n conn.on('open', () => {\n clearTimeout(timeoutId);\n conn.send('VIEWER_READY');\n });\n\n conn.on('data', (data: any) => {\n if (data && data.type === 'REJECT_FULL') {\n clearTimeout(timeoutId);\n conn.close();\n this.handleParentDisconnect(targetPeerId);\n }\n });\n\n conn.on('close', () => {\n clearTimeout(timeoutId);\n this.handleParentDisconnect(targetPeerId);\n });\n\n conn.on('error', () => {\n clearTimeout(timeoutId);\n this.handleParentDisconnect(targetPeerId);\n });\n\n } catch (e) {\n this.options.onStatusChange?.(\"連線失敗\");\n console.error(e);\n }\n }\n\n private async handleParentDisconnect(deadPeerId: string) {\n if (this.isReconnecting) return;\n this.isReconnecting = true;\n this.options.onStatusChange?.(\"上層節點斷線,重新尋找路徑...\");\n\n if (this.options.reportDeadFn) {\n await this.options.reportDeadFn(deadPeerId).catch(console.error);\n }\n\n // 延遲一下避免大量 request\n setTimeout(async () => {\n this.isReconnecting = false;\n await this.connectToMesh();\n }, 2000);\n }\n\n public destroy() {\n if (this.peer) {\n this.peer.destroy();\n this.peer = null;\n }\n }\n}\n"],"mappings":"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","names":[]}