whepts 1.1.11 → 1.1.12

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 CHANGED
@@ -1,1498 +1 @@
1
- import EventEmitter from 'eventemitter3';
2
- import { atom } from 'nanostores';
3
-
4
- /**
5
- * 错误类型常量
6
- *
7
- * 错误分类:
8
- * - SIGNAL_ERROR: 信令异常(SDP、ICE 服务器)
9
- * - STATE_ERROR: 状态异常(播放器已关闭等)
10
- * - REQUEST_ERROR: 请求异常(400、406 等错误状态码)
11
- * - NOT_FOUND_ERROR: 资源未找到(404)
12
- *
13
- * 注意:流连接断开已由 `flow:stalled` 事件独立处理,不作为错误类型。
14
- */
15
- const ErrorTypes = {
16
- SIGNAL_ERROR: 'SignalError', // 信令异常
17
- STATE_ERROR: 'StateError', // 状态异常
18
- REQUEST_ERROR: 'RequestError', // 请求异常
19
- NOT_FOUND_ERROR: 'NotFoundError', // 没有找到
20
- VIDEO_ERROR: 'VideoError', // 播放容器错误
21
- OTHER_ERROR: 'OtherError', // 其他错误
22
- };
23
- /**
24
- * 错误
25
- */
26
- class WebRTCError extends Error {
27
- constructor(type, message, options) {
28
- super(message, options);
29
- this.type = type;
30
- }
31
- }
32
-
33
- /**
34
- * WebRTC utilities
35
- */
36
- class WebRtcUtils {
37
- /**
38
- * Check if the browser supports a non-advertised codec.
39
- */
40
- static async supportsNonAdvertisedCodec(codec, fmtp) {
41
- return new Promise((resolve) => {
42
- const pc = new RTCPeerConnection({ iceServers: [] });
43
- // 自动检测媒体类型:video 开头为视频,其他为音频
44
- const mediaType = codec.startsWith('video/') ? 'video' : 'audio';
45
- let payloadType = '';
46
- pc.addTransceiver(mediaType, { direction: 'recvonly' });
47
- pc.createOffer()
48
- .then((offer) => {
49
- if (!offer.sdp) {
50
- throw new Error('SDP not present');
51
- }
52
- if (offer.sdp.includes(` ${codec}`)) {
53
- // codec is advertised, there's no need to add it manually
54
- throw new Error('already present');
55
- }
56
- const sections = offer.sdp.split(`m=${mediaType}`);
57
- const payloadTypes = sections
58
- .slice(1)
59
- .map(s => s.split('\r\n')[0].split(' ').slice(3))
60
- .reduce((prev, cur) => [...prev, ...cur], []);
61
- payloadType = WebRtcUtils.reservePayloadType(payloadTypes);
62
- const lines = sections[1].split('\r\n');
63
- lines[0] += ` ${payloadType}`;
64
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} ${codec}`);
65
- if (fmtp !== undefined) {
66
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} ${fmtp}`);
67
- }
68
- sections[1] = lines.join('\r\n');
69
- offer.sdp = sections.join(`m=${mediaType}`);
70
- return pc.setLocalDescription(offer);
71
- })
72
- .then(() => pc.setRemoteDescription(new RTCSessionDescription({
73
- type: 'answer',
74
- sdp: `v=0\r\n`
75
- + `o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n`
76
- + `s=-\r\n`
77
- + `t=0 0\r\n`
78
- + `a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n`
79
- + `m=${mediaType} 9 UDP/TLS/RTP/SAVPF ${payloadType}\r\n`
80
- + `c=IN IP4 0.0.0.0\r\n`
81
- + `a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n`
82
- + `a=ice-ufrag:29e036dc\r\n`
83
- + `a=sendonly\r\n`
84
- + `a=rtcp-mux\r\n`
85
- + `a=rtpmap:${payloadType} ${codec}\r\n${fmtp !== undefined ? `a=fmtp:${payloadType} ${fmtp}\r\n` : ''}`,
86
- })))
87
- .then(() => resolve(true))
88
- .catch(() => resolve(false))
89
- .finally(() => pc.close());
90
- });
91
- }
92
- /**
93
- * Unquote a credential string.
94
- */
95
- static unquoteCredential(v) {
96
- return JSON.parse(`"${v}"`);
97
- }
98
- /**
99
- * Convert Link header to iceServers array.
100
- */
101
- static linkToIceServers(links) {
102
- if (links) {
103
- return links.split(', ').map((link) => {
104
- const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);
105
- if (!m) {
106
- throw new WebRTCError(ErrorTypes.SIGNAL_ERROR, 'Invalid ICE server link format');
107
- }
108
- const ret = {
109
- urls: [m[1]],
110
- };
111
- if (m[3]) {
112
- ret.username = WebRtcUtils.unquoteCredential(m[3]);
113
- ret.credential = WebRtcUtils.unquoteCredential(m[4]);
114
- ret.credentialType = 'password';
115
- }
116
- return ret;
117
- });
118
- }
119
- return [];
120
- }
121
- /**
122
- * Reserve a payload type.
123
- */
124
- static reservePayloadType(payloadTypes) {
125
- // everything is valid between 30 and 127, except for interval between 64 and 95
126
- // https://chromium.googlesource.com/external/webrtc/+/refs/heads/master/call/payload_type.h#29
127
- for (let i = 30; i <= 127; i++) {
128
- if ((i <= 63 || i >= 96) && !payloadTypes.includes(i.toString())) {
129
- const pl = i.toString();
130
- payloadTypes.push(pl);
131
- return pl;
132
- }
133
- }
134
- throw new Error('unable to find a free payload type');
135
- }
136
- }
137
-
138
- class CodecDetector {
139
- constructor(options) {
140
- this.options = options;
141
- }
142
- detect() {
143
- Promise.all([
144
- // 音频编解码器
145
- ['pcma/8000/2'],
146
- ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],
147
- ['L16/48000/2'],
148
- // 视频编解码器
149
- ['video/H265'],
150
- ['video/VP9'],
151
- ['video/AV1'],
152
- ].map(c => WebRtcUtils.supportsNonAdvertisedCodec(c[0], c[1]).then(r => (r ? c[0] : false))))
153
- .then(c => c.filter((e) => e !== false))
154
- .then((codecs) => {
155
- const currentState = this.options.getState();
156
- if (currentState !== 'getting_codecs')
157
- throw new WebRTCError(ErrorTypes.STATE_ERROR, `State changed to ${currentState}`);
158
- this.options.emitter.emit('codecs:detected', codecs);
159
- })
160
- .catch(err => this.options.emitter.emit('error', err));
161
- }
162
- }
163
-
164
- /**
165
- * SDP processing utilities
166
- */
167
- class SdpUtils {
168
- /**
169
- * Parse an offer SDP into iceUfrag, icePwd, and medias.
170
- */
171
- static parseOffer(sdp) {
172
- const ret = {
173
- iceUfrag: '',
174
- icePwd: '',
175
- medias: [],
176
- };
177
- for (const line of sdp.split('\r\n')) {
178
- if (line.startsWith('m=')) {
179
- ret.medias.push(line.slice('m='.length));
180
- }
181
- else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
182
- ret.iceUfrag = line.slice('a=ice-ufrag:'.length);
183
- }
184
- else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {
185
- ret.icePwd = line.slice('a=ice-pwd:'.length);
186
- }
187
- }
188
- return ret;
189
- }
190
- /**
191
- * Reserve a payload type.
192
- */
193
- static reservePayloadType(payloadTypes) {
194
- // everything is valid between 30 and 127, except for interval between 64 and 95
195
- // https://chromium.googlesource.com/external/webrtc/+/refs/heads/master/call/payload_type.h#29
196
- for (let i = 30; i <= 127; i++) {
197
- if ((i <= 63 || i >= 96) && !payloadTypes.includes(i.toString())) {
198
- const pl = i.toString();
199
- payloadTypes.push(pl);
200
- return pl;
201
- }
202
- }
203
- throw new Error('unable to find a free payload type');
204
- }
205
- /**
206
- * Enable stereo PCMA/PCMU codecs.
207
- */
208
- static enableStereoPcmau(payloadTypes, section) {
209
- const lines = section.split('\r\n');
210
- let payloadType = SdpUtils.reservePayloadType(payloadTypes);
211
- lines[0] += ` ${payloadType}`;
212
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMU/8000/2`);
213
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
214
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
215
- lines[0] += ` ${payloadType}`;
216
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMA/8000/2`);
217
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
218
- return lines.join('\r\n');
219
- }
220
- /**
221
- * Enable multichannel Opus codec.
222
- */
223
- static enableMultichannelOpus(payloadTypes, section) {
224
- const lines = section.split('\r\n');
225
- let payloadType = SdpUtils.reservePayloadType(payloadTypes);
226
- lines[0] += ` ${payloadType}`;
227
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/3`);
228
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`);
229
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
230
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
231
- lines[0] += ` ${payloadType}`;
232
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/4`);
233
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`);
234
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
235
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
236
- lines[0] += ` ${payloadType}`;
237
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/5`);
238
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`);
239
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
240
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
241
- lines[0] += ` ${payloadType}`;
242
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/6`);
243
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`);
244
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
245
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
246
- lines[0] += ` ${payloadType}`;
247
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/7`);
248
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`);
249
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
250
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
251
- lines[0] += ` ${payloadType}`;
252
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/8`);
253
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`);
254
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
255
- return lines.join('\r\n');
256
- }
257
- /**
258
- * Enable L16 codec.
259
- */
260
- static enableL16(payloadTypes, section) {
261
- const lines = section.split('\r\n');
262
- let payloadType = SdpUtils.reservePayloadType(payloadTypes);
263
- lines[0] += ` ${payloadType}`;
264
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/8000/2`);
265
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
266
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
267
- lines[0] += ` ${payloadType}`;
268
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/16000/2`);
269
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
270
- payloadType = SdpUtils.reservePayloadType(payloadTypes);
271
- lines[0] += ` ${payloadType}`;
272
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/48000/2`);
273
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
274
- return lines.join('\r\n');
275
- }
276
- /**
277
- * Enable H265 codec.
278
- */
279
- static enableH265(payloadTypes, section) {
280
- const lines = section.split('\r\n');
281
- const payloadType = SdpUtils.reservePayloadType(payloadTypes);
282
- lines[0] += ` ${payloadType}`;
283
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} H265/90000`);
284
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
285
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} profile-id=1`);
286
- return lines.join('\r\n');
287
- }
288
- /**
289
- * Enable VP9 codec.
290
- */
291
- static enableVP9(payloadTypes, section) {
292
- const lines = section.split('\r\n');
293
- const payloadType = SdpUtils.reservePayloadType(payloadTypes);
294
- lines[0] += ` ${payloadType}`;
295
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} VP9/90000`);
296
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
297
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} profile-id=0`);
298
- return lines.join('\r\n');
299
- }
300
- /**
301
- * Enable AV1 codec.
302
- */
303
- static enableAV1(payloadTypes, section) {
304
- const lines = section.split('\r\n');
305
- const payloadType = SdpUtils.reservePayloadType(payloadTypes);
306
- lines[0] += ` ${payloadType}`;
307
- lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} AV1/90000`);
308
- lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
309
- lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} profile-id=0`);
310
- return lines.join('\r\n');
311
- }
312
- /**
313
- * Enable stereo Opus codec.
314
- */
315
- static enableStereoOpus(section) {
316
- let opusPayloadFormat = '';
317
- const lines = section.split('\r\n');
318
- for (let i = 0; i < lines.length; i++) {
319
- if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) {
320
- opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0];
321
- break;
322
- }
323
- }
324
- if (opusPayloadFormat === '') {
325
- return section;
326
- }
327
- for (let i = 0; i < lines.length; i++) {
328
- if (lines[i].startsWith(`a=fmtp:${opusPayloadFormat} `)) {
329
- if (!lines[i].includes('stereo')) {
330
- lines[i] += ';stereo=1';
331
- }
332
- if (!lines[i].includes('sprop-stereo')) {
333
- lines[i] += ';sprop-stereo=1';
334
- }
335
- }
336
- }
337
- return lines.join('\r\n');
338
- }
339
- /**
340
- * Edit an offer SDP to enable non-advertised codecs.
341
- */
342
- static editOffer(sdp, nonAdvertisedCodecs) {
343
- const sections = sdp.split('m=');
344
- const payloadTypes = sections
345
- .slice(1)
346
- .map(s => s.split('\r\n')[0].split(' ').slice(3))
347
- .reduce((prev, cur) => [...prev, ...cur], []);
348
- for (let i = 1; i < sections.length; i++) {
349
- // 处理音频编解码器
350
- if (sections[i].startsWith('audio')) {
351
- sections[i] = SdpUtils.enableStereoOpus(sections[i]);
352
- if (nonAdvertisedCodecs.includes('pcma/8000/2')) {
353
- sections[i] = SdpUtils.enableStereoPcmau(payloadTypes, sections[i]);
354
- }
355
- if (nonAdvertisedCodecs.includes('multiopus/48000/6')) {
356
- sections[i] = SdpUtils.enableMultichannelOpus(payloadTypes, sections[i]);
357
- }
358
- if (nonAdvertisedCodecs.includes('L16/48000/2')) {
359
- sections[i] = SdpUtils.enableL16(payloadTypes, sections[i]);
360
- }
361
- }
362
- // 处理视频编解码器
363
- if (sections[i].startsWith('video')) {
364
- if (nonAdvertisedCodecs.includes('video/H265') || nonAdvertisedCodecs.includes('video/hevc')) {
365
- sections[i] = SdpUtils.enableH265(payloadTypes, sections[i]);
366
- }
367
- if (nonAdvertisedCodecs.includes('video/VP9') || nonAdvertisedCodecs.includes('video/vp9')) {
368
- sections[i] = SdpUtils.enableVP9(payloadTypes, sections[i]);
369
- }
370
- if (nonAdvertisedCodecs.includes('video/AV1') || nonAdvertisedCodecs.includes('video/av1')) {
371
- sections[i] = SdpUtils.enableAV1(payloadTypes, sections[i]);
372
- }
373
- }
374
- }
375
- return sections.join('m=');
376
- }
377
- /**
378
- * Generate an SDP fragment.
379
- */
380
- static generateSdpFragment(od, candidates) {
381
- const candidatesByMedia = {};
382
- for (const candidate of candidates) {
383
- const mid = candidate.sdpMLineIndex;
384
- if (mid) {
385
- if (candidatesByMedia[mid] === undefined) {
386
- candidatesByMedia[mid] = [];
387
- }
388
- candidatesByMedia[mid].push(candidate);
389
- }
390
- }
391
- let frag = `a=ice-ufrag:${od.iceUfrag}\r\n` + `a=ice-pwd:${od.icePwd}\r\n`;
392
- let mid = 0;
393
- for (const media of od.medias) {
394
- if (candidatesByMedia[mid] !== undefined) {
395
- frag += `m=${media}\r\n` + `a=mid:${mid}\r\n`;
396
- for (const candidate of candidatesByMedia[mid]) {
397
- frag += `a=${candidate.candidate}\r\n`;
398
- }
399
- }
400
- mid++;
401
- }
402
- return frag;
403
- }
404
- }
405
-
406
- /**
407
- * WebRTC 连接管理器
408
- *
409
- * 负责管理 RTCPeerConnection 的生命周期和事件处理。
410
- */
411
- class ConnectionManager {
412
- constructor(options) {
413
- this.options = options;
414
- /**
415
- * 处理本地 ICE 候选事件
416
- */
417
- this.handleIceCandidate = (evt) => {
418
- if (this.options.getState() !== 'running')
419
- return;
420
- if (evt.candidate)
421
- this.options.emitter.emit('candidate', evt.candidate);
422
- };
423
- /**
424
- * 处理连接状态变化
425
- *
426
- * connectionState 可能的值:
427
- * - 'new': 刚创建,还未连接
428
- * 'connecting': 正在连接中
429
- * 'connected': 已连接
430
- * - 'disconnected': 已断开(可能短暂)
431
- * - 'failed': 连接失败
432
- * - 'closed': 已关闭
433
- *
434
- * 连接失败或关闭属于流断开,触发 flow:stalled 事件进行恢复。
435
- */
436
- this.handleConnectionState = () => {
437
- if (this.options.getState() !== 'running' || !this.pc)
438
- return;
439
- const { connectionState } = this.pc;
440
- // 连接失败或关闭,触发流停滞事件
441
- if (connectionState === 'failed' || connectionState === 'closed') {
442
- this.options.emitter.emit('flow:stalled', {
443
- reason: `peer connection ${connectionState}`,
444
- });
445
- }
446
- };
447
- /**
448
- * 处理 ICE 连接状态变化
449
- *
450
- * iceConnectionState 可能的值:
451
- * - 'new': 刚创建,还未检查
452
- * - 'checking': 正在检查
453
- * 'connected': ICE 连接成功
454
- * 'completed': ICE 连接完成
455
- * 'failed': ICE 连接失败
456
- * 'disconnected': ICE 连接断开(可能短暂)
457
- */
458
- this.handleIceConnectionState = () => {
459
- if (this.options.getState() !== 'running' || !this.pc)
460
- return;
461
- const { iceConnectionState } = this.pc;
462
- // ICE 连接失败,尝试重启 ICE
463
- if (iceConnectionState === 'failed') {
464
- this.restartIceConnection();
465
- }
466
- };
467
- /**
468
- * 处理媒体轨道事件
469
- */
470
- this.handleTrack = (evt) => {
471
- this.options.emitter.emit('track', evt);
472
- };
473
- }
474
- /**
475
- * 创建并配置 PeerConnection
476
- */
477
- async setupPeerConnection(iceServers) {
478
- if (this.options.getState() !== 'running')
479
- throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed');
480
- const pc = new RTCPeerConnection({
481
- iceServers,
482
- sdpSemantics: 'unified-plan',
483
- });
484
- this.pc = pc;
485
- const direction = 'recvonly';
486
- pc.addTransceiver('video', { direction });
487
- pc.addTransceiver('audio', { direction });
488
- // 注册事件处理器(使用类方法引用,便于管理和清理)
489
- pc.onicecandidate = this.handleIceCandidate;
490
- pc.onconnectionstatechange = this.handleConnectionState;
491
- pc.oniceconnectionstatechange = this.handleIceConnectionState;
492
- pc.ontrack = this.handleTrack;
493
- return pc.createOffer().then((offer) => {
494
- if (!offer.sdp)
495
- throw new WebRTCError(ErrorTypes.SIGNAL_ERROR, 'Failed to create offer SDP');
496
- offer.sdp = SdpUtils.editOffer(offer.sdp, this.options.getNonAdvertisedCodecs());
497
- this.offerData = SdpUtils.parseOffer(offer.sdp);
498
- return pc.setLocalDescription(offer).then(() => offer.sdp);
499
- });
500
- }
501
- /**
502
- * 设置远程 SDP Answer
503
- */
504
- async setAnswer(answer) {
505
- if (this.options.getState() !== 'running')
506
- throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed');
507
- return this.pc.setRemoteDescription(new RTCSessionDescription({
508
- type: 'answer',
509
- sdp: answer,
510
- }));
511
- }
512
- getPeerConnection() {
513
- return this.pc;
514
- }
515
- getOfferData() {
516
- return this.offerData;
517
- }
518
- close() {
519
- this.pc?.close();
520
- this.pc = undefined;
521
- this.offerData = undefined;
522
- }
523
- /**
524
- * 重启 ICE 连接
525
- *
526
- * 当 ICE 连接失败时,尝试重启 ICE 以恢复连接
527
- */
528
- restartIceConnection() {
529
- if (!this.pc)
530
- return;
531
- try {
532
- this.pc.restartIce();
533
- }
534
- catch (error) {
535
- console.error('[ConnectionManager] ICE restart failed:', error);
536
- }
537
- }
538
- }
539
-
540
- class HttpClient {
541
- constructor(options) {
542
- this.options = options;
543
- }
544
- authHeader() {
545
- if (this.options.conf.user && this.options.conf.user !== '') {
546
- const credentials = btoa(`${this.options.conf.user}:${this.options.conf.pass}`);
547
- return { Authorization: `Basic ${credentials}` };
548
- }
549
- if (this.options.conf.token && this.options.conf.token !== '') {
550
- return { Authorization: `Bearer ${this.options.conf.token}` };
551
- }
552
- return {};
553
- }
554
- async requestICEServers() {
555
- if (this.options.conf.iceServers && this.options.conf.iceServers.length > 0)
556
- return this.options.conf.iceServers;
557
- return fetch(this.options.conf.url, {
558
- method: 'OPTIONS',
559
- headers: {
560
- ...this.authHeader(),
561
- },
562
- }).then(res => WebRtcUtils.linkToIceServers(res.headers.get('Link')));
563
- }
564
- async sendOffer(offer) {
565
- if (this.options.getState() !== 'running')
566
- throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed');
567
- return fetch(this.options.conf.url, {
568
- method: 'POST',
569
- headers: {
570
- ...this.authHeader(),
571
- 'Content-Type': 'application/sdp',
572
- },
573
- body: offer,
574
- }).then((res) => {
575
- switch (res.status) {
576
- case 201:
577
- break;
578
- case 404:
579
- case 406:
580
- throw new WebRTCError(ErrorTypes.NOT_FOUND_ERROR, 'stream not found');
581
- case 400:
582
- return res.json().then((e) => {
583
- throw new WebRTCError(ErrorTypes.REQUEST_ERROR, e.error);
584
- });
585
- default:
586
- throw new WebRTCError(ErrorTypes.REQUEST_ERROR, `bad status code ${res.status}`);
587
- }
588
- const location = res.headers.get('Location');
589
- const sessionUrl = location
590
- ? new URL(location, this.options.conf.url).toString()
591
- : undefined;
592
- return res.text().then(answer => ({ sessionUrl, answer }));
593
- });
594
- }
595
- sendLocalCandidates(sessionUrl, offerData, candidates) {
596
- fetch(sessionUrl, {
597
- method: 'PATCH',
598
- headers: {
599
- 'Content-Type': 'application/trickle-ice-sdpfrag',
600
- 'If-Match': '*',
601
- },
602
- body: SdpUtils.generateSdpFragment(offerData, candidates),
603
- })
604
- .then((res) => {
605
- switch (res.status) {
606
- case 204:
607
- break;
608
- case 404:
609
- throw new WebRTCError(ErrorTypes.NOT_FOUND_ERROR, 'stream not found');
610
- default:
611
- throw new WebRTCError(ErrorTypes.REQUEST_ERROR, `bad status code ${res.status}`);
612
- }
613
- })
614
- .catch(err => this.options.emitter.emit('error', err));
615
- }
616
- }
617
-
618
- /**
619
- * 任务优先级
620
- */
621
- var TaskPriority;
622
- (function (TaskPriority) {
623
- TaskPriority[TaskPriority["LOW"] = 0] = "LOW";
624
- TaskPriority[TaskPriority["NORMAL"] = 1] = "NORMAL";
625
- TaskPriority[TaskPriority["HIGH"] = 2] = "HIGH";
626
- TaskPriority[TaskPriority["CRITICAL"] = 3] = "CRITICAL";
627
- })(TaskPriority || (TaskPriority = {}));
628
- /**
629
- * 任务状态
630
- */
631
- var TaskStatus;
632
- (function (TaskStatus) {
633
- TaskStatus["SCHEDULED"] = "scheduled";
634
- TaskStatus["RUNNING"] = "running";
635
- TaskStatus["PAUSED"] = "paused";
636
- })(TaskStatus || (TaskStatus = {}));
637
- /**
638
- * 高性能监控调度器
639
- *
640
- * 轻量级设计,专为播放器内部少量监控任务优化。
641
- *
642
- * 核心特性:
643
- * - 优先级队列:高优先级任务优先执行
644
- * - 智能降频:检测性能压力,自动降低全局频率
645
- * - 防重复注册:相同 owner + interval 自动替换旧任务
646
- * - 暂停/恢复:支持按 owner 批量暂停任务
647
- */
648
- class MonitorScheduler {
649
- constructor(options = {}) {
650
- this.tasks = new Map();
651
- this.currentTick = 0;
652
- this.lastTickTime = 0;
653
- this.throttleLevel = 0; // 0=正常, 1=2x降频, 2=4x降频
654
- this.isRunning = false;
655
- this.baseInterval = options.baseInterval ?? 1000;
656
- this.maxTasksPerTick = options.maxTasksPerTick ?? 10;
657
- this.performanceThreshold = options.performanceThreshold ?? 16;
658
- this.enableAdaptiveThrottling = options.enableAdaptiveThrottling ?? true;
659
- }
660
- /**
661
- * 注册任务
662
- *
663
- * 如果已存在相同 owner 的任务,会自动替换。
664
- *
665
- * @returns 取消注册函数
666
- */
667
- register(options) {
668
- const { interval, owner, priority = TaskPriority.NORMAL, callback } = options;
669
- const taskId = `${owner}-${interval}`;
670
- const now = Date.now();
671
- // 创建或更新任务
672
- this.tasks.set(taskId, {
673
- id: taskId,
674
- interval,
675
- priority,
676
- status: TaskStatus.SCHEDULED,
677
- callback,
678
- lastRun: now - interval, // 立即执行一次
679
- nextRun: now,
680
- });
681
- // 启动调度器
682
- this.ensureStarted();
683
- // 返回取消函数
684
- return () => {
685
- this.tasks.delete(taskId);
686
- if (this.tasks.size === 0) {
687
- this.stop();
688
- }
689
- };
690
- }
691
- /**
692
- * 暂停指定 owner 的所有任务
693
- */
694
- pauseByOwner(owner) {
695
- for (const task of this.tasks.values()) {
696
- if (task.id.startsWith(owner) && task.status === TaskStatus.SCHEDULED) {
697
- task.status = TaskStatus.PAUSED;
698
- }
699
- }
700
- }
701
- /**
702
- * 恢复指定 owner 的所有任务
703
- */
704
- resumeByOwner(owner) {
705
- const now = Date.now();
706
- for (const task of this.tasks.values()) {
707
- if (task.id.startsWith(owner) && task.status === TaskStatus.PAUSED) {
708
- task.status = TaskStatus.SCHEDULED;
709
- task.nextRun = now + task.interval;
710
- }
711
- }
712
- }
713
- /**
714
- * 启动调度器
715
- */
716
- ensureStarted() {
717
- if (this.isRunning)
718
- return;
719
- this.isRunning = true;
720
- this.lastTickTime = Date.now();
721
- this.timer = setInterval(() => {
722
- this.tick();
723
- }, this.baseInterval);
724
- }
725
- /**
726
- * 停止调度器
727
- */
728
- stop() {
729
- if (this.timer) {
730
- clearInterval(this.timer);
731
- this.timer = undefined;
732
- }
733
- this.isRunning = false;
734
- this.throttleLevel = 0;
735
- }
736
- /**
737
- * 主调度循环
738
- */
739
- tick() {
740
- const now = Date.now();
741
- // 计算上一帧的执行时间
742
- const lastFrameDuration = this.lastTickTime > 0 ? now - this.lastTickTime : 0;
743
- this.lastTickTime = now;
744
- // 自适应降频
745
- if (this.enableAdaptiveThrottling) {
746
- this.updateThrottleLevel(lastFrameDuration);
747
- }
748
- // 获取需要执行的任务
749
- const tasksToRun = this.getTasksToRun(now);
750
- if (tasksToRun.length === 0)
751
- return;
752
- // 执行任务
753
- this.runTasks(tasksToRun, now);
754
- this.currentTick++;
755
- }
756
- /**
757
- * 更新降频级别
758
- */
759
- updateThrottleLevel(frameDuration) {
760
- if (frameDuration > this.performanceThreshold * 2) {
761
- // 严重超载,4x 降频
762
- this.throttleLevel = Math.min(this.throttleLevel + 1, 2);
763
- }
764
- else if (frameDuration > this.performanceThreshold) {
765
- // 轻微超载,2x 降频
766
- this.throttleLevel = Math.max(this.throttleLevel, 1);
767
- }
768
- else if (frameDuration < this.performanceThreshold * 0.5) {
769
- // 性能良好,降低降频级别
770
- this.throttleLevel = Math.max(this.throttleLevel - 1, 0);
771
- }
772
- }
773
- /**
774
- * 获取当前需要执行的任务
775
- *
776
- * 按优先级排序,并考虑降频级别
777
- */
778
- getTasksToRun(now) {
779
- const tasksToRun = [];
780
- const throttleMultiplier = 2 ** this.throttleLevel;
781
- for (const task of this.tasks.values()) {
782
- // 跳过暂停的任务
783
- if (task.status !== TaskStatus.SCHEDULED)
784
- continue;
785
- // 计算有效间隔(考虑降频)
786
- const effectiveInterval = task.interval * throttleMultiplier;
787
- if (now - task.lastRun >= effectiveInterval) {
788
- tasksToRun.push(task);
789
- }
790
- }
791
- // 按优先级排序(高优先级先执行)
792
- tasksToRun.sort((a, b) => b.priority - a.priority);
793
- return tasksToRun;
794
- }
795
- /**
796
- * 执行任务列表
797
- *
798
- * 限制每次执行的任务数量,避免阻塞
799
- */
800
- runTasks(tasks, now) {
801
- const maxTasks = Math.min(tasks.length, this.maxTasksPerTick);
802
- for (let i = 0; i < maxTasks; i++) {
803
- const task = tasks[i];
804
- this.runTask(task, now);
805
- }
806
- }
807
- /**
808
- * 执行单个任务
809
- */
810
- async runTask(task, now) {
811
- task.status = TaskStatus.RUNNING;
812
- try {
813
- await task.callback();
814
- task.lastRun = now;
815
- task.nextRun = now + task.interval;
816
- task.status = TaskStatus.SCHEDULED;
817
- }
818
- catch (error) {
819
- console.error(`[MonitorScheduler] Task ${task.id} error:`, error);
820
- // 错误任务继续调度,避免单次错误影响后续
821
- task.status = TaskStatus.SCHEDULED;
822
- }
823
- }
824
- /**
825
- * 清理所有任务
826
- */
827
- destroy() {
828
- this.stop();
829
- this.tasks.clear();
830
- this.currentTick = 0;
831
- }
832
- /**
833
- * 获取调度器状态(用于调试)
834
- */
835
- getStatus() {
836
- return {
837
- taskCount: this.tasks.size,
838
- throttleLevel: this.throttleLevel,
839
- currentTick: this.currentTick,
840
- };
841
- }
842
- }
843
-
844
- /**
845
- * 流量监控器
846
- *
847
- * 监控 WebRTC 视频流的数据传输状态,通过检测视频接收字节数是否增长来判定是否断流。
848
- *
849
- * 性能优化特性:
850
- * - 自适应轮询:初始阶段高频检查,稳定后降低频率
851
- * - 容错机制:连续多次无进度才判定断流,避免误判
852
- * - 持续监控:即使视频不可见,也保持监控以确保连接健康
853
- * - 专注视频:只监控视频流,不监控音频流
854
- */
855
- class FlowMonitor {
856
- constructor(options, scheduler) {
857
- this.options = options;
858
- this.lastBytesReceived = 0;
859
- this.isFirstCheck = true;
860
- // 状态跟踪
861
- this.consecutiveNoProgress = 0;
862
- this.startTime = 0;
863
- this.scheduler = scheduler;
864
- this.baseInterval = options.interval;
865
- this.stableInterval = options.stableInterval || (options.interval * 2);
866
- this.maxNoProgress = options.maxNoProgress || 3;
867
- this.stabilizationTime = options.stabilizationTime || 30000; // 30秒
868
- }
869
- /**
870
- * 启动断流检测
871
- */
872
- start(pc) {
873
- if (!pc) {
874
- return;
875
- }
876
- this.pc = pc;
877
- this.reset();
878
- this.startTime = Date.now();
879
- this.scheduleCheck();
880
- }
881
- /**
882
- * 停止断流检测并清理资源
883
- */
884
- stop() {
885
- if (this.unregisterMonitor) {
886
- this.unregisterMonitor();
887
- this.unregisterMonitor = undefined;
888
- }
889
- this.pc = undefined;
890
- }
891
- /**
892
- * 重置检测状态
893
- */
894
- reset() {
895
- this.consecutiveNoProgress = 0;
896
- this.isFirstCheck = true;
897
- this.lastBytesReceived = 0;
898
- }
899
- /**
900
- * 调度检查任务
901
- */
902
- scheduleCheck() {
903
- // 取消旧任务
904
- if (this.unregisterMonitor) {
905
- this.unregisterMonitor();
906
- }
907
- const interval = this.getNextCheckInterval();
908
- // 注册新任务(使用固定的类型名称作为 owner)
909
- this.unregisterMonitor = this.scheduler.register({
910
- interval,
911
- owner: 'flow-monitor', // 固定的类型名称
912
- priority: TaskPriority.HIGH,
913
- callback: () => this.performCheck(),
914
- });
915
- }
916
- /**
917
- * 执行流状态检查
918
- */
919
- performCheck() {
920
- if (!this.shouldCheck()) {
921
- return;
922
- }
923
- this.checkFlowState().catch(() => {
924
- // 忽略 getStats 错误,继续下次检查
925
- });
926
- // 重新调度(检查间隔可能变化)
927
- if (this.shouldContinueMonitoring()) {
928
- this.scheduleCheck();
929
- }
930
- }
931
- /**
932
- * 判断是否应该继续监控
933
- */
934
- shouldContinueMonitoring() {
935
- return !!(this.pc && this.pc.connectionState === 'connected');
936
- }
937
- /**
938
- * 判断是否应该进行检查
939
- */
940
- shouldCheck() {
941
- return !!(this.pc && this.pc.connectionState === 'connected');
942
- }
943
- /**
944
- * 计算下次检查间隔
945
- */
946
- getNextCheckInterval() {
947
- const elapsedTime = Date.now() - this.startTime;
948
- return elapsedTime > this.stabilizationTime ? this.stableInterval : this.baseInterval;
949
- }
950
- /**
951
- * 检测流状态(通过接收字节数判断是否断流)
952
- */
953
- async checkFlowState() {
954
- const currentBytes = await this.getReceivedBytes();
955
- // 首次检查时只记录初始值,不进行断流判定
956
- if (this.isFirstCheck) {
957
- this.lastBytesReceived = currentBytes;
958
- this.isFirstCheck = false;
959
- return;
960
- }
961
- // 检测是否有数据增长
962
- if (this.hasDataProgress(currentBytes)) {
963
- this.consecutiveNoProgress = 0;
964
- }
965
- else {
966
- this.handleNoProgress();
967
- }
968
- // 更新上一次字节数
969
- this.lastBytesReceived = currentBytes;
970
- }
971
- /**
972
- * 获取当前已接收的视频字节数
973
- *
974
- * 作为视频播放器,只监控视频流的数据传输状态。
975
- */
976
- async getReceivedBytes() {
977
- if (!this.pc) {
978
- return 0;
979
- }
980
- try {
981
- const stats = await this.pc.getStats();
982
- let videoBytes = 0;
983
- stats.forEach((stat) => {
984
- if (stat.type === 'inbound-rtp') {
985
- const inboundRtp = stat;
986
- // 只监控视频流
987
- if (inboundRtp.kind === 'video' && inboundRtp.bytesReceived !== undefined) {
988
- videoBytes = inboundRtp.bytesReceived;
989
- }
990
- }
991
- });
992
- return videoBytes;
993
- }
994
- catch {
995
- return 0;
996
- }
997
- }
998
- /**
999
- * 判断是否有数据增长
1000
- */
1001
- hasDataProgress(currentBytes) {
1002
- return currentBytes > this.lastBytesReceived;
1003
- }
1004
- /**
1005
- * 处理无进度情况
1006
- */
1007
- handleNoProgress() {
1008
- this.consecutiveNoProgress++;
1009
- if (this.consecutiveNoProgress >= this.maxNoProgress) {
1010
- this.triggerStreamError();
1011
- this.consecutiveNoProgress = 0;
1012
- }
1013
- }
1014
- /**
1015
- * 触发断流停滞事件
1016
- */
1017
- triggerStreamError() {
1018
- this.options.emitter.emit('flow:stalled', {
1019
- reason: 'Stream interrupted: video flow stalled',
1020
- });
1021
- }
1022
- }
1023
-
1024
- /**
1025
- * 播放监控器
1026
- *
1027
- * 处理媒体元素播放逻辑和错误监控,包括:
1028
- * - 自动播放、错误处理、播放验证和停滞检测
1029
- * - Video 元素崩溃检测和自动恢复
1030
- *
1031
- * 使用调度器进行定时检查,与其他监控器共享计时器资源。
1032
- */
1033
- class PlayMonitor {
1034
- constructor(options, scheduler) {
1035
- this.options = options;
1036
- this.lastCurrentTime = 0;
1037
- this.scheduler = scheduler;
1038
- // 设置 video 元素错误监听器(保存引用以便在 destroy 时移除)
1039
- this.container.addEventListener('error', this.handleVideoError = (evt) => {
1040
- this.onVideoError(evt);
1041
- });
1042
- }
1043
- /**
1044
- * 获取媒体容器
1045
- */
1046
- get container() {
1047
- return this.options.container;
1048
- }
1049
- /**
1050
- * 获取事件发射器
1051
- */
1052
- get emitter() {
1053
- return this.options.emitter;
1054
- }
1055
- /**
1056
- * 尝试播放媒体
1057
- */
1058
- async play() {
1059
- // 调试信息
1060
- const videoElement = this.container;
1061
- console.warn('[PlayMonitor] play() called:', {
1062
- paused: this.container.paused,
1063
- srcObject: !!this.container.srcObject,
1064
- muted: this.container.muted,
1065
- readyState: this.container.readyState,
1066
- videoWidth: videoElement.videoWidth,
1067
- videoHeight: videoElement.videoHeight,
1068
- currentTime: this.container.currentTime,
1069
- duration: this.container.duration,
1070
- });
1071
- // 如果已经在播放,跳过播放但仍需启动监控
1072
- if (!this.container.paused) {
1073
- console.warn('[PlayMonitor] Already playing, starting monitoring');
1074
- this.startMonitoring();
1075
- return;
1076
- }
1077
- try {
1078
- await this.container.play();
1079
- console.warn('[PlayMonitor] play() succeeded, checking video state...');
1080
- // 触发成功事件
1081
- this.emitter.emit('play:success', {
1082
- muted: this.container.muted,
1083
- });
1084
- // 延迟验证播放状态
1085
- setTimeout(() => this.verifyPlayback(), 500);
1086
- // 启动停滞监控
1087
- this.startMonitoring();
1088
- }
1089
- catch (error) {
1090
- console.warn('[PlayMonitor] play() failed, trying muted:', error);
1091
- // 如果未静音,尝试静音后重试
1092
- if (!this.container.muted) {
1093
- this.container.muted = true;
1094
- try {
1095
- await this.container.play();
1096
- console.warn('[PlayMonitor] play() with muted succeeded');
1097
- this.emitter.emit('play:success', {
1098
- muted: true,
1099
- });
1100
- // 延迟验证播放状态
1101
- setTimeout(() => this.verifyPlayback(), 500);
1102
- // 启动停滞监控
1103
- this.startMonitoring();
1104
- return;
1105
- }
1106
- catch {
1107
- // 静音重试也失败,继续处理错误
1108
- }
1109
- }
1110
- // 完全失败
1111
- this.emitter.emit('play:failed', {
1112
- reason: error instanceof Error ? error.message : 'Autoplay failed',
1113
- muted: this.container.muted,
1114
- });
1115
- }
1116
- }
1117
- /**
1118
- * 验证 play() 调用后播放是否真正开始。
1119
- */
1120
- verifyPlayback() {
1121
- // 检查视频是否真正在播放
1122
- if (this.container.paused) {
1123
- this.emitter.emit('play:stalled', {
1124
- reason: 'Playback verification failed: media is paused',
1125
- });
1126
- return;
1127
- }
1128
- // 检查 currentTime 是否在前进
1129
- if (this.container.currentTime === 0) {
1130
- this.emitter.emit('play:stalled', {
1131
- reason: 'Playback verification failed: currentTime is not advancing',
1132
- });
1133
- }
1134
- }
1135
- /**
1136
- * 启动播放停滞监控
1137
- */
1138
- startMonitoring() {
1139
- // 先停止已有的监控
1140
- this.stopMonitoring();
1141
- // 初始化 lastCurrentTime
1142
- this.lastCurrentTime = this.container.currentTime;
1143
- // 注册监控任务(使用固定的类型名称作为 owner)
1144
- this.unregisterMonitor = this.scheduler.register({
1145
- interval: 5000,
1146
- owner: 'play-monitor', // 固定的类型名称
1147
- priority: TaskPriority.NORMAL,
1148
- callback: () => this.checkStalled(),
1149
- });
1150
- }
1151
- /**
1152
- * 停止播放停滞监控
1153
- */
1154
- stopMonitoring() {
1155
- if (this.unregisterMonitor) {
1156
- this.unregisterMonitor();
1157
- this.unregisterMonitor = undefined;
1158
- }
1159
- }
1160
- /**
1161
- * 检查播放是否停滞
1162
- */
1163
- checkStalled() {
1164
- const currentTime = this.container.currentTime;
1165
- const timeAdvanced = currentTime - this.lastCurrentTime;
1166
- // 如果 5 秒内时间前进少于 0.1 秒,认为已停滞
1167
- if (timeAdvanced < 0.1 && !this.container.paused) {
1168
- this.emitter.emit('play:stalled', {
1169
- reason: `Playback stalled: currentTime not advancing (advanced ${timeAdvanced.toFixed(2)}s in 5s)`,
1170
- });
1171
- }
1172
- this.lastCurrentTime = currentTime;
1173
- }
1174
- /**
1175
- * 处理 video 元素错误
1176
- *
1177
- * 常见原因:
1178
- * - 解码错误(MEDIA_ERR_DECODE)
1179
- * - 网络错误(MEDIA_ERR_NETWORK)
1180
- * - 源不支持(MEDIA_ERR_SRC_NOT_SUPPORTED)
1181
- */
1182
- onVideoError(evt) {
1183
- const video = evt.target;
1184
- const errorCode = video.error?.code;
1185
- const errorMessage = video.error?.message;
1186
- console.error('[PlayMonitor] Video element error:', video.error);
1187
- // 触发错误事件
1188
- this.emitter.emit('error', new WebRTCError(ErrorTypes.VIDEO_ERROR, `Video element error (code: ${errorCode}): ${errorMessage}`));
1189
- }
1190
- /**
1191
- * 移除错误监听器
1192
- */
1193
- destroy() {
1194
- this.stopMonitoring();
1195
- if (this.handleVideoError) {
1196
- this.container.removeEventListener('error', this.handleVideoError);
1197
- this.handleVideoError = undefined;
1198
- }
1199
- }
1200
- }
1201
-
1202
- class TrackManager {
1203
- constructor(container, eventEmitter, lazyLoad = true, scheduler) {
1204
- this.container = container;
1205
- this.eventEmitter = eventEmitter;
1206
- this.lazyLoad = lazyLoad;
1207
- this.showStore = atom(false);
1208
- this.playMonitor = new PlayMonitor({
1209
- container,
1210
- emitter: this.eventEmitter,
1211
- }, scheduler);
1212
- // 创建流量监控器
1213
- this.flowMonitor = new FlowMonitor({
1214
- interval: 5000,
1215
- emitter: this.eventEmitter,
1216
- }, scheduler);
1217
- // 监听显示状态
1218
- this.showStore.subscribe((show) => {
1219
- if (show)
1220
- this.resume();
1221
- else
1222
- this.pause();
1223
- });
1224
- // 创建新的可见性观察器,自动处理暂停/恢复
1225
- if (this.lazyLoad) {
1226
- this.observer = new IntersectionObserver(([entry]) => {
1227
- if (entry.isIntersecting)
1228
- this.showStore.set(true);
1229
- else
1230
- this.showStore.set(false);
1231
- }, { threshold: 0.5 });
1232
- this.observer.observe(this.container);
1233
- }
1234
- else {
1235
- // 如果不启用懒加载,默认设置为显示状态
1236
- this.showStore.set(true);
1237
- }
1238
- }
1239
- onTrack(evt, pc) {
1240
- this.stream = evt.streams[0];
1241
- // 检查流中是否包含视频轨道
1242
- const hasVideo = this.stream.getTracks().some(t => t.kind === 'video');
1243
- if (!hasVideo) {
1244
- const error = new WebRTCError(ErrorTypes.OTHER_ERROR, 'No video track found in stream. Only audio streams are not supported.');
1245
- this.eventEmitter.emit('error', error);
1246
- return;
1247
- }
1248
- if (this.showStore.get()) {
1249
- this.container.srcObject = this.stream;
1250
- this.playMonitor.play();
1251
- }
1252
- // 自动启动流量监控
1253
- this.flowMonitor.start(pc);
1254
- }
1255
- get paused() {
1256
- return this.container.srcObject === null;
1257
- }
1258
- pause() {
1259
- // 暂停播放监控
1260
- this.playMonitor.stopMonitoring();
1261
- // 清除媒体源(停止渲染)
1262
- this.container.srcObject = null;
1263
- }
1264
- resume() {
1265
- if (this.stream) {
1266
- this.container.srcObject = this.stream;
1267
- this.playMonitor.play();
1268
- }
1269
- }
1270
- stop() {
1271
- this.playMonitor.stopMonitoring();
1272
- this.flowMonitor.stop();
1273
- this.stream = undefined;
1274
- }
1275
- /**
1276
- * 永久销毁(仅在 WebRTCWhep.close() 时调用)
1277
- */
1278
- destroy() {
1279
- this.playMonitor.destroy();
1280
- this.flowMonitor.stop();
1281
- this.stream = undefined;
1282
- // 清理观察器
1283
- if (this.observer) {
1284
- this.observer.disconnect();
1285
- this.observer = undefined;
1286
- }
1287
- }
1288
- }
1289
-
1290
- /** WebRTC/WHEP reader. */
1291
- class WebRTCWhep extends EventEmitter {
1292
- constructor(conf) {
1293
- super();
1294
- this.retryPause = 2000;
1295
- this.stateStore = atom('getting_codecs');
1296
- this.queuedCandidates = [];
1297
- this.nonAdvertisedCodecs = [];
1298
- this.conf = conf;
1299
- // Listen to state changes and emit events
1300
- this.stateStore.subscribe((current, previous) => {
1301
- console.warn(`State: ${previous} → ${current}`);
1302
- this.emit('state:change', { from: previous, to: current });
1303
- });
1304
- // 创建调度器实例
1305
- this.scheduler = new MonitorScheduler();
1306
- this.trackManager = new TrackManager(this.conf.container, this, this.conf.lazyLoad, this.scheduler);
1307
- this.httpClient = new HttpClient({
1308
- conf: this.conf,
1309
- getState: () => this.stateStore.get(),
1310
- emitter: this,
1311
- });
1312
- this.connectionManager = new ConnectionManager({
1313
- getState: () => this.stateStore.get(),
1314
- emitter: this,
1315
- getNonAdvertisedCodecs: () => this.nonAdvertisedCodecs,
1316
- });
1317
- this.codecDetector = new CodecDetector({
1318
- getState: () => this.stateStore.get(),
1319
- emitter: this,
1320
- });
1321
- // Listen to codec detection events
1322
- this.on('codecs:detected', (codecs) => this.handleCodecsDetected(codecs));
1323
- // Listen to connection events
1324
- this.on('candidate', (candidate) => this.handleCandidate(candidate));
1325
- this.on('track', (evt) => this.trackManager.onTrack(evt, this.connectionManager.getPeerConnection()));
1326
- // 监听停滞事件,并尝试恢复
1327
- this.on('play:stalled', payload => this.handlePlayStalled(payload));
1328
- this.on('flow:stalled', payload => this.handleFlowStalled(payload));
1329
- // 监听异常,并尝试处理
1330
- this.on('error', err => this.handleError(err));
1331
- this.codecDetector.detect();
1332
- }
1333
- get state() {
1334
- return this.stateStore.get();
1335
- }
1336
- close() {
1337
- this.stateStore.set('closed');
1338
- this.connectionManager.close();
1339
- this.trackManager.destroy(); // 永久销毁
1340
- this.scheduler.destroy(); // 销毁调度器
1341
- if (this.restartTimeout) {
1342
- clearTimeout(this.restartTimeout);
1343
- }
1344
- this.emit('close');
1345
- }
1346
- cleanupSession() {
1347
- if (this.restartTimeout) {
1348
- clearTimeout(this.restartTimeout);
1349
- this.restartTimeout = undefined;
1350
- }
1351
- this.connectionManager.close();
1352
- this.trackManager.stop();
1353
- this.queuedCandidates = [];
1354
- if (this.sessionUrl) {
1355
- fetch(this.sessionUrl, {
1356
- method: 'DELETE',
1357
- }).catch(() => { }); // Ignore deletion errors
1358
- this.sessionUrl = undefined;
1359
- }
1360
- }
1361
- handleError(err) {
1362
- // 永久性错误:信号、请求、状态错误 → failed
1363
- if (err instanceof WebRTCError) {
1364
- this.cleanupSession();
1365
- this.stateStore.set('failed');
1366
- }
1367
- // 初始化阶段的其他错误 → failed
1368
- else if (this.stateStore.get() === 'getting_codecs') {
1369
- this.cleanupSession();
1370
- this.stateStore.set('failed');
1371
- }
1372
- // 运行时的其他错误 → 尝试重启
1373
- else if (this.stateStore.get() === 'running') {
1374
- // 如果是容器错误,尝试重置容器
1375
- if (err instanceof WebRTCError && err.type === ErrorTypes.VIDEO_ERROR) {
1376
- this.handlePlayStalled({ reason: err.message });
1377
- return;
1378
- }
1379
- this.cleanupSession();
1380
- this.stateStore.set('restarting');
1381
- this.emit('restart');
1382
- this.restartTimeout = setTimeout(() => {
1383
- this.restartTimeout = undefined;
1384
- this.stateStore.set('running');
1385
- this.start();
1386
- }, this.retryPause);
1387
- }
1388
- }
1389
- /**
1390
- * 处理播放停滞事件
1391
- *
1392
- * 尝试重新播放,如果失败则触发错误
1393
- */
1394
- handlePlayStalled(payload) {
1395
- console.warn('[PlayStalled]', payload.reason);
1396
- this.pause();
1397
- this.conf.container.load();
1398
- setTimeout(() => this.resume(), 100);
1399
- }
1400
- /**
1401
- * 处理流量停滞事件
1402
- *
1403
- * 清理会话并重新连接,类似于网络错误的处理
1404
- */
1405
- handleFlowStalled(payload) {
1406
- console.warn('[FlowStalled]', payload.reason);
1407
- // 流停滞,清理会话并重新连接
1408
- if (this.stateStore.get() === 'running') {
1409
- this.cleanupSession();
1410
- this.stateStore.set('restarting');
1411
- this.emit('restart');
1412
- this.restartTimeout = setTimeout(() => {
1413
- this.restartTimeout = undefined;
1414
- this.stateStore.set('running');
1415
- this.start();
1416
- }, this.retryPause);
1417
- }
1418
- }
1419
- handleCodecsDetected(codecs) {
1420
- this.nonAdvertisedCodecs = codecs;
1421
- this.stateStore.set('running');
1422
- this.start();
1423
- }
1424
- start() {
1425
- this.httpClient.requestICEServers()
1426
- .then(iceServers => this.connectionManager.setupPeerConnection(iceServers))
1427
- .then(offer => this.httpClient.sendOffer(offer))
1428
- .then(({ sessionUrl, answer }) => this.handleOfferResponse(sessionUrl, answer))
1429
- .catch(err => this.handleError(err));
1430
- }
1431
- handleOfferResponse(sessionUrl, answer) {
1432
- if (sessionUrl)
1433
- this.sessionUrl = sessionUrl;
1434
- return this.connectionManager.setAnswer(answer).then(() => {
1435
- if (this.stateStore.get() !== 'running')
1436
- return;
1437
- if (this.queuedCandidates.length !== 0) {
1438
- const offerData = this.connectionManager.getOfferData();
1439
- if (offerData && this.sessionUrl) {
1440
- this.httpClient.sendLocalCandidates(this.sessionUrl, offerData, this.queuedCandidates);
1441
- this.queuedCandidates = [];
1442
- }
1443
- }
1444
- });
1445
- }
1446
- handleCandidate(candidate) {
1447
- if (this.sessionUrl) {
1448
- const offerData = this.connectionManager.getOfferData();
1449
- if (offerData) {
1450
- this.httpClient.sendLocalCandidates(this.sessionUrl, offerData, [candidate]);
1451
- }
1452
- }
1453
- else {
1454
- this.queuedCandidates.push(candidate);
1455
- }
1456
- }
1457
- get paused() {
1458
- return this.trackManager.paused;
1459
- }
1460
- pause() {
1461
- this.trackManager.pause();
1462
- }
1463
- resume() {
1464
- this.trackManager.resume();
1465
- }
1466
- /**
1467
- * Update the WHEP endpoint URL and restart playback.
1468
- * Useful when the current URL fails and you need to switch to a new URL.
1469
- *
1470
- * @param url - The new WHEP endpoint URL
1471
- *
1472
- * @example
1473
- * ```ts
1474
- * player.on('error', () => {
1475
- * // Get new URL from your server
1476
- * const newUrl = await getNewStreamUrl()
1477
- * player.updateUrl(newUrl)
1478
- * })
1479
- * ```
1480
- */
1481
- updateUrl(url) {
1482
- // Cannot update URL if already closed
1483
- if (this.state === 'closed') {
1484
- this.emit('error', new WebRTCError(ErrorTypes.STATE_ERROR, 'Cannot update URL: instance is closed'));
1485
- return;
1486
- }
1487
- // Update the URL
1488
- this.conf.url = url;
1489
- // Cleanup existing session
1490
- this.cleanupSession();
1491
- // Reset to running state and start with new URL
1492
- this.stateStore.set('running');
1493
- this.start();
1494
- }
1495
- }
1496
-
1497
- export { ErrorTypes, WebRTCError, WebRTCWhep as default };
1498
- //# sourceMappingURL=index.js.map
1
+ import t from"eventemitter3";import{atom as e}from"nanostores";const s={SIGNAL_ERROR:"SignalError",STATE_ERROR:"StateError",REQUEST_ERROR:"RequestError",NOT_FOUND_ERROR:"NotFoundError",VIDEO_ERROR:"VideoError",OTHER_ERROR:"OtherError"};class i extends Error{constructor(t,e,s){super(e,s),this.type=t}}class r{static async supportsNonAdvertisedCodec(t,e){return new Promise(s=>{const i=new RTCPeerConnection({iceServers:[]}),n=t.startsWith("video/")?"video":"audio";let o="";i.addTransceiver(n,{direction:"recvonly"}),i.createOffer().then(s=>{if(!s.sdp)throw new Error("SDP not present");if(s.sdp.includes(` ${t}`))throw new Error("already present");const a=s.sdp.split(`m=${n}`),c=a.slice(1).map(t=>t.split("\r\n")[0].split(" ").slice(3)).reduce((t,e)=>[...t,...e],[]);o=r.reservePayloadType(c);const h=a[1].split("\r\n");return h[0]+=` ${o}`,h.splice(h.length-1,0,`a=rtpmap:${o} ${t}`),void 0!==e&&h.splice(h.length-1,0,`a=fmtp:${o} ${e}`),a[1]=h.join("\r\n"),s.sdp=a.join(`m=${n}`),i.setLocalDescription(s)}).then(()=>i.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:`v=0\r\no=- 6539324223450680508 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\nm=${n} 9 UDP/TLS/RTP/SAVPF ${o}\r\nc=IN IP4 0.0.0.0\r\na=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\na=ice-ufrag:29e036dc\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:${o} ${t}\r\n${void 0!==e?`a=fmtp:${o} ${e}\r\n`:""}`}))).then(()=>s(!0)).catch(()=>s(!1)).finally(()=>i.close())})}static unquoteCredential(t){return JSON.parse(`"${t}"`)}static linkToIceServers(t){return t?t.split(", ").map(t=>{const e=t.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!e)throw new i(s.SIGNAL_ERROR,"Invalid ICE server link format");const n={urls:[e[1]]};return e[3]&&(n.username=r.unquoteCredential(e[3]),n.credential=r.unquoteCredential(e[4]),n.credentialType="password"),n}):[]}static reservePayloadType(t){for(let e=30;e<=127;e++)if((e<=63||e>=96)&&!t.includes(e.toString())){const s=e.toString();return t.push(s),s}throw new Error("unable to find a free payload type")}}class n{constructor(t){this.options=t}detect(){Promise.all([["pcma/8000/2"],["multiopus/48000/6","channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"],["L16/48000/2"],["video/H265"],["video/VP9"],["video/AV1"]].map(t=>r.supportsNonAdvertisedCodec(t[0],t[1]).then(e=>!!e&&t[0]))).then(t=>t.filter(t=>!1!==t)).then(t=>{const e=this.options.getState();if("getting_codecs"!==e)throw new i(s.STATE_ERROR,`State changed to ${e}`);this.options.emitter.emit("codecs:detected",t)}).catch(t=>this.options.emitter.emit("error",t))}}class o{static parseOffer(t){const e={iceUfrag:"",icePwd:"",medias:[]};for(const s of t.split("\r\n"))s.startsWith("m=")?e.medias.push(s.slice(2)):""===e.iceUfrag&&s.startsWith("a=ice-ufrag:")?e.iceUfrag=s.slice(12):""===e.icePwd&&s.startsWith("a=ice-pwd:")&&(e.icePwd=s.slice(10));return e}static reservePayloadType(t){for(let e=30;e<=127;e++)if((e<=63||e>=96)&&!t.includes(e.toString())){const s=e.toString();return t.push(s),s}throw new Error("unable to find a free payload type")}static enableStereoPcmau(t,e){const s=e.split("\r\n");let i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} PCMU/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} PCMA/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.join("\r\n")}static enableMultichannelOpus(t,e){const s=e.split("\r\n");let i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/3`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/4`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/5`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/6`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/7`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/8`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.join("\r\n")}static enableL16(t,e){const s=e.split("\r\n");let i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} L16/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} L16/16000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} L16/48000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.join("\r\n")}static enableH265(t,e){const s=e.split("\r\n"),i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} H265/90000`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.splice(s.length-1,0,`a=fmtp:${i} profile-id=1`),s.join("\r\n")}static enableVP9(t,e){const s=e.split("\r\n"),i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} VP9/90000`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.splice(s.length-1,0,`a=fmtp:${i} profile-id=0`),s.join("\r\n")}static enableAV1(t,e){const s=e.split("\r\n"),i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} AV1/90000`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.splice(s.length-1,0,`a=fmtp:${i} profile-id=0`),s.join("\r\n")}static enableStereoOpus(t){let e="";const s=t.split("\r\n");for(let t=0;t<s.length;t++)if(s[t].startsWith("a=rtpmap:")&&s[t].toLowerCase().includes("opus/")){e=s[t].slice(9).split(" ")[0];break}if(""===e)return t;for(let t=0;t<s.length;t++)s[t].startsWith(`a=fmtp:${e} `)&&(s[t].includes("stereo")||(s[t]+=";stereo=1"),s[t].includes("sprop-stereo")||(s[t]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(t,e){const s=t.split("m="),i=s.slice(1).map(t=>t.split("\r\n")[0].split(" ").slice(3)).reduce((t,e)=>[...t,...e],[]);for(let t=1;t<s.length;t++)s[t].startsWith("audio")&&(s[t]=o.enableStereoOpus(s[t]),e.includes("pcma/8000/2")&&(s[t]=o.enableStereoPcmau(i,s[t])),e.includes("multiopus/48000/6")&&(s[t]=o.enableMultichannelOpus(i,s[t])),e.includes("L16/48000/2")&&(s[t]=o.enableL16(i,s[t]))),s[t].startsWith("video")&&((e.includes("video/H265")||e.includes("video/hevc"))&&(s[t]=o.enableH265(i,s[t])),(e.includes("video/VP9")||e.includes("video/vp9"))&&(s[t]=o.enableVP9(i,s[t])),(e.includes("video/AV1")||e.includes("video/av1"))&&(s[t]=o.enableAV1(i,s[t])));return s.join("m=")}static generateSdpFragment(t,e){const s={};for(const t of e){const e=t.sdpMLineIndex;e&&(void 0===s[e]&&(s[e]=[]),s[e].push(t))}let i=`a=ice-ufrag:${t.iceUfrag}\r\na=ice-pwd:${t.icePwd}\r\n`,r=0;for(const e of t.medias){if(void 0!==s[r]){i+=`m=${e}\r\na=mid:${r}\r\n`;for(const t of s[r])i+=`a=${t.candidate}\r\n`}r++}return i}}class a{constructor(t){this.options=t,this.handleIceCandidate=t=>{"running"===this.options.getState()&&t.candidate&&this.options.emitter.emit("candidate",t.candidate)},this.handleConnectionState=()=>{if("running"!==this.options.getState()||!this.pc)return;const{connectionState:t}=this.pc;"failed"!==t&&"closed"!==t||this.options.emitter.emit("flow:stalled",{reason:`peer connection ${t}`})},this.handleIceConnectionState=()=>{if("running"!==this.options.getState()||!this.pc)return;const{iceConnectionState:t}=this.pc;"failed"===t&&this.restartIceConnection()},this.handleTrack=t=>{this.options.emitter.emit("track",t)}}async setupPeerConnection(t){if("running"!==this.options.getState())throw new i(s.STATE_ERROR,"closed");const e=new RTCPeerConnection({iceServers:t,sdpSemantics:"unified-plan"});this.pc=e;const r="recvonly";return e.addTransceiver("video",{direction:r}),e.addTransceiver("audio",{direction:r}),e.onicecandidate=this.handleIceCandidate,e.onconnectionstatechange=this.handleConnectionState,e.oniceconnectionstatechange=this.handleIceConnectionState,e.ontrack=this.handleTrack,e.createOffer().then(t=>{if(!t.sdp)throw new i(s.SIGNAL_ERROR,"Failed to create offer SDP");return t.sdp=o.editOffer(t.sdp,this.options.getNonAdvertisedCodecs()),this.offerData=o.parseOffer(t.sdp),e.setLocalDescription(t).then(()=>t.sdp)})}async setAnswer(t){if("running"!==this.options.getState())throw new i(s.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:t}))}getPeerConnection(){return this.pc}getOfferData(){return this.offerData}close(){this.pc?.close(),this.pc=void 0,this.offerData=void 0}restartIceConnection(){if(this.pc)try{this.pc.restartIce()}catch(t){}}}class c{constructor(t){this.options=t}authHeader(){if(this.options.conf.user&&""!==this.options.conf.user){return{Authorization:`Basic ${btoa(`${this.options.conf.user}:${this.options.conf.pass}`)}`}}return this.options.conf.token&&""!==this.options.conf.token?{Authorization:`Bearer ${this.options.conf.token}`}:{}}async requestICEServers(){return this.options.conf.iceServers&&this.options.conf.iceServers.length>0?this.options.conf.iceServers:fetch(this.options.conf.url,{method:"OPTIONS",headers:{...this.authHeader()}}).then(t=>r.linkToIceServers(t.headers.get("Link")))}async sendOffer(t){if("running"!==this.options.getState())throw new i(s.STATE_ERROR,"closed");return fetch(this.options.conf.url,{method:"POST",headers:{...this.authHeader(),"Content-Type":"application/sdp"},body:t}).then(t=>{switch(t.status){case 201:break;case 404:case 406:throw new i(s.NOT_FOUND_ERROR,"stream not found");case 400:return t.json().then(t=>{throw new i(s.REQUEST_ERROR,t.error)});default:throw new i(s.REQUEST_ERROR,`bad status code ${t.status}`)}const e=t.headers.get("Location"),r=e?new URL(e,this.options.conf.url).toString():void 0;return t.text().then(t=>({sessionUrl:r,answer:t}))})}sendLocalCandidates(t,e,r){fetch(t,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:o.generateSdpFragment(e,r)}).then(t=>{switch(t.status){case 204:break;case 404:throw new i(s.NOT_FOUND_ERROR,"stream not found");default:throw new i(s.REQUEST_ERROR,`bad status code ${t.status}`)}}).catch(t=>this.options.emitter.emit("error",t))}}var h,l;!function(t){t[t.LOW=0]="LOW",t[t.NORMAL=1]="NORMAL",t[t.HIGH=2]="HIGH",t[t.CRITICAL=3]="CRITICAL"}(h||(h={})),function(t){t.SCHEDULED="scheduled",t.RUNNING="running",t.PAUSED="paused"}(l||(l={}));class d{constructor(t={}){this.tasks=new Map,this.currentTick=0,this.lastTickTime=0,this.throttleLevel=0,this.isRunning=!1,this.baseInterval=t.baseInterval??1e3,this.maxTasksPerTick=t.maxTasksPerTick??10,this.performanceThreshold=t.performanceThreshold??16,this.enableAdaptiveThrottling=t.enableAdaptiveThrottling??!0}register(t){const{interval:e,owner:s,priority:i=h.NORMAL,callback:r}=t,n=`${s}-${e}`,o=Date.now();return this.tasks.set(n,{id:n,interval:e,priority:i,status:l.SCHEDULED,callback:r,lastRun:o-e,nextRun:o}),this.ensureStarted(),()=>{this.tasks.delete(n),0===this.tasks.size&&this.stop()}}pauseByOwner(t){for(const e of this.tasks.values())e.id.startsWith(t)&&e.status===l.SCHEDULED&&(e.status=l.PAUSED)}resumeByOwner(t){const e=Date.now();for(const s of this.tasks.values())s.id.startsWith(t)&&s.status===l.PAUSED&&(s.status=l.SCHEDULED,s.nextRun=e+s.interval)}ensureStarted(){this.isRunning||(this.isRunning=!0,this.lastTickTime=Date.now(),this.timer=setInterval(()=>{this.tick()},this.baseInterval))}stop(){this.timer&&(clearInterval(this.timer),this.timer=void 0),this.isRunning=!1,this.throttleLevel=0}tick(){const t=Date.now(),e=this.lastTickTime>0?t-this.lastTickTime:0;this.lastTickTime=t,this.enableAdaptiveThrottling&&this.updateThrottleLevel(e);const s=this.getTasksToRun(t);0!==s.length&&(this.runTasks(s,t),this.currentTick++)}updateThrottleLevel(t){t>2*this.performanceThreshold?this.throttleLevel=Math.min(this.throttleLevel+1,2):t>this.performanceThreshold?this.throttleLevel=Math.max(this.throttleLevel,1):t<.5*this.performanceThreshold&&(this.throttleLevel=Math.max(this.throttleLevel-1,0))}getTasksToRun(t){const e=[],s=2**this.throttleLevel;for(const i of this.tasks.values()){if(i.status!==l.SCHEDULED)continue;const r=i.interval*s;t-i.lastRun>=r&&e.push(i)}return e.sort((t,e)=>e.priority-t.priority),e}runTasks(t,e){const s=Math.min(t.length,this.maxTasksPerTick);for(let i=0;i<s;i++){const s=t[i];this.runTask(s,e)}}async runTask(t,e){t.status=l.RUNNING;try{await t.callback(),t.lastRun=e,t.nextRun=e+t.interval,t.status=l.SCHEDULED}catch(e){t.status=l.SCHEDULED}}destroy(){this.stop(),this.tasks.clear(),this.currentTick=0}getStatus(){return{taskCount:this.tasks.size,throttleLevel:this.throttleLevel,currentTick:this.currentTick}}}class p{constructor(t,e){this.options=t,this.lastBytesReceived=0,this.isFirstCheck=!0,this.consecutiveNoProgress=0,this.startTime=0,this.scheduler=e,this.baseInterval=t.interval,this.stableInterval=t.stableInterval||2*t.interval,this.maxNoProgress=t.maxNoProgress||3,this.stabilizationTime=t.stabilizationTime||3e4}start(t){t&&(this.pc=t,this.reset(),this.startTime=Date.now(),this.scheduleCheck())}stop(){this.unregisterMonitor&&(this.unregisterMonitor(),this.unregisterMonitor=void 0),this.pc=void 0}reset(){this.consecutiveNoProgress=0,this.isFirstCheck=!0,this.lastBytesReceived=0}scheduleCheck(){this.unregisterMonitor&&this.unregisterMonitor();const t=this.getNextCheckInterval();this.unregisterMonitor=this.scheduler.register({interval:t,owner:"flow-monitor",priority:h.HIGH,callback:()=>this.performCheck()})}performCheck(){this.shouldCheck()&&(this.checkFlowState().catch(()=>{}),this.shouldContinueMonitoring()&&this.scheduleCheck())}shouldContinueMonitoring(){return!(!this.pc||"connected"!==this.pc.connectionState)}shouldCheck(){return!(!this.pc||"connected"!==this.pc.connectionState)}getNextCheckInterval(){return Date.now()-this.startTime>this.stabilizationTime?this.stableInterval:this.baseInterval}async checkFlowState(){const t=await this.getReceivedBytes();if(this.isFirstCheck)return this.lastBytesReceived=t,void(this.isFirstCheck=!1);this.hasDataProgress(t)?this.consecutiveNoProgress=0:this.handleNoProgress(),this.lastBytesReceived=t}async getReceivedBytes(){if(!this.pc)return 0;try{const t=await this.pc.getStats();let e=0;return t.forEach(t=>{if("inbound-rtp"===t.type){const s=t;"video"===s.kind&&void 0!==s.bytesReceived&&(e=s.bytesReceived)}}),e}catch{return 0}}hasDataProgress(t){return t>this.lastBytesReceived}handleNoProgress(){this.consecutiveNoProgress++,this.consecutiveNoProgress>=this.maxNoProgress&&(this.triggerStreamError(),this.consecutiveNoProgress=0)}triggerStreamError(){this.options.emitter.emit("flow:stalled",{reason:"Stream interrupted: video flow stalled"})}}class u{constructor(t,e){this.options=t,this.lastCurrentTime=0,this.scheduler=e,this.container.addEventListener("error",this.handleVideoError=t=>{this.onVideoError(t)})}get container(){return this.options.container}get emitter(){return this.options.emitter}async play(){if(this.container.paused)try{await this.container.play(),this.emitter.emit("play:success",{muted:this.container.muted}),setTimeout(()=>this.verifyPlayback(),500),this.startMonitoring()}catch(t){if(!this.container.muted){this.container.muted=!0;try{return await this.container.play(),this.emitter.emit("play:success",{muted:!0}),setTimeout(()=>this.verifyPlayback(),500),void this.startMonitoring()}catch{}}this.emitter.emit("play:failed",{reason:t instanceof Error?t.message:"Autoplay failed",muted:this.container.muted})}else this.startMonitoring()}verifyPlayback(){this.container.paused?this.emitter.emit("play:stalled",{reason:"Playback verification failed: media is paused"}):0===this.container.currentTime&&this.emitter.emit("play:stalled",{reason:"Playback verification failed: currentTime is not advancing"})}startMonitoring(){this.stopMonitoring(),this.lastCurrentTime=this.container.currentTime,this.unregisterMonitor=this.scheduler.register({interval:5e3,owner:"play-monitor",priority:h.NORMAL,callback:()=>this.checkStalled()})}stopMonitoring(){this.unregisterMonitor&&(this.unregisterMonitor(),this.unregisterMonitor=void 0)}checkStalled(){const t=this.container.currentTime,e=t-this.lastCurrentTime;e<.1&&!this.container.paused?this.emitter.emit("play:stalled",{reason:`Playback stalled: currentTime not advancing (advanced ${e.toFixed(2)}s in 5s), video error: ${this.container.error}`}):this.lastCurrentTime=t}onVideoError(t){const e=t.target,r=e.error?.code,n=e.error?.message;this.emitter.emit("error",new i(s.VIDEO_ERROR,`Video element error (code: ${r}): ${n}`))}destroy(){this.stopMonitoring(),this.handleVideoError&&(this.container.removeEventListener("error",this.handleVideoError),this.handleVideoError=void 0)}}class m{constructor(t,s,i=!0,r){this.container=t,this.eventEmitter=s,this.lazyLoad=i,this.showStore=e(!1),this.playMonitor=new u({container:t,emitter:this.eventEmitter},r),this.flowMonitor=new p({interval:5e3,emitter:this.eventEmitter},r),this.showStore.subscribe(t=>{t?this.resume():this.pause()}),this.lazyLoad?(this.observer=new IntersectionObserver(([t])=>{t.isIntersecting?this.showStore.set(!0):this.showStore.set(!1)},{threshold:.5}),this.observer.observe(this.container)):this.showStore.set(!0)}onTrack(t,e){this.stream=t.streams[0];if(!this.stream.getTracks().some(t=>"video"===t.kind)){const t=new i(s.OTHER_ERROR,"No video track found in stream. Only audio streams are not supported.");return void this.eventEmitter.emit("error",t)}this.showStore.get()&&(this.container.srcObject=this.stream,this.playMonitor.play()),this.flowMonitor.start(e)}get paused(){return null===this.container.srcObject}pause(){this.playMonitor.stopMonitoring(),this.container.srcObject=null}resume(){this.stream&&(this.container.srcObject=this.stream,this.playMonitor.play())}stop(){this.playMonitor.stopMonitoring(),this.flowMonitor.stop(),this.stream=void 0}destroy(){this.playMonitor.destroy(),this.flowMonitor.stop(),this.stream=void 0,this.observer&&(this.observer.disconnect(),this.observer=void 0)}}class g extends t{constructor(t){super(),this.retryPause=2e3,this.stateStore=e("getting_codecs"),this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=t,this.stateStore.subscribe((t,e)=>{this.emit("state:change",{from:e,to:t})}),this.scheduler=new d,this.trackManager=new m(this.conf.container,this,this.conf.lazyLoad,this.scheduler),this.httpClient=new c({conf:this.conf,getState:()=>this.stateStore.get(),emitter:this}),this.connectionManager=new a({getState:()=>this.stateStore.get(),emitter:this,getNonAdvertisedCodecs:()=>this.nonAdvertisedCodecs}),this.codecDetector=new n({getState:()=>this.stateStore.get(),emitter:this}),this.on("codecs:detected",t=>this.handleCodecsDetected(t)),this.on("candidate",t=>this.handleCandidate(t)),this.on("track",t=>this.trackManager.onTrack(t,this.connectionManager.getPeerConnection())),this.on("play:stalled",t=>this.handlePlayStalled(t)),this.on("flow:stalled",t=>this.handleFlowStalled(t)),this.on("error",t=>this.handleError(t)),this.codecDetector.detect()}get state(){return this.stateStore.get()}close(){this.stateStore.set("closed"),this.connectionManager.close(),this.trackManager.destroy(),this.scheduler.destroy(),this.restartTimeout&&clearTimeout(this.restartTimeout),this.emit("close")}cleanupSession(){this.restartTimeout&&(clearTimeout(this.restartTimeout),this.restartTimeout=void 0),this.connectionManager.close(),this.trackManager.stop(),this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}).catch(()=>{}),this.sessionUrl=void 0)}handleError(t){if(t instanceof i)this.cleanupSession(),this.stateStore.set("failed");else if("getting_codecs"===this.stateStore.get())this.cleanupSession(),this.stateStore.set("failed");else if("running"===this.stateStore.get()){if(t instanceof i&&t.type===s.VIDEO_ERROR)return void this.handlePlayStalled({reason:t.message});this.cleanupSession(),this.stateStore.set("restarting"),this.emit("restart"),this.restartTimeout=setTimeout(()=>{this.restartTimeout=void 0,this.stateStore.set("running"),this.start()},this.retryPause)}}handlePlayStalled(t){this.pause(),setTimeout(()=>this.resume(),100)}handleFlowStalled(t){"running"===this.stateStore.get()&&(this.cleanupSession(),this.stateStore.set("restarting"),this.emit("restart"),this.restartTimeout=setTimeout(()=>{this.restartTimeout=void 0,this.stateStore.set("running"),this.start()},this.retryPause))}handleCodecsDetected(t){this.nonAdvertisedCodecs=t,this.stateStore.set("running"),this.start()}start(){this.httpClient.requestICEServers().then(t=>this.connectionManager.setupPeerConnection(t)).then(t=>this.httpClient.sendOffer(t)).then(({sessionUrl:t,answer:e})=>this.handleOfferResponse(t,e)).catch(t=>this.handleError(t))}handleOfferResponse(t,e){return t&&(this.sessionUrl=t),this.connectionManager.setAnswer(e).then(()=>{if("running"===this.stateStore.get()&&0!==this.queuedCandidates.length){const t=this.connectionManager.getOfferData();t&&this.sessionUrl&&(this.httpClient.sendLocalCandidates(this.sessionUrl,t,this.queuedCandidates),this.queuedCandidates=[])}})}handleCandidate(t){if(this.sessionUrl){const e=this.connectionManager.getOfferData();e&&this.httpClient.sendLocalCandidates(this.sessionUrl,e,[t])}else this.queuedCandidates.push(t)}get paused(){return this.trackManager.paused}pause(){this.trackManager.pause()}resume(){this.trackManager.resume()}updateUrl(t){"closed"!==this.state?(this.conf.url=t,this.cleanupSession(),this.stateStore.set("running"),this.start()):this.emit("error",new i(s.STATE_ERROR,"Cannot update URL: instance is closed"))}}export{s as ErrorTypes,i as WebRTCError,g as default};