@growth-labs/video 0.3.8 → 0.3.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/video",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -94,17 +94,22 @@ const playerBoolAttrs = buildPlayerBooleanAttrs({
94
94
  <script>
95
95
  import { bootstrapPremiumPlayer } from './premium-bootstrap'
96
96
 
97
- import('vidstack/elements').then((mod: any) => {
98
- if (typeof mod.defineCustomElements === 'function') {
99
- mod.defineCustomElements()
100
- }
101
- })
97
+ // Vidstack registration is the consumer's responsibility they choose
98
+ // which player bundle to ship (`vidstack/player` vs. `vidstack/elements`
99
+ // vs. a per-component define) and which layouts to register. The
100
+ // bootstrap below uses `customElements.whenDefined('media-player')`
101
+ // internally to wait for whatever registration path the consumer picked,
102
+ // so we no longer fire-and-forget a load of `vidstack/elements` here
103
+ // (it has no `defineCustomElements` export anyway — that was dead code).
102
104
 
103
105
  const VIDEO_ACCESS_DENIED = 'gl:video-access-denied'
104
106
 
105
107
  // Premium players ship without `src` and with `data-pending-src` carrying
106
108
  // the manifest URL. Establish the session cookie *before* Vidstack loads
107
- // the manifest — otherwise preload="metadata" 401s on mount.
109
+ // the manifest — otherwise preload="metadata" 401s on mount. The
110
+ // bootstrap is single-flight (consumes `data-pending-src` on entry) so
111
+ // re-invocation (e.g. from a view-transition `astro:after-swap` re-running
112
+ // page scripts) is a no-op and never tears down an in-flight HLS load.
108
113
  document.querySelectorAll<HTMLElement>('media-player[data-premium="true"]').forEach((player) => {
109
114
  bootstrapPremiumPlayer(player as unknown as Parameters<typeof bootstrapPremiumPlayer>[0], {
110
115
  emitAccessDenied: (videoId) =>
@@ -1,11 +1,30 @@
1
1
  export interface PremiumPlayerLike {
2
2
  dataset: { videoId?: string; sessionRoute?: string; pendingSrc?: string }
3
3
  src?: string
4
+ tagName?: string
4
5
  }
5
6
 
6
7
  export interface BootstrapOptions {
7
8
  fetch?: typeof fetch
8
9
  emitAccessDenied?: (videoId: string) => void
10
+ /**
11
+ * Hook used by tests to bypass `customElements.whenDefined('media-player')`.
12
+ * In production the bootstrap awaits the upgrade so `player.src` (a Vidstack
13
+ * property setter installed during upgrade) is in place before assignment.
14
+ */
15
+ waitForUpgrade?: (tagName: string) => Promise<void>
16
+ }
17
+
18
+ /**
19
+ * Default `waitForUpgrade` — resolves once `customElements.whenDefined()` for
20
+ * the player tag has run. Resolves immediately if `customElements` is missing
21
+ * (SSR / non-browser tests) so the bootstrap is still callable.
22
+ */
23
+ function defaultWaitForUpgrade(tagName: string): Promise<void> {
24
+ if (typeof globalThis === 'undefined') return Promise.resolve()
25
+ const ce = (globalThis as { customElements?: CustomElementRegistry }).customElements
26
+ if (!ce || typeof ce.whenDefined !== 'function') return Promise.resolve()
27
+ return ce.whenDefined(tagName).then(() => undefined)
9
28
  }
10
29
 
11
30
  export async function bootstrapPremiumPlayer(
@@ -15,6 +34,19 @@ export async function bootstrapPremiumPlayer(
15
34
  const { videoId, sessionRoute, pendingSrc } = player.dataset
16
35
  if (!videoId || !sessionRoute) return 'skipped'
17
36
 
37
+ // Single-flight guard. The bootstrap is supposed to run exactly once per
38
+ // premium <media-player>, but consumers can re-run page scripts on
39
+ // view-transition swaps (`astro:after-swap`), or a defensive consumer can
40
+ // call into the bootstrap from multiple lifecycle hooks. A second
41
+ // `player.src = pendingSrc` after Vidstack has attached the HLS provider
42
+ // tears the provider down — the in-flight master.m3u8 / playlist.m3u8 XHRs
43
+ // abort, the player is left without a source, and `<media-quality-radio-
44
+ // group>` reports zero qualities so the picker disappears. Consuming
45
+ // `data-pending-src` makes the function a no-op on every call after the
46
+ // first authorized one, and prevents the race regardless of how many times
47
+ // the host script invokes it.
48
+ delete player.dataset.pendingSrc
49
+
18
50
  const f = opts.fetch ?? fetch
19
51
  let ok = false
20
52
  try {
@@ -30,7 +62,21 @@ export async function bootstrapPremiumPlayer(
30
62
  }
31
63
 
32
64
  if (ok) {
33
- if (pendingSrc) player.src = pendingSrc
65
+ if (pendingSrc) {
66
+ // Wait for Vidstack to upgrade <media-player>. The `src` getter/setter
67
+ // is installed during element upgrade; assigning to `player.src`
68
+ // before then writes a plain JS property that the Vidstack provider
69
+ // never reads, so the manifest URL is silently lost and the player
70
+ // stays inert. `customElements.whenDefined('media-player')` resolves
71
+ // the moment `vidstack/player`'s `defineCustomElement(MediaPlayer
72
+ // Element)` has run; in practice this is the same microtask as the
73
+ // session POST returns. The await is virtually free on a warm page
74
+ // and prevents the lost-source race on a cold one.
75
+ const tagName = (player.tagName ?? 'media-player').toLowerCase()
76
+ const waitForUpgrade = opts.waitForUpgrade ?? defaultWaitForUpgrade
77
+ await waitForUpgrade(tagName)
78
+ player.src = pendingSrc
79
+ }
34
80
  return 'authorized'
35
81
  }
36
82
  opts.emitAccessDenied?.(videoId)