@oddin-gg/havik-player 1.17.0 → 1.18.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.d.cts CHANGED
@@ -125,6 +125,16 @@ export declare interface CreatePlayerOptions {
125
125
  statusOverlays?: boolean;
126
126
  /** Message on the end-of-stream card. Default: "This stream has ended". */
127
127
  endedMessage?: string;
128
+ /**
129
+ * Message shown instead of {@link endedMessage} when the stream stopped but
130
+ * the match is still running (the server's `interrupted` reason — an ingest
131
+ * outage that outlived the bridge's reconnect hold). Default:
132
+ * "Reconnecting — please stand by" — a promise the player keeps while
133
+ * {@link rejoinOnLive} is on (the default; it also needs
134
+ * {@link liveStateEvents}). Override it if you disable auto-rejoin, since
135
+ * nothing will then re-attach on its own.
136
+ */
137
+ interruptedMessage?: string;
128
138
  /** Message on the fatal-error card. Default: the error's own message. */
129
139
  errorMessage?: string;
130
140
  /**
@@ -304,6 +314,22 @@ export declare interface EmbedHandle {
304
314
  destroy(): void;
305
315
  }
306
316
 
317
+ /**
318
+ * Why a stream ended (havik-streams #191). `ended` alone cannot tell a viewer
319
+ * whether the MATCH finished or only its stream dropped — two situations that
320
+ * deserve opposite messaging:
321
+ *
322
+ * - `interrupted` — the match is still running; no session is serving (an
323
+ * ingest outage that outlived the bridge's reconnect grace). Play is
324
+ * expected to resume, so surface "reconnecting", never "this has ended".
325
+ * - `match_ended` — the match itself finished. A real end.
326
+ *
327
+ * Absent on every other state, and absent entirely from servers predating the
328
+ * field — so treat `undefined` as "unknown, assume a real end" (what clients
329
+ * did before it existed).
330
+ */
331
+ export declare type EndedReason = 'interrupted' | 'match_ended';
332
+
307
333
  /**
308
334
  * Enter fullscreen for `video`, walking the capability ladder above.
309
335
  * `container` is the element to present in element fullscreen (defaults to
@@ -525,6 +551,8 @@ export declare type LiveState = 'live' | 'upcoming' | 'ended' | 'gone';
525
551
  export declare interface LiveStateEvent {
526
552
  matchUrn: string;
527
553
  state: LiveState;
554
+ /** Set only on `ended`; see {@link EndedReason}. */
555
+ reason?: EndedReason;
528
556
  /** RFC3339 server timestamp the transition was stamped at. */
529
557
  serverTime: string;
530
558
  }
