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