@growth-labs/video 0.3.8 → 0.3.10
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/README.md +3 -3
- package/dist/utils/keys.js +2 -2
- package/dist/utils/keys.js.map +1 -1
- package/dist/utils/url.js +2 -2
- package/dist/utils/url.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Video.astro +11 -6
- package/src/components/premium-bootstrap.ts +47 -1
- package/src/utils/keys.ts +2 -2
- package/src/utils/url.ts +2 -2
package/README.md
CHANGED
|
@@ -88,8 +88,8 @@ video/<videoId>/hls/<rendition>/index.m3u8
|
|
|
88
88
|
video/<videoId>/hls/<rendition>/init.mp4
|
|
89
89
|
video/<videoId>/hls/<rendition>/segment-00001.m4s
|
|
90
90
|
video/<videoId>/poster.jpg
|
|
91
|
-
video/<videoId>/captions_en.vtt
|
|
92
|
-
video/<videoId>/chapters.vtt
|
|
91
|
+
video/<videoId>/text/captions_en.vtt
|
|
92
|
+
video/<videoId>/text/chapters.vtt
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
## Ingest Helpers
|
|
@@ -116,6 +116,6 @@ await writeChaptersVtt({ bucket: env.MEDIA_BUCKET, videoId: 'intro-video', vtt:
|
|
|
116
116
|
|
|
117
117
|
The ingestion helper validates `master.m3u8`, referenced rendition playlists, segment paths, and local HLS `URI="..."` attributes such as `EXT-X-MAP`, `EXT-X-MEDIA`, `EXT-X-I-FRAME-STREAM-INF`, and `EXT-X-KEY` before writing to R2. Absolute/external URLs and unsafe relative paths are rejected. Transcoding is out of scope; pass already-generated HLS and VTT files.
|
|
118
118
|
|
|
119
|
-
All R2 key helpers normalize `videoId` with the same safe contract: IDs must start with an ASCII letter or number and then contain only ASCII letters, numbers, `_`, or `-`. Text-track helpers write only under `video/<videoId>/captions_<lang>.vtt` and `video/<videoId>/chapters.vtt`.
|
|
119
|
+
All R2 key helpers normalize `videoId` with the same safe contract: IDs must start with an ASCII letter or number and then contain only ASCII letters, numbers, `_`, or `-`. Text-track helpers write only under `video/<videoId>/text/captions_<lang>.vtt` and `video/<videoId>/text/chapters.vtt`.
|
|
120
120
|
|
|
121
121
|
Injected routes read bindings from `cloudflare:workers` `env`; no `locals.runtime` shim is required.
|
package/dist/utils/keys.js
CHANGED
|
@@ -19,11 +19,11 @@ export function captionsKey(videoId, lang = 'en') {
|
|
|
19
19
|
.trim()
|
|
20
20
|
.toLowerCase()
|
|
21
21
|
.replace(/[^a-z0-9-]/g, '_') || 'en';
|
|
22
|
-
return `video/${safeVideoId}/captions_${normalized}.vtt`;
|
|
22
|
+
return `video/${safeVideoId}/text/captions_${normalized}.vtt`;
|
|
23
23
|
}
|
|
24
24
|
export function chaptersKey(videoId) {
|
|
25
25
|
const safeVideoId = normalizeVideoId(videoId);
|
|
26
|
-
return `video/${safeVideoId}/chapters.vtt`;
|
|
26
|
+
return `video/${safeVideoId}/text/chapters.vtt`;
|
|
27
27
|
}
|
|
28
28
|
export function chaptersJsonKey(videoId) {
|
|
29
29
|
const safeVideoId = normalizeVideoId(videoId);
|
package/dist/utils/keys.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/utils/keys.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IACjC,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,UAAU,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,kBAAkB,CAAA;AAC9C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe;IACxC,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,aAAa,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAe,IAAI;IAC/D,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,UAAU,GACf,IAAI;SACF,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,IAAI,IAAI,CAAA;IACtC,OAAO,SAAS,WAAW,
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/utils/keys.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IACjC,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,UAAU,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,kBAAkB,CAAA;AAC9C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe;IACxC,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,aAAa,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAe,IAAI;IAC/D,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,UAAU,GACf,IAAI;SACF,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,IAAI,IAAI,CAAA;IACtC,OAAO,SAAS,WAAW,kBAAkB,UAAU,MAAM,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe;IAC1C,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,oBAAoB,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe;IAC9C,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,gBAAgB,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe;IAC9C,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,OAAO,SAAS,WAAW,OAAO,CAAA;AACnC,CAAC"}
|
package/dist/utils/url.js
CHANGED
|
@@ -9,10 +9,10 @@ export function captionsUrl(publicDomain, videoId, lang = 'en') {
|
|
|
9
9
|
.trim()
|
|
10
10
|
.toLowerCase()
|
|
11
11
|
.replace(/[^a-z0-9-]/g, '_') || 'en';
|
|
12
|
-
return `https://${publicDomain}/video/${videoId}/captions_${normalized}.vtt`;
|
|
12
|
+
return `https://${publicDomain}/video/${videoId}/text/captions_${normalized}.vtt`;
|
|
13
13
|
}
|
|
14
14
|
export function chaptersUrl(publicDomain, videoId) {
|
|
15
|
-
return `https://${publicDomain}/video/${videoId}/chapters.vtt`;
|
|
15
|
+
return `https://${publicDomain}/video/${videoId}/text/chapters.vtt`;
|
|
16
16
|
}
|
|
17
17
|
export function chaptersJsonUrl(publicDomain, videoId) {
|
|
18
18
|
return `https://${publicDomain}/video/${videoId}/chapters.json`;
|
package/dist/utils/url.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,OAAe;IAChE,OAAO,WAAW,YAAY,UAAU,OAAO,kBAAkB,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,YAAoB,EAAE,OAAe;IAC9D,OAAO,WAAW,YAAY,+EAA+E,OAAO,aAAa,CAAA;AAClI,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,OAAe,EAAE,OAAe,IAAI;IACrF,MAAM,UAAU,GACf,IAAI;SACF,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,IAAI,IAAI,CAAA;IACtC,OAAO,WAAW,YAAY,UAAU,OAAO,
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,OAAe;IAChE,OAAO,WAAW,YAAY,UAAU,OAAO,kBAAkB,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,YAAoB,EAAE,OAAe;IAC9D,OAAO,WAAW,YAAY,+EAA+E,OAAO,aAAa,CAAA;AAClI,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,OAAe,EAAE,OAAe,IAAI;IACrF,MAAM,UAAU,GACf,IAAI;SACF,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,IAAI,IAAI,CAAA;IACtC,OAAO,WAAW,YAAY,UAAU,OAAO,kBAAkB,UAAU,MAAM,CAAA;AAClF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,OAAe;IAChE,OAAO,WAAW,YAAY,UAAU,OAAO,oBAAoB,CAAA;AACpE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,YAAoB,EAAE,OAAe;IACpE,OAAO,WAAW,YAAY,UAAU,OAAO,gBAAgB,CAAA;AAChE,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,aAAqB,EAAE,OAAe;IACxE,OAAO,GAAG,cAAc,CAAC,aAAa,CAAC,IAAI,OAAO,kBAAkB,CAAA;AACrE,CAAC;AAED,MAAM,UAAU,kBAAkB,CACjC,aAAqB,EACrB,OAAe,EACf,OAAe;IAEf,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;IACpE,OAAO,GAAG,cAAc,CAAC,aAAa,CAAC,IAAI,OAAO,IAAI,OAAO,EAAE,CAAA;AAChE,CAAC"}
|
package/package.json
CHANGED
|
@@ -94,17 +94,22 @@ const playerBoolAttrs = buildPlayerBooleanAttrs({
|
|
|
94
94
|
<script>
|
|
95
95
|
import { bootstrapPremiumPlayer } from './premium-bootstrap'
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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)
|
|
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)
|
package/src/utils/keys.ts
CHANGED
|
@@ -23,12 +23,12 @@ export function captionsKey(videoId: string, lang: string = 'en'): string {
|
|
|
23
23
|
.trim()
|
|
24
24
|
.toLowerCase()
|
|
25
25
|
.replace(/[^a-z0-9-]/g, '_') || 'en'
|
|
26
|
-
return `video/${safeVideoId}/captions_${normalized}.vtt`
|
|
26
|
+
return `video/${safeVideoId}/text/captions_${normalized}.vtt`
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function chaptersKey(videoId: string): string {
|
|
30
30
|
const safeVideoId = normalizeVideoId(videoId)
|
|
31
|
-
return `video/${safeVideoId}/chapters.vtt`
|
|
31
|
+
return `video/${safeVideoId}/text/chapters.vtt`
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function chaptersJsonKey(videoId: string): string {
|
package/src/utils/url.ts
CHANGED
|
@@ -12,11 +12,11 @@ export function captionsUrl(publicDomain: string, videoId: string, lang: string
|
|
|
12
12
|
.trim()
|
|
13
13
|
.toLowerCase()
|
|
14
14
|
.replace(/[^a-z0-9-]/g, '_') || 'en'
|
|
15
|
-
return `https://${publicDomain}/video/${videoId}/captions_${normalized}.vtt`
|
|
15
|
+
return `https://${publicDomain}/video/${videoId}/text/captions_${normalized}.vtt`
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function chaptersUrl(publicDomain: string, videoId: string): string {
|
|
19
|
-
return `https://${publicDomain}/video/${videoId}/chapters.vtt`
|
|
19
|
+
return `https://${publicDomain}/video/${videoId}/text/chapters.vtt`
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function chaptersJsonUrl(publicDomain: string, videoId: string): string {
|