@daydreamlive/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1058 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BaseDaydreamError: () => BaseDaydreamError,
24
+ Broadcast: () => Broadcast,
25
+ ConnectionError: () => ConnectionError,
26
+ DEFAULT_AUDIO_BITRATE: () => DEFAULT_AUDIO_BITRATE,
27
+ DEFAULT_ICE_SERVERS: () => DEFAULT_ICE_SERVERS,
28
+ DEFAULT_VIDEO_BITRATE: () => DEFAULT_VIDEO_BITRATE,
29
+ NetworkError: () => NetworkError,
30
+ Player: () => Player,
31
+ StreamNotFoundError: () => StreamNotFoundError,
32
+ UnauthorizedError: () => UnauthorizedError,
33
+ createBroadcast: () => createBroadcast2,
34
+ createPlayer: () => createPlayer2,
35
+ livepeerResponseHandler: () => livepeerResponseHandler
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/types.ts
40
+ var DEFAULT_ICE_SERVERS = [
41
+ { urls: "stun:stun.l.google.com:19302" },
42
+ { urls: "stun:stun1.l.google.com:19302" },
43
+ { urls: "stun:stun.cloudflare.com:3478" }
44
+ ];
45
+ var DEFAULT_VIDEO_BITRATE = 2e6;
46
+ var DEFAULT_AUDIO_BITRATE = 64e3;
47
+
48
+ // src/errors.ts
49
+ var BaseDaydreamError = class extends Error {
50
+ constructor(code, message, cause) {
51
+ super(message);
52
+ this.name = "DaydreamError";
53
+ this.code = code;
54
+ this.cause = cause;
55
+ }
56
+ };
57
+ var NetworkError = class extends BaseDaydreamError {
58
+ constructor(message, cause) {
59
+ super("NETWORK_ERROR", message, cause);
60
+ this.name = "NetworkError";
61
+ }
62
+ };
63
+ var ConnectionError = class extends BaseDaydreamError {
64
+ constructor(message, cause) {
65
+ super("CONNECTION_FAILED", message, cause);
66
+ this.name = "ConnectionError";
67
+ }
68
+ };
69
+ var StreamNotFoundError = class extends BaseDaydreamError {
70
+ constructor(message, cause) {
71
+ super("STREAM_NOT_FOUND", message, cause);
72
+ this.name = "StreamNotFoundError";
73
+ }
74
+ };
75
+ var UnauthorizedError = class extends BaseDaydreamError {
76
+ constructor(message, cause) {
77
+ super("UNAUTHORIZED", message, cause);
78
+ this.name = "UnauthorizedError";
79
+ }
80
+ };
81
+
82
+ // src/internal/dependencies.ts
83
+ var defaultMediaStreamFactory = {
84
+ create: () => new MediaStream()
85
+ };
86
+ var defaultPeerConnectionFactory = {
87
+ create: (config) => new RTCPeerConnection(config)
88
+ };
89
+ var defaultFetch = globalThis.fetch.bind(globalThis);
90
+ var defaultTimerProvider = {
91
+ setTimeout: (cb, ms) => globalThis.setTimeout(cb, ms),
92
+ clearTimeout: (id) => globalThis.clearTimeout(id),
93
+ setInterval: (cb, ms) => globalThis.setInterval(cb, ms),
94
+ clearInterval: (id) => globalThis.clearInterval(id)
95
+ };
96
+
97
+ // src/internal/WHIPClient.ts
98
+ var PLAYBACK_ID_PATTERN = /([/+])([^/+?]+)$/;
99
+ var PLAYBACK_ID_PLACEHOLDER = "__PLAYBACK_ID__";
100
+ var LRURedirectCache = class {
101
+ constructor(maxSize = 10) {
102
+ this.cache = /* @__PURE__ */ new Map();
103
+ this.maxSize = maxSize;
104
+ }
105
+ get(key) {
106
+ const cached = this.cache.get(key);
107
+ if (cached) {
108
+ this.cache.delete(key);
109
+ this.cache.set(key, cached);
110
+ }
111
+ return cached;
112
+ }
113
+ set(key, value) {
114
+ if (this.cache.has(key)) {
115
+ this.cache.delete(key);
116
+ } else if (this.cache.size >= this.maxSize) {
117
+ const oldestKey = this.cache.keys().next().value;
118
+ if (oldestKey) this.cache.delete(oldestKey);
119
+ }
120
+ this.cache.set(key, value);
121
+ }
122
+ };
123
+ function preferH264(sdp) {
124
+ const lines = sdp.split("\r\n");
125
+ const mLineIndex = lines.findIndex((line) => line.startsWith("m=video"));
126
+ if (mLineIndex === -1) return sdp;
127
+ const codecRegex = /a=rtpmap:(\d+) H264(\/\d+)+/;
128
+ const codecLine = lines.find((line) => codecRegex.test(line));
129
+ if (!codecLine) return sdp;
130
+ const match = codecRegex.exec(codecLine);
131
+ const codecPayload = match?.[1];
132
+ if (!codecPayload) return sdp;
133
+ const mLine = lines[mLineIndex];
134
+ if (!mLine) return sdp;
135
+ const mLineElements = mLine.split(" ");
136
+ const reorderedMLine = [
137
+ ...mLineElements.slice(0, 3),
138
+ codecPayload,
139
+ ...mLineElements.slice(3).filter((payload) => payload !== codecPayload)
140
+ ];
141
+ lines[mLineIndex] = reorderedMLine.join(" ");
142
+ return lines.join("\r\n");
143
+ }
144
+ var sharedRedirectCache = new LRURedirectCache();
145
+ var WHIPClient = class {
146
+ constructor(config) {
147
+ this.pc = null;
148
+ this.resourceUrl = null;
149
+ this.abortController = null;
150
+ this.statsTimer = null;
151
+ this.videoSender = null;
152
+ this.audioSender = null;
153
+ this.videoTransceiver = null;
154
+ this.audioTransceiver = null;
155
+ this.iceGatheringTimer = null;
156
+ this.url = config.url;
157
+ this.iceServers = config.iceServers ?? DEFAULT_ICE_SERVERS;
158
+ this.videoBitrate = config.videoBitrate ?? DEFAULT_VIDEO_BITRATE;
159
+ this.audioBitrate = config.audioBitrate ?? DEFAULT_AUDIO_BITRATE;
160
+ this.maxFramerate = config.maxFramerate;
161
+ this.onStats = config.onStats;
162
+ this.statsIntervalMs = config.statsIntervalMs ?? 5e3;
163
+ this.onResponse = config.onResponse;
164
+ this.pcFactory = config.peerConnectionFactory ?? defaultPeerConnectionFactory;
165
+ this.fetch = config.fetch ?? defaultFetch;
166
+ this.timers = config.timers ?? defaultTimerProvider;
167
+ this.redirectCache = config.redirectCache ?? sharedRedirectCache;
168
+ this.skipIceGathering = config.skipIceGathering ?? true;
169
+ }
170
+ async connect(stream) {
171
+ this.cleanup();
172
+ this.pc = this.pcFactory.create({
173
+ iceServers: this.iceServers,
174
+ iceCandidatePoolSize: 10
175
+ });
176
+ this.videoTransceiver = this.pc.addTransceiver("video", {
177
+ direction: "sendonly"
178
+ });
179
+ this.audioTransceiver = this.pc.addTransceiver("audio", {
180
+ direction: "sendonly"
181
+ });
182
+ this.videoSender = this.videoTransceiver.sender;
183
+ this.audioSender = this.audioTransceiver.sender;
184
+ const videoTrack = stream.getVideoTracks()[0];
185
+ const audioTrack = stream.getAudioTracks()[0];
186
+ if (videoTrack) {
187
+ if (videoTrack.contentHint === "") {
188
+ videoTrack.contentHint = "motion";
189
+ }
190
+ await this.videoSender.replaceTrack(videoTrack);
191
+ }
192
+ if (audioTrack) {
193
+ await this.audioSender.replaceTrack(audioTrack);
194
+ }
195
+ this.setCodecPreferences();
196
+ await this.applyBitrateConstraints();
197
+ const offer = await this.pc.createOffer({
198
+ offerToReceiveAudio: false,
199
+ offerToReceiveVideo: false
200
+ });
201
+ const enhancedSdp = preferH264(offer.sdp ?? "");
202
+ await this.pc.setLocalDescription({ type: "offer", sdp: enhancedSdp });
203
+ if (!this.skipIceGathering) {
204
+ await this.waitForIceGathering();
205
+ }
206
+ this.abortController = new AbortController();
207
+ const timeoutId = this.timers.setTimeout(
208
+ () => this.abortController?.abort(),
209
+ 1e4
210
+ );
211
+ try {
212
+ const fetchUrl = this.getUrlWithCachedRedirect();
213
+ const response = await this.fetch(fetchUrl, {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/sdp" },
216
+ body: this.pc.localDescription.sdp,
217
+ signal: this.abortController.signal
218
+ });
219
+ this.timers.clearTimeout(timeoutId);
220
+ if (!response.ok) {
221
+ const errorText = await response.text().catch(() => "");
222
+ throw new ConnectionError(
223
+ `WHIP connection failed: ${response.status} ${response.statusText} ${errorText}`
224
+ );
225
+ }
226
+ this.cacheRedirectIfNeeded(fetchUrl, response.url);
227
+ const location = response.headers.get("location");
228
+ if (location) {
229
+ this.resourceUrl = new URL(location, this.url).toString();
230
+ }
231
+ const responseResult = this.onResponse?.(response);
232
+ const answerSdp = await response.text();
233
+ await this.pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
234
+ await this.applyBitrateConstraints();
235
+ this.startStatsTimer();
236
+ return { whepUrl: responseResult?.whepUrl ?? null };
237
+ } catch (error) {
238
+ this.timers.clearTimeout(timeoutId);
239
+ if (error instanceof ConnectionError) {
240
+ throw error;
241
+ }
242
+ if (error instanceof Error && error.name === "AbortError") {
243
+ throw new NetworkError("Connection timeout");
244
+ }
245
+ throw new NetworkError("Failed to establish connection", error);
246
+ }
247
+ }
248
+ setCodecPreferences() {
249
+ if (!this.videoTransceiver?.setCodecPreferences) return;
250
+ try {
251
+ const caps = RTCRtpSender.getCapabilities("video");
252
+ if (!caps?.codecs?.length) return;
253
+ const h264Codecs = caps.codecs.filter(
254
+ (c) => c.mimeType.toLowerCase().includes("h264")
255
+ );
256
+ if (h264Codecs.length) {
257
+ this.videoTransceiver.setCodecPreferences(h264Codecs);
258
+ }
259
+ } catch {
260
+ }
261
+ }
262
+ async applyBitrateConstraints() {
263
+ if (!this.pc) return;
264
+ const senders = this.pc.getSenders();
265
+ for (const sender of senders) {
266
+ if (!sender.track) continue;
267
+ const params = sender.getParameters();
268
+ if (!params.encodings) params.encodings = [{}];
269
+ const encoding = params.encodings[0];
270
+ if (!encoding) continue;
271
+ if (sender.track.kind === "video") {
272
+ encoding.maxBitrate = this.videoBitrate;
273
+ if (this.maxFramerate && this.maxFramerate > 0) {
274
+ encoding.maxFramerate = this.maxFramerate;
275
+ }
276
+ encoding.scaleResolutionDownBy = 1;
277
+ encoding.priority = "high";
278
+ encoding.networkPriority = "high";
279
+ params.degradationPreference = "maintain-resolution";
280
+ } else if (sender.track.kind === "audio") {
281
+ encoding.maxBitrate = this.audioBitrate;
282
+ encoding.priority = "medium";
283
+ encoding.networkPriority = "medium";
284
+ }
285
+ try {
286
+ await sender.setParameters(params);
287
+ } catch {
288
+ }
289
+ }
290
+ }
291
+ waitForIceGathering() {
292
+ return new Promise((resolve) => {
293
+ if (!this.pc) {
294
+ resolve();
295
+ return;
296
+ }
297
+ if (this.pc.iceGatheringState === "complete") {
298
+ resolve();
299
+ return;
300
+ }
301
+ const onStateChange = () => {
302
+ if (this.pc?.iceGatheringState === "complete") {
303
+ this.pc.removeEventListener("icegatheringstatechange", onStateChange);
304
+ if (this.iceGatheringTimer !== null) {
305
+ this.timers.clearTimeout(this.iceGatheringTimer);
306
+ this.iceGatheringTimer = null;
307
+ }
308
+ resolve();
309
+ }
310
+ };
311
+ this.pc.addEventListener("icegatheringstatechange", onStateChange);
312
+ this.iceGatheringTimer = this.timers.setTimeout(() => {
313
+ this.pc?.removeEventListener("icegatheringstatechange", onStateChange);
314
+ this.iceGatheringTimer = null;
315
+ resolve();
316
+ }, 1e3);
317
+ });
318
+ }
319
+ startStatsTimer() {
320
+ if (!this.onStats || !this.pc) return;
321
+ this.stopStatsTimer();
322
+ this.statsTimer = this.timers.setInterval(async () => {
323
+ if (!this.pc) return;
324
+ try {
325
+ const report = await this.pc.getStats();
326
+ this.onStats?.(report);
327
+ } catch {
328
+ }
329
+ }, this.statsIntervalMs);
330
+ }
331
+ stopStatsTimer() {
332
+ if (this.statsTimer !== null) {
333
+ this.timers.clearInterval(this.statsTimer);
334
+ this.statsTimer = null;
335
+ }
336
+ }
337
+ async replaceTrack(track) {
338
+ if (!this.pc) {
339
+ throw new ConnectionError("Not connected");
340
+ }
341
+ const sender = track.kind === "video" ? this.videoSender : this.audioSender;
342
+ if (!sender) {
343
+ throw new ConnectionError(
344
+ `No sender found for track kind: ${track.kind}`
345
+ );
346
+ }
347
+ await sender.replaceTrack(track);
348
+ await this.applyBitrateConstraints();
349
+ }
350
+ setMaxFramerate(fps) {
351
+ this.maxFramerate = fps;
352
+ void this.applyBitrateConstraints();
353
+ }
354
+ cleanup() {
355
+ this.stopStatsTimer();
356
+ if (this.iceGatheringTimer !== null) {
357
+ this.timers.clearTimeout(this.iceGatheringTimer);
358
+ this.iceGatheringTimer = null;
359
+ }
360
+ if (this.abortController) {
361
+ try {
362
+ this.abortController.abort();
363
+ } catch {
364
+ }
365
+ this.abortController = null;
366
+ }
367
+ if (this.pc) {
368
+ try {
369
+ this.pc.getTransceivers().forEach((t) => {
370
+ try {
371
+ t.stop();
372
+ } catch {
373
+ }
374
+ });
375
+ } catch {
376
+ }
377
+ try {
378
+ this.pc.close();
379
+ } catch {
380
+ }
381
+ this.pc = null;
382
+ }
383
+ this.videoSender = null;
384
+ this.audioSender = null;
385
+ this.videoTransceiver = null;
386
+ this.audioTransceiver = null;
387
+ }
388
+ async disconnect() {
389
+ if (this.resourceUrl) {
390
+ try {
391
+ await this.fetch(this.resourceUrl, { method: "DELETE" });
392
+ } catch {
393
+ }
394
+ }
395
+ this.cleanup();
396
+ this.resourceUrl = null;
397
+ }
398
+ getPeerConnection() {
399
+ return this.pc;
400
+ }
401
+ restartIce() {
402
+ if (this.pc) {
403
+ try {
404
+ this.pc.restartIce();
405
+ } catch {
406
+ }
407
+ }
408
+ }
409
+ isConnected() {
410
+ return this.pc !== null && this.pc.connectionState === "connected";
411
+ }
412
+ getUrlWithCachedRedirect() {
413
+ const originalUrl = new URL(this.url);
414
+ const playbackIdMatch = originalUrl.pathname.match(PLAYBACK_ID_PATTERN);
415
+ const playbackId = playbackIdMatch?.[2];
416
+ const cachedTemplate = this.redirectCache.get(this.url);
417
+ if (!cachedTemplate || !playbackId) {
418
+ return this.url;
419
+ }
420
+ const redirectedUrl = new URL(cachedTemplate);
421
+ redirectedUrl.pathname = cachedTemplate.pathname.replace(
422
+ PLAYBACK_ID_PLACEHOLDER,
423
+ playbackId
424
+ );
425
+ return redirectedUrl.toString();
426
+ }
427
+ cacheRedirectIfNeeded(requestUrl, responseUrl) {
428
+ if (requestUrl === responseUrl) return;
429
+ try {
430
+ const actualRedirect = new URL(responseUrl);
431
+ const template = new URL(actualRedirect);
432
+ template.pathname = template.pathname.replace(
433
+ PLAYBACK_ID_PATTERN,
434
+ `$1${PLAYBACK_ID_PLACEHOLDER}`
435
+ );
436
+ this.redirectCache.set(this.url, template);
437
+ } catch {
438
+ }
439
+ }
440
+ };
441
+
442
+ // src/internal/TypedEventEmitter.ts
443
+ var TypedEventEmitter = class {
444
+ constructor() {
445
+ this.listeners = /* @__PURE__ */ new Map();
446
+ }
447
+ on(event, handler) {
448
+ if (!this.listeners.has(event)) {
449
+ this.listeners.set(event, /* @__PURE__ */ new Set());
450
+ }
451
+ this.listeners.get(event).add(handler);
452
+ return this;
453
+ }
454
+ off(event, handler) {
455
+ this.listeners.get(event)?.delete(handler);
456
+ return this;
457
+ }
458
+ emit(event, ...args) {
459
+ this.listeners.get(event)?.forEach((handler) => {
460
+ handler(...args);
461
+ });
462
+ }
463
+ clearListeners() {
464
+ this.listeners.clear();
465
+ }
466
+ };
467
+
468
+ // src/internal/StateMachine.ts
469
+ function createStateMachine(initial, transitions, onChange) {
470
+ let current = initial;
471
+ return {
472
+ get current() {
473
+ return current;
474
+ },
475
+ can(next) {
476
+ return transitions[current].includes(next);
477
+ },
478
+ transition(next) {
479
+ if (!transitions[current].includes(next)) return false;
480
+ const prev = current;
481
+ current = next;
482
+ onChange?.(prev, next);
483
+ return true;
484
+ },
485
+ force(next) {
486
+ const prev = current;
487
+ current = next;
488
+ onChange?.(prev, next);
489
+ }
490
+ };
491
+ }
492
+
493
+ // src/Broadcast.ts
494
+ var BROADCAST_TRANSITIONS = {
495
+ connecting: ["live", "error"],
496
+ live: ["reconnecting", "ended"],
497
+ reconnecting: ["live", "ended"],
498
+ ended: [],
499
+ error: ["connecting"]
500
+ };
501
+ var Broadcast = class extends TypedEventEmitter {
502
+ constructor(config) {
503
+ super();
504
+ this._whepUrl = null;
505
+ this.reconnectAttempts = 0;
506
+ this.reconnectTimeout = null;
507
+ this.disconnectedGraceTimeout = null;
508
+ this.currentStream = config.stream;
509
+ this.reconnectConfig = {
510
+ enabled: config.reconnect?.enabled ?? true,
511
+ maxAttempts: config.reconnect?.maxAttempts ?? 5,
512
+ baseDelayMs: config.reconnect?.baseDelayMs ?? 1e3
513
+ };
514
+ this.whipClient = new WHIPClient({
515
+ url: config.whipUrl,
516
+ ...config.whipConfig
517
+ });
518
+ this.stateMachine = createStateMachine(
519
+ "connecting",
520
+ BROADCAST_TRANSITIONS,
521
+ (_from, to) => this.emit("stateChange", to)
522
+ );
523
+ }
524
+ get state() {
525
+ return this.stateMachine.current;
526
+ }
527
+ get whepUrl() {
528
+ return this._whepUrl;
529
+ }
530
+ get stream() {
531
+ return this.currentStream;
532
+ }
533
+ async connect() {
534
+ try {
535
+ const result = await this.whipClient.connect(this.currentStream);
536
+ if (result.whepUrl) {
537
+ this._whepUrl = result.whepUrl;
538
+ }
539
+ this.setupConnectionMonitoring();
540
+ this.stateMachine.transition("live");
541
+ } catch (error) {
542
+ this.stateMachine.transition("error");
543
+ const daydreamError = error instanceof Error ? error : new ConnectionError("Failed to connect", error);
544
+ this.emit("error", daydreamError);
545
+ throw daydreamError;
546
+ }
547
+ }
548
+ async stop() {
549
+ this.stateMachine.force("ended");
550
+ this.clearTimeouts();
551
+ await this.whipClient.disconnect();
552
+ this.clearListeners();
553
+ }
554
+ async replaceStream(newStream) {
555
+ if (!this.whipClient.isConnected()) {
556
+ this.currentStream = newStream;
557
+ return;
558
+ }
559
+ const videoTrack = newStream.getVideoTracks()[0];
560
+ const audioTrack = newStream.getAudioTracks()[0];
561
+ try {
562
+ if (videoTrack) {
563
+ await this.whipClient.replaceTrack(videoTrack);
564
+ }
565
+ if (audioTrack) {
566
+ await this.whipClient.replaceTrack(audioTrack);
567
+ }
568
+ this.currentStream = newStream;
569
+ } catch {
570
+ this.currentStream = newStream;
571
+ this.scheduleReconnect();
572
+ }
573
+ }
574
+ setupConnectionMonitoring() {
575
+ const pc = this.whipClient.getPeerConnection();
576
+ if (!pc) return;
577
+ pc.onconnectionstatechange = () => {
578
+ if (this.state === "ended") return;
579
+ const connState = pc.connectionState;
580
+ if (connState === "connected") {
581
+ this.clearGraceTimeout();
582
+ if (this.state === "reconnecting") {
583
+ this.stateMachine.transition("live");
584
+ this.reconnectAttempts = 0;
585
+ }
586
+ return;
587
+ }
588
+ if (connState === "disconnected") {
589
+ this.clearGraceTimeout();
590
+ this.whipClient.restartIce();
591
+ this.disconnectedGraceTimeout = setTimeout(() => {
592
+ if (this.state === "ended") return;
593
+ const currentState = pc.connectionState;
594
+ if (currentState === "disconnected") {
595
+ this.scheduleReconnect();
596
+ }
597
+ }, 2e3);
598
+ return;
599
+ }
600
+ if (connState === "failed" || connState === "closed") {
601
+ this.clearGraceTimeout();
602
+ this.scheduleReconnect();
603
+ }
604
+ };
605
+ }
606
+ clearGraceTimeout() {
607
+ if (this.disconnectedGraceTimeout) {
608
+ clearTimeout(this.disconnectedGraceTimeout);
609
+ this.disconnectedGraceTimeout = null;
610
+ }
611
+ }
612
+ clearReconnectTimeout() {
613
+ if (this.reconnectTimeout) {
614
+ clearTimeout(this.reconnectTimeout);
615
+ this.reconnectTimeout = null;
616
+ }
617
+ }
618
+ clearTimeouts() {
619
+ this.clearGraceTimeout();
620
+ this.clearReconnectTimeout();
621
+ }
622
+ scheduleReconnect() {
623
+ if (this.state === "ended") return;
624
+ if (!this.reconnectConfig.enabled) {
625
+ this.stateMachine.transition("ended");
626
+ return;
627
+ }
628
+ const maxAttempts = this.reconnectConfig.maxAttempts ?? 5;
629
+ if (this.reconnectAttempts >= maxAttempts) {
630
+ this.stateMachine.transition("ended");
631
+ return;
632
+ }
633
+ this.clearReconnectTimeout();
634
+ this.stateMachine.transition("reconnecting");
635
+ const baseDelay = this.reconnectConfig.baseDelayMs ?? 1e3;
636
+ const delay = baseDelay * Math.pow(2, this.reconnectAttempts);
637
+ this.reconnectAttempts++;
638
+ this.reconnectTimeout = setTimeout(async () => {
639
+ if (this.state === "ended") return;
640
+ try {
641
+ await this.whipClient.disconnect();
642
+ const result = await this.whipClient.connect(this.currentStream);
643
+ if (result.whepUrl) {
644
+ this._whepUrl = result.whepUrl;
645
+ }
646
+ this.setupConnectionMonitoring();
647
+ this.stateMachine.transition("live");
648
+ this.reconnectAttempts = 0;
649
+ } catch {
650
+ this.scheduleReconnect();
651
+ }
652
+ }, delay);
653
+ }
654
+ };
655
+ function createBroadcast(options) {
656
+ const {
657
+ whipUrl,
658
+ stream,
659
+ reconnect,
660
+ video,
661
+ onStats,
662
+ statsIntervalMs,
663
+ onResponse
664
+ } = options;
665
+ return new Broadcast({
666
+ whipUrl,
667
+ stream,
668
+ reconnect,
669
+ whipConfig: {
670
+ videoBitrate: video?.bitrate,
671
+ maxFramerate: video?.maxFramerate,
672
+ onStats,
673
+ statsIntervalMs,
674
+ onResponse
675
+ }
676
+ });
677
+ }
678
+
679
+ // src/internal/WHEPClient.ts
680
+ var WHEPClient = class {
681
+ constructor(config) {
682
+ this.pc = null;
683
+ this.resourceUrl = null;
684
+ this.stream = null;
685
+ this.abortController = null;
686
+ this.statsTimer = null;
687
+ this.iceGatheringTimer = null;
688
+ this.url = config.url;
689
+ this.iceServers = config.iceServers ?? DEFAULT_ICE_SERVERS;
690
+ this.onStats = config.onStats;
691
+ this.statsIntervalMs = config.statsIntervalMs ?? 5e3;
692
+ this.pcFactory = config.peerConnectionFactory ?? defaultPeerConnectionFactory;
693
+ this.fetch = config.fetch ?? defaultFetch;
694
+ this.timers = config.timers ?? defaultTimerProvider;
695
+ this.mediaStreamFactory = config.mediaStreamFactory ?? defaultMediaStreamFactory;
696
+ }
697
+ async connect() {
698
+ this.cleanup();
699
+ this.pc = this.pcFactory.create({
700
+ iceServers: this.iceServers
701
+ });
702
+ this.pc.addTransceiver("video", { direction: "recvonly" });
703
+ this.pc.addTransceiver("audio", { direction: "recvonly" });
704
+ this.stream = this.mediaStreamFactory.create();
705
+ this.pc.ontrack = (event) => {
706
+ const [remoteStream] = event.streams;
707
+ if (remoteStream) {
708
+ this.stream = remoteStream;
709
+ } else if (this.stream) {
710
+ this.stream.addTrack(event.track);
711
+ }
712
+ };
713
+ const offer = await this.pc.createOffer();
714
+ await this.pc.setLocalDescription(offer);
715
+ await this.waitForIceGathering();
716
+ this.abortController = new AbortController();
717
+ const timeoutId = this.timers.setTimeout(
718
+ () => this.abortController?.abort(),
719
+ 1e4
720
+ );
721
+ try {
722
+ const response = await this.fetch(this.url, {
723
+ method: "POST",
724
+ headers: { "Content-Type": "application/sdp" },
725
+ body: this.pc.localDescription.sdp,
726
+ signal: this.abortController.signal
727
+ });
728
+ this.timers.clearTimeout(timeoutId);
729
+ if (!response.ok) {
730
+ const errorText = await response.text().catch(() => "");
731
+ throw new ConnectionError(
732
+ `WHEP connection failed: ${response.status} ${response.statusText} ${errorText}`
733
+ );
734
+ }
735
+ const location = response.headers.get("location");
736
+ if (location) {
737
+ this.resourceUrl = new URL(location, this.url).toString();
738
+ }
739
+ const answerSdp = await response.text();
740
+ await this.pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
741
+ this.startStatsTimer();
742
+ return this.stream;
743
+ } catch (error) {
744
+ this.timers.clearTimeout(timeoutId);
745
+ if (error instanceof ConnectionError) {
746
+ throw error;
747
+ }
748
+ if (error instanceof Error && error.name === "AbortError") {
749
+ throw new NetworkError("Connection timeout");
750
+ }
751
+ throw new NetworkError("Failed to establish connection", error);
752
+ }
753
+ }
754
+ waitForIceGathering() {
755
+ return new Promise((resolve) => {
756
+ if (!this.pc) {
757
+ resolve();
758
+ return;
759
+ }
760
+ if (this.pc.iceGatheringState === "complete") {
761
+ resolve();
762
+ return;
763
+ }
764
+ const onStateChange = () => {
765
+ if (this.pc?.iceGatheringState === "complete") {
766
+ this.pc.removeEventListener("icegatheringstatechange", onStateChange);
767
+ if (this.iceGatheringTimer !== null) {
768
+ this.timers.clearTimeout(this.iceGatheringTimer);
769
+ this.iceGatheringTimer = null;
770
+ }
771
+ resolve();
772
+ }
773
+ };
774
+ this.pc.addEventListener("icegatheringstatechange", onStateChange);
775
+ this.iceGatheringTimer = this.timers.setTimeout(() => {
776
+ this.pc?.removeEventListener("icegatheringstatechange", onStateChange);
777
+ this.iceGatheringTimer = null;
778
+ resolve();
779
+ }, 1e3);
780
+ });
781
+ }
782
+ startStatsTimer() {
783
+ if (!this.onStats || !this.pc) return;
784
+ this.stopStatsTimer();
785
+ this.statsTimer = this.timers.setInterval(async () => {
786
+ if (!this.pc) return;
787
+ try {
788
+ const report = await this.pc.getStats();
789
+ this.onStats?.(report);
790
+ } catch {
791
+ }
792
+ }, this.statsIntervalMs);
793
+ }
794
+ stopStatsTimer() {
795
+ if (this.statsTimer !== null) {
796
+ this.timers.clearInterval(this.statsTimer);
797
+ this.statsTimer = null;
798
+ }
799
+ }
800
+ cleanup() {
801
+ this.stopStatsTimer();
802
+ if (this.iceGatheringTimer !== null) {
803
+ this.timers.clearTimeout(this.iceGatheringTimer);
804
+ this.iceGatheringTimer = null;
805
+ }
806
+ if (this.abortController) {
807
+ try {
808
+ this.abortController.abort();
809
+ } catch {
810
+ }
811
+ this.abortController = null;
812
+ }
813
+ if (this.pc) {
814
+ try {
815
+ this.pc.getTransceivers().forEach((t) => {
816
+ try {
817
+ t.stop();
818
+ } catch {
819
+ }
820
+ });
821
+ } catch {
822
+ }
823
+ try {
824
+ this.pc.close();
825
+ } catch {
826
+ }
827
+ this.pc = null;
828
+ }
829
+ }
830
+ async disconnect() {
831
+ if (this.resourceUrl) {
832
+ try {
833
+ await this.fetch(this.resourceUrl, { method: "DELETE" });
834
+ } catch {
835
+ }
836
+ }
837
+ this.cleanup();
838
+ this.stream = null;
839
+ this.resourceUrl = null;
840
+ }
841
+ getStream() {
842
+ return this.stream;
843
+ }
844
+ getPeerConnection() {
845
+ return this.pc;
846
+ }
847
+ restartIce() {
848
+ if (this.pc) {
849
+ try {
850
+ this.pc.restartIce();
851
+ } catch {
852
+ }
853
+ }
854
+ }
855
+ };
856
+
857
+ // src/Player.ts
858
+ var PLAYER_TRANSITIONS = {
859
+ connecting: ["playing", "buffering", "error"],
860
+ playing: ["buffering", "ended"],
861
+ buffering: ["playing", "ended"],
862
+ ended: [],
863
+ error: ["connecting"]
864
+ };
865
+ var Player = class extends TypedEventEmitter {
866
+ constructor(config) {
867
+ super();
868
+ this._stream = null;
869
+ this.reconnectAttempts = 0;
870
+ this.reconnectTimeout = null;
871
+ this.disconnectedGraceTimeout = null;
872
+ this.whepUrl = config.whepUrl;
873
+ this.whepConfig = config.whepConfig;
874
+ this.reconnectConfig = {
875
+ enabled: config.reconnect?.enabled ?? true,
876
+ maxAttempts: config.reconnect?.maxAttempts ?? 30,
877
+ baseDelayMs: config.reconnect?.baseDelayMs ?? 200
878
+ };
879
+ this.whepClient = new WHEPClient({
880
+ url: config.whepUrl,
881
+ ...this.whepConfig
882
+ });
883
+ this.stateMachine = createStateMachine(
884
+ "connecting",
885
+ PLAYER_TRANSITIONS,
886
+ (_from, to) => this.emit("stateChange", to)
887
+ );
888
+ }
889
+ get state() {
890
+ return this.stateMachine.current;
891
+ }
892
+ get stream() {
893
+ return this._stream;
894
+ }
895
+ async connect() {
896
+ try {
897
+ this._stream = await this.whepClient.connect();
898
+ this.setupConnectionMonitoring();
899
+ this.stateMachine.transition("playing");
900
+ this.reconnectAttempts = 0;
901
+ } catch (error) {
902
+ if (this.reconnectConfig.enabled && this.reconnectAttempts < (this.reconnectConfig.maxAttempts ?? 30)) {
903
+ this.scheduleReconnect();
904
+ return;
905
+ }
906
+ this.stateMachine.transition("error");
907
+ const daydreamError = error instanceof Error ? error : new ConnectionError("Failed to connect", error);
908
+ this.emit("error", daydreamError);
909
+ throw daydreamError;
910
+ }
911
+ }
912
+ attachTo(video) {
913
+ if (this._stream) {
914
+ video.srcObject = this._stream;
915
+ }
916
+ }
917
+ async stop() {
918
+ this.stateMachine.force("ended");
919
+ this.clearTimeouts();
920
+ await this.whepClient.disconnect();
921
+ this._stream = null;
922
+ this.clearListeners();
923
+ }
924
+ setupConnectionMonitoring() {
925
+ const pc = this.whepClient.getPeerConnection();
926
+ if (!pc) return;
927
+ pc.oniceconnectionstatechange = () => {
928
+ if (this.state === "ended") return;
929
+ const iceState = pc.iceConnectionState;
930
+ if (iceState === "connected" || iceState === "completed") {
931
+ this.clearGraceTimeout();
932
+ if (this.state === "buffering") {
933
+ this.stateMachine.transition("playing");
934
+ this.reconnectAttempts = 0;
935
+ }
936
+ return;
937
+ }
938
+ if (iceState === "disconnected") {
939
+ this.clearGraceTimeout();
940
+ this.whepClient.restartIce();
941
+ this.disconnectedGraceTimeout = setTimeout(() => {
942
+ if (this.state === "ended") return;
943
+ const currentState = pc.iceConnectionState;
944
+ if (currentState === "disconnected") {
945
+ this.scheduleReconnect();
946
+ }
947
+ }, 2e3);
948
+ return;
949
+ }
950
+ if (iceState === "failed" || iceState === "closed") {
951
+ this.clearGraceTimeout();
952
+ this.scheduleReconnect();
953
+ }
954
+ };
955
+ }
956
+ clearGraceTimeout() {
957
+ if (this.disconnectedGraceTimeout) {
958
+ clearTimeout(this.disconnectedGraceTimeout);
959
+ this.disconnectedGraceTimeout = null;
960
+ }
961
+ }
962
+ clearReconnectTimeout() {
963
+ if (this.reconnectTimeout) {
964
+ clearTimeout(this.reconnectTimeout);
965
+ this.reconnectTimeout = null;
966
+ }
967
+ }
968
+ clearTimeouts() {
969
+ this.clearGraceTimeout();
970
+ this.clearReconnectTimeout();
971
+ }
972
+ scheduleReconnect() {
973
+ if (this.state === "ended") return;
974
+ if (!this.reconnectConfig.enabled) {
975
+ this.stateMachine.transition("ended");
976
+ return;
977
+ }
978
+ const maxAttempts = this.reconnectConfig.maxAttempts ?? 10;
979
+ if (this.reconnectAttempts >= maxAttempts) {
980
+ this.stateMachine.transition("ended");
981
+ return;
982
+ }
983
+ this.clearReconnectTimeout();
984
+ this.stateMachine.transition("buffering");
985
+ const baseDelay = this.reconnectConfig.baseDelayMs ?? 300;
986
+ const delay = this.calculateReconnectDelay(
987
+ this.reconnectAttempts,
988
+ baseDelay
989
+ );
990
+ this.reconnectAttempts++;
991
+ this.reconnectTimeout = setTimeout(async () => {
992
+ if (this.state === "ended") return;
993
+ try {
994
+ await this.whepClient.disconnect();
995
+ this.whepClient = new WHEPClient({
996
+ url: this.whepUrl,
997
+ ...this.whepConfig
998
+ });
999
+ this._stream = await this.whepClient.connect();
1000
+ this.setupConnectionMonitoring();
1001
+ this.stateMachine.transition("playing");
1002
+ this.reconnectAttempts = 0;
1003
+ } catch {
1004
+ this.scheduleReconnect();
1005
+ }
1006
+ }, delay);
1007
+ }
1008
+ calculateReconnectDelay(attempt, baseDelay) {
1009
+ const linearPhaseEndCount = 10;
1010
+ const maxDelay = 6e4;
1011
+ if (attempt === 0) return 0;
1012
+ if (attempt <= linearPhaseEndCount) return baseDelay;
1013
+ const exponentialAttempt = attempt - linearPhaseEndCount;
1014
+ const delay = 500 * Math.pow(2, exponentialAttempt - 1);
1015
+ return Math.min(delay, maxDelay);
1016
+ }
1017
+ };
1018
+ function createPlayer(whepUrl, options) {
1019
+ return new Player({
1020
+ whepUrl,
1021
+ reconnect: options?.reconnect,
1022
+ whepConfig: {
1023
+ onStats: options?.onStats,
1024
+ statsIntervalMs: options?.statsIntervalMs
1025
+ }
1026
+ });
1027
+ }
1028
+
1029
+ // src/index.ts
1030
+ var livepeerResponseHandler = (response) => ({
1031
+ whepUrl: response.headers.get("livepeer-playback-url") ?? void 0
1032
+ });
1033
+ function createBroadcast2(options) {
1034
+ return createBroadcast({
1035
+ ...options,
1036
+ onResponse: livepeerResponseHandler
1037
+ });
1038
+ }
1039
+ function createPlayer2(whepUrl, options) {
1040
+ return createPlayer(whepUrl, options);
1041
+ }
1042
+ // Annotate the CommonJS export names for ESM import in node:
1043
+ 0 && (module.exports = {
1044
+ BaseDaydreamError,
1045
+ Broadcast,
1046
+ ConnectionError,
1047
+ DEFAULT_AUDIO_BITRATE,
1048
+ DEFAULT_ICE_SERVERS,
1049
+ DEFAULT_VIDEO_BITRATE,
1050
+ NetworkError,
1051
+ Player,
1052
+ StreamNotFoundError,
1053
+ UnauthorizedError,
1054
+ createBroadcast,
1055
+ createPlayer,
1056
+ livepeerResponseHandler
1057
+ });
1058
+ //# sourceMappingURL=index.cjs.map