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.
@@ -0,0 +1,33 @@
1
+ export interface RTCTreeClientOptions {
2
+ onStreamReceived?: (stream: MediaStream) => void;
3
+ onStatusChange?: (status: string) => void;
4
+ onError?: (error: any) => void;
5
+ fetchParentIdFn?: () => Promise<string | null>;
6
+ reportDeadFn?: (deadPeerId: string) => Promise<void>;
7
+ }
8
+ export declare class RTCTreeClient {
9
+ private options;
10
+ private peer;
11
+ private myPeerId;
12
+ private myStream;
13
+ private activeCalls;
14
+ private parentConnection;
15
+ private isStreamer;
16
+ private isReconnecting;
17
+ constructor(options: RTCTreeClientOptions);
18
+ /**
19
+ * 初始化為直播主 (Streamer)
20
+ * @param stream 本地攝影機/麥克風的 MediaStream
21
+ * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)
22
+ * @returns 建立完成後的 Peer ID
23
+ */
24
+ initStreamer(stream: MediaStream, maxChildren?: number): Promise<string>;
25
+ /**
26
+ * 初始化為觀眾 (Viewer)
27
+ * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)
28
+ */
29
+ initViewer(maxChildren?: number): Promise<string>;
30
+ private connectToMesh;
31
+ private handleParentDisconnect;
32
+ destroy(): void;
33
+ }
@@ -0,0 +1 @@
1
+ export * from './RTCTreeClient';
@@ -0,0 +1,205 @@
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/client/index.ts
31
+ var client_exports = {};
32
+ __export(client_exports, {
33
+ RTCTreeClient: () => RTCTreeClient
34
+ });
35
+ module.exports = __toCommonJS(client_exports);
36
+
37
+ // src/client/RTCTreeClient.ts
38
+ var import_peerjs = __toESM(require("peerjs"));
39
+ var RTCTreeClient = class {
40
+ constructor(options) {
41
+ this.options = options;
42
+ this.peer = null;
43
+ this.myPeerId = null;
44
+ this.myStream = null;
45
+ this.activeCalls = {};
46
+ this.parentConnection = null;
47
+ // State
48
+ this.isStreamer = false;
49
+ this.isReconnecting = false;
50
+ }
51
+ /**
52
+ * 初始化為直播主 (Streamer)
53
+ * @param stream 本地攝影機/麥克風的 MediaStream
54
+ * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)
55
+ * @returns 建立完成後的 Peer ID
56
+ */
57
+ initStreamer(stream, maxChildren = 4) {
58
+ this.isStreamer = true;
59
+ this.myStream = stream;
60
+ return new Promise((resolve, reject) => {
61
+ this.peer = new import_peerjs.default();
62
+ this.peer.on("open", (id) => {
63
+ this.myPeerId = id;
64
+ this.options.onStatusChange?.("\u76F4\u64AD\u5DF2\u958B\u59CB");
65
+ resolve(id);
66
+ });
67
+ this.peer.on("error", (err) => {
68
+ this.options.onError?.(err);
69
+ reject(err);
70
+ });
71
+ this.peer.on("connection", (conn) => {
72
+ conn.on("data", (data) => {
73
+ if (data === "VIEWER_READY") {
74
+ if (Object.keys(this.activeCalls).length < maxChildren) {
75
+ const call = this.peer.call(conn.peer, this.myStream);
76
+ this.activeCalls[conn.peer] = call;
77
+ } else {
78
+ conn.send({ type: "REJECT_FULL" });
79
+ }
80
+ }
81
+ });
82
+ conn.on("close", () => {
83
+ if (this.activeCalls[conn.peer]) {
84
+ this.activeCalls[conn.peer].close();
85
+ delete this.activeCalls[conn.peer];
86
+ }
87
+ });
88
+ });
89
+ });
90
+ }
91
+ /**
92
+ * 初始化為觀眾 (Viewer)
93
+ * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)
94
+ */
95
+ initViewer(maxChildren = 4) {
96
+ this.isStreamer = false;
97
+ return new Promise((resolve, reject) => {
98
+ this.peer = new import_peerjs.default();
99
+ this.peer.on("open", async (id) => {
100
+ this.myPeerId = id;
101
+ this.options.onStatusChange?.("\u6B63\u5728\u5206\u914D\u7BC0\u9EDE...");
102
+ resolve(id);
103
+ await this.connectToMesh();
104
+ });
105
+ this.peer.on("error", (err) => {
106
+ this.options.onError?.(err);
107
+ reject(err);
108
+ });
109
+ this.peer.on("connection", (conn) => {
110
+ conn.on("data", (data) => {
111
+ if (data === "VIEWER_READY") {
112
+ if (Object.keys(this.activeCalls).length < maxChildren && this.myStream) {
113
+ const call = this.peer.call(conn.peer, this.myStream);
114
+ this.activeCalls[conn.peer] = call;
115
+ } else {
116
+ conn.send({ type: "REJECT_FULL" });
117
+ }
118
+ }
119
+ });
120
+ conn.on("close", () => {
121
+ if (this.activeCalls[conn.peer]) {
122
+ this.activeCalls[conn.peer].close();
123
+ delete this.activeCalls[conn.peer];
124
+ }
125
+ });
126
+ });
127
+ this.peer.on("call", (call) => {
128
+ this.options.onStatusChange?.("\u63A5\u6536\u5F71\u50CF\u4E2D...");
129
+ this.parentConnection = call;
130
+ call.answer();
131
+ call.on("stream", (remoteStream) => {
132
+ this.myStream = remoteStream;
133
+ this.options.onStatusChange?.("");
134
+ this.options.onStreamReceived?.(remoteStream);
135
+ });
136
+ call.on("close", () => {
137
+ this.handleParentDisconnect(call.peer);
138
+ });
139
+ });
140
+ });
141
+ }
142
+ async connectToMesh() {
143
+ if (!this.options.fetchParentIdFn) {
144
+ throw new Error("fetchParentIdFn is required for viewers");
145
+ }
146
+ try {
147
+ const targetPeerId = await this.options.fetchParentIdFn();
148
+ if (!targetPeerId) {
149
+ this.options.onStatusChange?.("\u66AB\u7121\u53EF\u7528\u7BC0\u9EDE\uFF0C\u8ACB\u7A0D\u5F8C\u91CD\u8A66");
150
+ return;
151
+ }
152
+ this.options.onStatusChange?.("\u9023\u7DDA\u4E2D...");
153
+ const conn = this.peer.connect(targetPeerId);
154
+ const timeoutId = setTimeout(() => {
155
+ conn.close();
156
+ this.handleParentDisconnect(targetPeerId);
157
+ }, 5e3);
158
+ conn.on("open", () => {
159
+ clearTimeout(timeoutId);
160
+ conn.send("VIEWER_READY");
161
+ });
162
+ conn.on("data", (data) => {
163
+ if (data && data.type === "REJECT_FULL") {
164
+ clearTimeout(timeoutId);
165
+ conn.close();
166
+ this.handleParentDisconnect(targetPeerId);
167
+ }
168
+ });
169
+ conn.on("close", () => {
170
+ clearTimeout(timeoutId);
171
+ this.handleParentDisconnect(targetPeerId);
172
+ });
173
+ conn.on("error", () => {
174
+ clearTimeout(timeoutId);
175
+ this.handleParentDisconnect(targetPeerId);
176
+ });
177
+ } catch (e) {
178
+ this.options.onStatusChange?.("\u9023\u7DDA\u5931\u6557");
179
+ console.error(e);
180
+ }
181
+ }
182
+ async handleParentDisconnect(deadPeerId) {
183
+ if (this.isReconnecting) return;
184
+ this.isReconnecting = true;
185
+ this.options.onStatusChange?.("\u4E0A\u5C64\u7BC0\u9EDE\u65B7\u7DDA\uFF0C\u91CD\u65B0\u5C0B\u627E\u8DEF\u5F91...");
186
+ if (this.options.reportDeadFn) {
187
+ await this.options.reportDeadFn(deadPeerId).catch(console.error);
188
+ }
189
+ setTimeout(async () => {
190
+ this.isReconnecting = false;
191
+ await this.connectToMesh();
192
+ }, 2e3);
193
+ }
194
+ destroy() {
195
+ if (this.peer) {
196
+ this.peer.destroy();
197
+ this.peer = null;
198
+ }
199
+ }
200
+ };
201
+ // Annotate the CommonJS export names for ESM import in node:
202
+ 0 && (module.exports = {
203
+ RTCTreeClient
204
+ });
205
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/index.ts","../../src/client/RTCTreeClient.ts"],"sourcesContent":["export * from './RTCTreeClient';\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"]}
@@ -0,0 +1,168 @@
1
+ // src/client/RTCTreeClient.ts
2
+ import Peer from "peerjs";
3
+ var RTCTreeClient = class {
4
+ constructor(options) {
5
+ this.options = options;
6
+ this.peer = null;
7
+ this.myPeerId = null;
8
+ this.myStream = null;
9
+ this.activeCalls = {};
10
+ this.parentConnection = null;
11
+ // State
12
+ this.isStreamer = false;
13
+ this.isReconnecting = false;
14
+ }
15
+ /**
16
+ * 初始化為直播主 (Streamer)
17
+ * @param stream 本地攝影機/麥克風的 MediaStream
18
+ * @param maxChildren 第一層最大觀眾數 (Layer 1 Capacity)
19
+ * @returns 建立完成後的 Peer ID
20
+ */
21
+ initStreamer(stream, maxChildren = 4) {
22
+ this.isStreamer = true;
23
+ this.myStream = stream;
24
+ return new Promise((resolve, reject) => {
25
+ this.peer = new Peer();
26
+ this.peer.on("open", (id) => {
27
+ this.myPeerId = id;
28
+ this.options.onStatusChange?.("\u76F4\u64AD\u5DF2\u958B\u59CB");
29
+ resolve(id);
30
+ });
31
+ this.peer.on("error", (err) => {
32
+ this.options.onError?.(err);
33
+ reject(err);
34
+ });
35
+ this.peer.on("connection", (conn) => {
36
+ conn.on("data", (data) => {
37
+ if (data === "VIEWER_READY") {
38
+ if (Object.keys(this.activeCalls).length < maxChildren) {
39
+ const call = this.peer.call(conn.peer, this.myStream);
40
+ this.activeCalls[conn.peer] = call;
41
+ } else {
42
+ conn.send({ type: "REJECT_FULL" });
43
+ }
44
+ }
45
+ });
46
+ conn.on("close", () => {
47
+ if (this.activeCalls[conn.peer]) {
48
+ this.activeCalls[conn.peer].close();
49
+ delete this.activeCalls[conn.peer];
50
+ }
51
+ });
52
+ });
53
+ });
54
+ }
55
+ /**
56
+ * 初始化為觀眾 (Viewer)
57
+ * @param maxChildren 轉發最大觀眾數 (Layer N Capacity)
58
+ */
59
+ initViewer(maxChildren = 4) {
60
+ this.isStreamer = false;
61
+ return new Promise((resolve, reject) => {
62
+ this.peer = new Peer();
63
+ this.peer.on("open", async (id) => {
64
+ this.myPeerId = id;
65
+ this.options.onStatusChange?.("\u6B63\u5728\u5206\u914D\u7BC0\u9EDE...");
66
+ resolve(id);
67
+ await this.connectToMesh();
68
+ });
69
+ this.peer.on("error", (err) => {
70
+ this.options.onError?.(err);
71
+ reject(err);
72
+ });
73
+ this.peer.on("connection", (conn) => {
74
+ conn.on("data", (data) => {
75
+ if (data === "VIEWER_READY") {
76
+ if (Object.keys(this.activeCalls).length < maxChildren && this.myStream) {
77
+ const call = this.peer.call(conn.peer, this.myStream);
78
+ this.activeCalls[conn.peer] = call;
79
+ } else {
80
+ conn.send({ type: "REJECT_FULL" });
81
+ }
82
+ }
83
+ });
84
+ conn.on("close", () => {
85
+ if (this.activeCalls[conn.peer]) {
86
+ this.activeCalls[conn.peer].close();
87
+ delete this.activeCalls[conn.peer];
88
+ }
89
+ });
90
+ });
91
+ this.peer.on("call", (call) => {
92
+ this.options.onStatusChange?.("\u63A5\u6536\u5F71\u50CF\u4E2D...");
93
+ this.parentConnection = call;
94
+ call.answer();
95
+ call.on("stream", (remoteStream) => {
96
+ this.myStream = remoteStream;
97
+ this.options.onStatusChange?.("");
98
+ this.options.onStreamReceived?.(remoteStream);
99
+ });
100
+ call.on("close", () => {
101
+ this.handleParentDisconnect(call.peer);
102
+ });
103
+ });
104
+ });
105
+ }
106
+ async connectToMesh() {
107
+ if (!this.options.fetchParentIdFn) {
108
+ throw new Error("fetchParentIdFn is required for viewers");
109
+ }
110
+ try {
111
+ const targetPeerId = await this.options.fetchParentIdFn();
112
+ if (!targetPeerId) {
113
+ this.options.onStatusChange?.("\u66AB\u7121\u53EF\u7528\u7BC0\u9EDE\uFF0C\u8ACB\u7A0D\u5F8C\u91CD\u8A66");
114
+ return;
115
+ }
116
+ this.options.onStatusChange?.("\u9023\u7DDA\u4E2D...");
117
+ const conn = this.peer.connect(targetPeerId);
118
+ const timeoutId = setTimeout(() => {
119
+ conn.close();
120
+ this.handleParentDisconnect(targetPeerId);
121
+ }, 5e3);
122
+ conn.on("open", () => {
123
+ clearTimeout(timeoutId);
124
+ conn.send("VIEWER_READY");
125
+ });
126
+ conn.on("data", (data) => {
127
+ if (data && data.type === "REJECT_FULL") {
128
+ clearTimeout(timeoutId);
129
+ conn.close();
130
+ this.handleParentDisconnect(targetPeerId);
131
+ }
132
+ });
133
+ conn.on("close", () => {
134
+ clearTimeout(timeoutId);
135
+ this.handleParentDisconnect(targetPeerId);
136
+ });
137
+ conn.on("error", () => {
138
+ clearTimeout(timeoutId);
139
+ this.handleParentDisconnect(targetPeerId);
140
+ });
141
+ } catch (e) {
142
+ this.options.onStatusChange?.("\u9023\u7DDA\u5931\u6557");
143
+ console.error(e);
144
+ }
145
+ }
146
+ async handleParentDisconnect(deadPeerId) {
147
+ if (this.isReconnecting) return;
148
+ this.isReconnecting = true;
149
+ this.options.onStatusChange?.("\u4E0A\u5C64\u7BC0\u9EDE\u65B7\u7DDA\uFF0C\u91CD\u65B0\u5C0B\u627E\u8DEF\u5F91...");
150
+ if (this.options.reportDeadFn) {
151
+ await this.options.reportDeadFn(deadPeerId).catch(console.error);
152
+ }
153
+ setTimeout(async () => {
154
+ this.isReconnecting = false;
155
+ await this.connectToMesh();
156
+ }, 2e3);
157
+ }
158
+ destroy() {
159
+ if (this.peer) {
160
+ this.peer.destroy();
161
+ this.peer = null;
162
+ }
163
+ }
164
+ };
165
+ export {
166
+ RTCTreeClient
167
+ };
168
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/RTCTreeClient.ts"],"sourcesContent":["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":[]}
@@ -0,0 +1,2 @@
1
+ export * from './server/index';
2
+ export * from './client/index';