@@ -647,6 +675,9 @@ export declare interface PlaybackStats {
647
675
  export declare interface Player {
648
676
  readonly state: PlayerState;
649
677
  readonly descriptor: StreamDescriptor | null;
678
+ /** Why the stream ended, when the server said so; null otherwise. Cleared
679
+ * when fresh playback commits (including a rejoin). See EndedReason. */
680
+ readonly endedReason: EndedReason | null;
650
681
  play(): Promise<void>;
651
682
  pause(): void;
652
683
  setMuted(muted: boolean): void;
@@ -696,7 +727,15 @@ export declare interface PlayerEventMap {
696
727
  playing: void;
697
728
  buffering: void;
698
729
  paused: void;
699
- ended: void;
730
+ /**
731
+ * End of stream. The payload says WHY when the server told us
732
+ * (`interrupted` = the match is still running and play should resume, so
733
+ * do NOT say "ended"; `match_ended` = a real end); `undefined` means the
734
+ * end came from the HLS staleness watchdog or a server predating the
735
+ * field — treat it as a real end, the pre-existing behavior. Fires again
736
+ * with `match_ended` if an interruption later turns out to be the end.
737
+ */
738
+ ended: EndedReason | undefined;
700
739
  error: PlaybackError;
701
740
  /** Non-fatal condition (e.g. FairPlay-not-implemented passthrough, a failed license refresh). Playback continues. */
702
741
  warning: PlaybackError;
package/dist/index.d.ts CHANGED
@@ -125,6 +125,16 @@ export declare interface CreatePlayerOptions {
125
125
  statusOverlays?: boolean;
126
126
  /** Message on the end-of-stream card. Default: "This stream has ended". */
127
127
  endedMessage?: string;
128
+ /**
129
+ * Message shown instead of {@link endedMessage} when the stream stopped but
130
+ * the match is still running (the server's `interrupted` reason — an ingest
131
+ * outage that outlived the bridge's reconnect hold). Default:
132
+ * "Reconnecting — please stand by" — a promise the player keeps while
133
+ * {@link rejoinOnLive} is on (the default; it also needs
134
+ * {@link liveStateEvents}). Override it if you disable auto-rejoin, since
135
+ * nothing will then re-attach on its own.
136
+ */
137
+ interruptedMessage?: string;
128
138
  /** Message on the fatal-error card. Default: the error's own message. */
129
139
  errorMessage?: string;
130
140
  /**
@@ -304,6 +314,22 @@ export declare interface EmbedHandle {
304
314
  destroy(): void;
305
315
  }
306
316
 
317
+ /**
318
+ * Why a stream ended (havik-streams #191). `ended` alone cannot tell a viewer
319
+ * whether the MATCH finished or only its stream dropped — two situations that
320
+ * deserve opposite messaging:
321
+ *
322
+ * - `interrupted` — the match is still running; no session is serving (an
323
+ * ingest outage that outlived the bridge's reconnect grace). Play is
324
+ * expected to resume, so surface "reconnecting", never "this has ended".
325
+ * - `match_ended` — the match itself finished. A real end.
326
+ *
327
+ * Absent on every other state, and absent entirely from servers predating the
328
+ * field — so treat `undefined` as "unknown, assume a real end" (what clients
329
+ * did before it existed).
330
+ */
331
+ export declare type EndedReason = 'interrupted' | 'match_ended';
332
+
307
333
  /**
308
334
  * Enter fullscreen for `video`, walking the capability ladder above.
309
335
  * `container` is the element to present in element fullscreen (defaults to
@@ -525,6 +551,8 @@ export declare type LiveState = 'live' | 'upcoming' | 'ended' | 'gone';
525
551
  export declare interface LiveStateEvent {
526
552
  matchUrn: string;
527
553
  state: LiveState;
554
+ /** Set only on `ended`; see {@link EndedReason}. */
555
+ reason?: EndedReason;
528
556
  /** RFC3339 server timestamp the transition was stamped at. */
529
557
  serverTime: string;
530
558
  }
@@ -647,6 +675,9 @@ export declare interface PlaybackStats {
647
675
  export declare interface Player {
648
676
  readonly state: PlayerState;
649
677
  readonly descriptor: StreamDescriptor | null;
678
+ /** Why the stream ended, when the server said so; null otherwise. Cleared
679
+ * when fresh playback commits (including a rejoin). See EndedReason. */
680
+ readonly endedReason: EndedReason | null;
650
681
  play(): Promise<void>;
651
682
  pause(): void;
652
683
  setMuted(muted: boolean): void;
@@ -696,7 +727,15 @@ export declare interface PlayerEventMap {
696
727
  playing: void;
697
728
  buffering: void;
698
729
  paused: void;
699
- ended: void;
730
+ /**
731
+ * End of stream. The payload says WHY when the server told us
732
+ * (`interrupted` = the match is still running and play should resume, so
733
+ * do NOT say "ended"; `match_ended` = a real end); `undefined` means the
734
+ * end came from the HLS staleness watchdog or a server predating the
735
+ * field — treat it as a real end, the pre-existing behavior. Fires again
736
+ * with `match_ended` if an interruption later turns out to be the end.
737
+ */
738
+ ended: EndedReason | undefined;
700
739
  error: PlaybackError;
701
740
  /** Non-fatal condition (e.g. FairPlay-not-implemented passthrough, a failed license refresh). Playback continues. */
702
741
  warning: PlaybackError;
package/dist/index.js CHANGED
@@ -1408,7 +1408,7 @@ var qe = class {
1408
1408
  keepalive: !0
1409
1409
  }).catch(() => {});
1410
1410
  }
1411
- }, Je = "1.17.0", W = {
1411
+ }, Je = "1.18.0", W = {
1412
1412
  accent: "#E1B600",
1413
1413
  accentText: "#1B1C23",
1414
1414
  background: "#0e0f13",
@@ -1505,18 +1505,20 @@ var tt = class {
1505
1505
  menu;
1506
1506
  statusBox = null;
1507
1507
  statusKind = null;
1508
+ statusMsg = null;
1508
1509
  hasPlayed = !1;
1509
1510
  lastError = null;
1510
1511
  logoUrl;
1511
1512
  statusOverlays;
1512
1513
  endedMessage;
1514
+ interruptedMessage;
1513
1515
  errorMessage;
1514
1516
  hideTimer;
1515
1517
  openMenu = null;
1516
1518
  scrubbing = !1;
1517
1519
  offFns = [];
1518
1520
  constructor(e) {
1519
- this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? Qe, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.errorMessage = e.errorMessage, Xe(this.doc), this.root.classList.add("havik-player"), this.root.hasAttribute("tabindex") || (this.root.tabIndex = 0), this.root.setAttribute("role", "region"), this.root.setAttribute("aria-label", "Video player");
1521
+ this.root = e.root, this.video = e.video, this.player = e.player, this.doc = e.root.ownerDocument ?? document, this.autoHideMs = e.autoHideMs ?? Qe, this.logoUrl = e.logoUrl, this.statusOverlays = e.statusOverlays !== !1, this.endedMessage = e.endedMessage ?? "This stream has ended", this.interruptedMessage = e.interruptedMessage ?? "Reconnecting — please stand by", this.errorMessage = e.errorMessage, Xe(this.doc), this.root.classList.add("havik-player"), this.root.hasAttribute("tabindex") || (this.root.tabIndex = 0), this.root.setAttribute("role", "region"), this.root.setAttribute("aria-label", "Video player");
1520
1522
  let t = (e, t, n) => {
1521
1523
  let r = this.doc.createElement(e);
1522
1524
  if (r.className = t, n) for (let [e, t] of Object.entries(n)) r.setAttribute(e, t);
@@ -1570,7 +1572,7 @@ var tt = class {
1570
1572
  }), this.on(this.muteBtn, "click", () => this.player.setMuted(!e.muted)), this.on(this.volSlider, "input", () => {
1571
1573
  let t = Number(this.volSlider.value);
1572
1574
  this.setVolumeFill(t), this.player.setVolume(t), t > 0 && e.muted && this.player.setMuted(!1);
1573
- }), this.on(this.liveGo, "click", () => this.player.seekToLive()), this.on(this.pipBtn, "click", () => void this.togglePip()), this.on(this.fsBtn, "click", () => void this.toggleFullscreen()), this.on(this.ccBtn, "click", () => this.toggleMenu("cc")), this.on(this.audioBtn, "click", () => this.toggleMenu("audio")), this.on(this.qualityBtn, "click", () => this.toggleMenu("quality")), this.on(this.seek, "pointerdown", (e) => this.onSeekDown(e)), this.on(this.seek, "keydown", (e) => this.onSeekKey(e)), this.on(e, "play", () => this.syncPlayPause()), this.on(e, "pause", () => this.syncPlayPause()), this.on(e, "volumechange", () => this.syncVolume()), this.on(e, "timeupdate", () => this.syncProgress()), this.on(e, "progress", () => this.syncProgress()), this.on(e, "durationchange", () => this.syncProgress()), this.offFns.push(this.player.on("statechange", () => this.syncState())), this.offFns.push(this.player.on("ready", () => this.refreshTracks())), this.offFns.push(this.player.on("qualitychange", () => this.onTracksChanged())), this.offFns.push(this.player.on("audiotrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("texttrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("pipchange", (e) => this.syncPip(e))), this.offFns.push(this.player.on("stats", () => this.syncLive())), this.offFns.push(this.player.on("error", (e) => {
1575
+ }), this.on(this.liveGo, "click", () => this.player.seekToLive()), this.on(this.pipBtn, "click", () => void this.togglePip()), this.on(this.fsBtn, "click", () => void this.toggleFullscreen()), this.on(this.ccBtn, "click", () => this.toggleMenu("cc")), this.on(this.audioBtn, "click", () => this.toggleMenu("audio")), this.on(this.qualityBtn, "click", () => this.toggleMenu("quality")), this.on(this.seek, "pointerdown", (e) => this.onSeekDown(e)), this.on(this.seek, "keydown", (e) => this.onSeekKey(e)), this.on(e, "play", () => this.syncPlayPause()), this.on(e, "pause", () => this.syncPlayPause()), this.on(e, "volumechange", () => this.syncVolume()), this.on(e, "timeupdate", () => this.syncProgress()), this.on(e, "progress", () => this.syncProgress()), this.on(e, "durationchange", () => this.syncProgress()), this.offFns.push(this.player.on("statechange", () => this.syncState())), this.offFns.push(this.player.on("ended", () => this.syncState())), this.offFns.push(this.player.on("ready", () => this.refreshTracks())), this.offFns.push(this.player.on("qualitychange", () => this.onTracksChanged())), this.offFns.push(this.player.on("audiotrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("texttrackchange", () => this.onTracksChanged())), this.offFns.push(this.player.on("pipchange", (e) => this.syncPip(e))), this.offFns.push(this.player.on("stats", () => this.syncLive())), this.offFns.push(this.player.on("error", (e) => {
1574
1576
  this.lastError = e, this.statusKind = null, this.syncState();
1575
1577
  })), this.on(this.root, "pointermove", () => this.poke()), this.on(this.root, "pointerleave", () => this.scheduleHide(0)), this.on(this.root, "focusin", () => this.poke()), this.on(this.root, "keydown", (e) => this.onKey(e)), this.on(this.doc, "pointerdown", (e) => this.onDocPointer(e), { capture: !0 }), this.on(this.doc, "fullscreenchange", () => this.syncFullscreen()), this.on(this.doc, "webkitfullscreenchange", () => this.syncFullscreen()), this.on(e, "webkitbeginfullscreen", () => this.syncFullscreen()), this.on(e, "webkitendfullscreen", () => this.syncFullscreen()), this.syncState();
1576
1578
  }
@@ -1591,24 +1593,28 @@ var tt = class {
1591
1593
  let t = null;
1592
1594
  this.statusOverlays && (e === "ended" ? t = "ended" : e === "error" ? t = "error" : e === "waiting" ? t = "waiting" : !this.hasPlayed && (e === "idle" || e === "paused") && (t = "preplay")), this.renderStatus(t);
1593
1595
  }
1596
+ statusMessageFor(e) {
1597
+ return e === "ended" ? this.player.endedReason === "interrupted" ? this.interruptedMessage : this.endedMessage : e === "waiting" ? this.hasPlayed ? "Reconnecting — please stand by" : "Starting soon…" : e === "error" ? this.errorMessage ?? this.lastError?.message ?? "Playback unavailable" : null;
1598
+ }
1594
1599
  renderStatus(e) {
1595
- if (e === this.statusKind || (this.statusKind = e, this.statusBox?.remove(), this.statusBox = null, this.root.classList.toggle("havik-has-status", !!e), !e)) return;
1596
- let t = this.doc.createElement("div");
1597
- if (t.className = `havik-status havik-status--${e}`, this.logoUrl) {
1600
+ let t = this.statusMessageFor(e);
1601
+ if (e === this.statusKind && t === this.statusMsg || (this.statusKind = e, this.statusMsg = t, this.statusBox?.remove(), this.statusBox = null, this.root.classList.toggle("havik-has-status", !!e), !e)) return;
1602
+ let n = this.doc.createElement("div");
1603
+ if (n.className = `havik-status havik-status--${e}`, this.logoUrl) {
1598
1604
  let e = this.doc.createElement("div");
1599
- e.className = "havik-status__logo", t.append(e);
1605
+ e.className = "havik-status__logo", n.append(e);
1600
1606
  }
1601
1607
  if (e === "preplay") {
1602
1608
  let e = this.doc.createElement("button");
1603
- e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = J("play"), e.addEventListener("click", () => void this.player.play()), t.append(e);
1609
+ e.type = "button", e.className = "havik-status__btn havik-status__btn--play", e.setAttribute("aria-label", "Play"), e.innerHTML = J("play"), e.addEventListener("click", () => void this.player.play()), n.append(e);
1604
1610
  } else {
1605
- let n = this.doc.createElement("div");
1606
- if (n.className = "havik-status__msg", n.textContent = e === "ended" ? this.endedMessage : e === "waiting" ? this.hasPlayed ? "Reconnecting — please stand by" : "Starting soon…" : this.errorMessage ?? this.lastError?.message ?? "Playback unavailable", t.append(n), e === "error" && this.lastError && !$e.has(this.lastError.code)) {
1611
+ let r = this.doc.createElement("div");
1612
+ if (r.className = "havik-status__msg", r.textContent = t ?? "", n.append(r), e === "error" && this.lastError && !$e.has(this.lastError.code)) {
1607
1613
  let e = this.doc.createElement("button");
1608
- e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${J("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), t.append(e);
1614
+ e.type = "button", e.className = "havik-status__btn", e.innerHTML = `${J("retry")}<span>Retry</span>`, e.addEventListener("click", () => void this.player.retry()), n.append(e);
1609
1615
  }
1610
1616
  }
1611
- this.root.append(t), this.statusBox = t, this.show();
1617
+ this.root.append(n), this.statusBox = n, this.show();
1612
1618
  }
1613
1619
  syncVolume() {
1614
1620
  let e = this.video, t = e.muted || e.volume === 0;
@@ -1846,6 +1852,7 @@ var lt = class {
1846
1852
  loadGen = 0;
1847
1853
  liveStateSub = null;
1848
1854
  endedHandled = !1;
1855
+ _endedReason = null;
1849
1856
  rejoinTimer = null;
1850
1857
  armingSub = null;
1851
1858
  armSseTimer = null;
@@ -1862,6 +1869,9 @@ var lt = class {
1862
1869
  get descriptor() {
1863
1870
  return this._descriptor;
1864
1871
  }
1872
+ get endedReason() {
1873
+ return this._endedReason;
1874
+ }
1865
1875
  async start() {
1866
1876
  this.opts.muted && (this.opts.video.muted = !0), this.opts.poster && (this.opts.video.poster = this.opts.poster), this.mountControls(), this.attachPipListeners(), this.setState("loading");
1867
1877
  let e = ++this.loadGen, t = this.resolveOptions();
@@ -1936,7 +1946,13 @@ var lt = class {
1936
1946
  }));
1937
1947
  }, g = () => {
1938
1948
  a || p() || y(this.resolveOptions()).then((e) => m(e)).catch((e) => {
1939
- a || p() || e instanceof n && o(e.code) || (a = !0, this.closeArming(), c.abort(), this.fail(e instanceof n ? e : new n("INTERNAL", 0, String(e))));
1949
+ if (!(a || p())) {
1950
+ if (e instanceof n && o(e.code)) {
1951
+ h();
1952
+ return;
1953
+ }
1954
+ a = !0, this.closeArming(), c.abort(), this.fail(e instanceof n ? e : new n("INTERNAL", 0, String(e)));
1955
+ }
1940
1956
  });
1941
1957
  }, _ = (e) => {
1942
1958
  this.armSseTimer && clearTimeout(this.armSseTimer), this.armSseTimer = setTimeout(() => {
@@ -1968,7 +1984,7 @@ var lt = class {
1968
1984
  }
1969
1985
  async onResolved(e, t) {
1970
1986
  if (this.destroyed || t !== this.loadGen) return;
1971
- this._descriptor = e, this.endedHandled = !1;
1987
+ this._descriptor = e, this.endedHandled = !1, this._endedReason = null;
1972
1988
  let n = await s(this.opts.credential);
1973
1989
  if (this.destroyed || t !== this.loadGen) return;
1974
1990
  this.setState("loading");
@@ -2067,7 +2083,7 @@ var lt = class {
2067
2083
  matchUrn: this.opts.matchUrn,
2068
2084
  credential: this.opts.credential,
2069
2085
  onState: (t) => {
2070
- this.destroyed || e !== this.loadGen || (t.state === "gone" ? this.onStreamEnded(!0) : t.state === "ended" ? this.onStreamEnded() : t.state === "live" && this.endedHandled && this.scheduleRejoin(e));
2086
+ this.destroyed || e !== this.loadGen || (t.state === "gone" ? this.onStreamEnded(!0, t.reason ?? "match_ended") : t.state === "ended" ? this.onStreamEnded(!1, t.reason) : t.state === "live" && this.endedHandled && this.scheduleRejoin(e));
2071
2087
  },
2072
2088
  onAlive: () => {
2073
2089
  r = 0, i = !1;
@@ -2080,8 +2096,14 @@ var lt = class {
2080
2096
  closeLiveState() {
2081
2097
  this.liveStateSub?.close(), this.liveStateSub = null;
2082
2098
  }
2083
- onStreamEnded(e = !1) {
2084
- this.destroyed || ((e || !this.rejoinEnabled()) && (this.cancelRejoin(), this.closeLiveState()), !this.endedHandled && (this.endedHandled = !0, this.stopRefresh(), this.engine?.stop(), this.setState("ended")));
2099
+ onStreamEnded(e = !1, t) {
2100
+ if (!this.destroyed) {
2101
+ if ((e || !this.rejoinEnabled()) && (this.cancelRejoin(), this.closeLiveState()), this.endedHandled) {
2102
+ t === "match_ended" && this._endedReason !== "match_ended" && (this._endedReason = t, this.emit("ended", t));
2103
+ return;
2104
+ }
2105
+ this.endedHandled = !0, this._endedReason = t ?? null, this.stopRefresh(), this.engine?.stop(), this.setState("ended");
2106
+ }
2085
2107
  }
2086
2108
  rejoinEnabled() {
2087
2109
  return this.opts.rejoinOnLive !== !1 && this.opts.liveStateEvents !== !1;
@@ -2120,6 +2142,7 @@ var lt = class {
2120
2142
  logoUrl: this.opts.theme?.logoUrl,
2121
2143
  statusOverlays: this.opts.statusOverlays,
2122
2144
  endedMessage: this.opts.endedMessage,
2145
+ interruptedMessage: this.opts.interruptedMessage,
2123
2146
  errorMessage: this.opts.errorMessage
2124
2147
  });
2125
2148
  }
@@ -2277,7 +2300,7 @@ var lt = class {
2277
2300
  case "paused":
2278
2301
  this.emit("paused");
2279
2302
  break;
2280
- case "ended": this.emit("ended");
2303
+ case "ended": this.emit("ended", this._endedReason ?? void 0);
2281
2304
  }
2282
2305
  }
2283
2306
  emit(e, t) {