@growth-labs/video 0.3.0 → 0.3.2
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
|
@@ -36,7 +36,14 @@ const src = premium
|
|
|
36
36
|
? premiumManifestUrl(config.premium!.playbackRoute, videoId)
|
|
37
37
|
: manifestUrl(config.publicDomain, videoId)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Convention-URL fallback fires only when the caller omits `poster` entirely.
|
|
40
|
+
// Explicit `poster={null | undefined | ""}` is treated as "no poster" so that
|
|
41
|
+
// consumers passing through video metadata with a missing posterUrl don't get
|
|
42
|
+
// a 404'd /cdn-cgi/image/.../poster.jpg attribute (broken-image icon).
|
|
43
|
+
const poster =
|
|
44
|
+
'poster' in Astro.props
|
|
45
|
+
? Astro.props.poster || undefined
|
|
46
|
+
: posterUrl(config.publicDomain, videoId)
|
|
40
47
|
const chaptersSrc = showChapters ? chaptersUrl(config.publicDomain, videoId) : null
|
|
41
48
|
const captionsSrc = showCaptions
|
|
42
49
|
? captionsUrl(config.publicDomain, videoId, captionsLang)
|
|
@@ -47,12 +54,13 @@ const captionsSrc = showCaptions
|
|
|
47
54
|
data-video-id={videoId}
|
|
48
55
|
data-premium={premium ? 'true' : 'false'}
|
|
49
56
|
data-session-route={premium ? config.premium?.sessionRoute : undefined}
|
|
57
|
+
data-pending-src={premium ? src : undefined}
|
|
50
58
|
data-chapters-src={chaptersSrc}
|
|
51
59
|
data-captions-src={captionsSrc}
|
|
52
60
|
data-captions-lang={captionsLang}
|
|
53
61
|
data-progress-marks={JSON.stringify(config.analytics.progressMarks)}
|
|
54
62
|
title={title}
|
|
55
|
-
src={src}
|
|
63
|
+
src={premium ? undefined : src}
|
|
56
64
|
poster={poster}
|
|
57
65
|
preload={config.player.preload}
|
|
58
66
|
autoplay={Astro.props.autoplay ?? config.player.autoplay}
|
|
@@ -75,6 +83,8 @@ const captionsSrc = showCaptions
|
|
|
75
83
|
</media-player>
|
|
76
84
|
|
|
77
85
|
<script>
|
|
86
|
+
import { bootstrapPremiumPlayer } from './premium-bootstrap'
|
|
87
|
+
|
|
78
88
|
import('vidstack/elements').then((mod: any) => {
|
|
79
89
|
if (typeof mod.defineCustomElements === 'function') {
|
|
80
90
|
mod.defineCustomElements()
|
|
@@ -83,36 +93,16 @@ const captionsSrc = showCaptions
|
|
|
83
93
|
|
|
84
94
|
const VIDEO_ACCESS_DENIED = 'gl:video-access-denied'
|
|
85
95
|
|
|
96
|
+
// Premium players ship without `src` and with `data-pending-src` carrying
|
|
97
|
+
// the manifest URL. Establish the session cookie *before* Vidstack loads
|
|
98
|
+
// the manifest — otherwise preload="metadata" 401s on mount.
|
|
86
99
|
document.querySelectorAll<HTMLElement>('media-player[data-premium="true"]').forEach((player) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!videoId || !sessionRoute) return
|
|
90
|
-
|
|
91
|
-
let sessionChecked = false
|
|
92
|
-
const ensure = async () => {
|
|
93
|
-
if (sessionChecked) return
|
|
94
|
-
sessionChecked = true
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch(sessionRoute, {
|
|
97
|
-
method: 'POST',
|
|
98
|
-
credentials: 'same-origin',
|
|
99
|
-
headers: { 'Content-Type': 'application/json' },
|
|
100
|
-
body: JSON.stringify({ videoId }),
|
|
101
|
-
})
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
window.dispatchEvent(
|
|
104
|
-
new CustomEvent(VIDEO_ACCESS_DENIED, { detail: { videoId } }),
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
100
|
+
bootstrapPremiumPlayer(player as unknown as Parameters<typeof bootstrapPremiumPlayer>[0], {
|
|
101
|
+
emitAccessDenied: (videoId) =>
|
|
108
102
|
window.dispatchEvent(
|
|
109
103
|
new CustomEvent(VIDEO_ACCESS_DENIED, { detail: { videoId } }),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
player.addEventListener('pointerenter', ensure, { once: true })
|
|
115
|
-
player.addEventListener('play', ensure)
|
|
104
|
+
),
|
|
105
|
+
})
|
|
116
106
|
})
|
|
117
107
|
|
|
118
108
|
const emit = (name: string, detail: unknown) =>
|
|
@@ -6,6 +6,7 @@ interface Item {
|
|
|
6
6
|
title: string
|
|
7
7
|
href: string
|
|
8
8
|
durationSeconds?: number
|
|
9
|
+
poster?: string | null
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
@@ -14,16 +15,24 @@ interface Props {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const { items, class: className } = Astro.props
|
|
18
|
+
// Only forward `poster` to VideoPreview when the item literal carries the key.
|
|
19
|
+
// Preserves VideoPreview's discriminator: items without a `poster` key fall
|
|
20
|
+
// back to the convention URL; items with `poster: null | undefined | ""`
|
|
21
|
+
// explicitly opt out of any <img> render.
|
|
22
|
+
const previewItems = items.map((item) => ({
|
|
23
|
+
common: {
|
|
24
|
+
videoId: item.videoId,
|
|
25
|
+
title: item.title,
|
|
26
|
+
href: item.href,
|
|
27
|
+
durationSeconds: item.durationSeconds,
|
|
28
|
+
},
|
|
29
|
+
posterProps: ('poster' in item ? { poster: item.poster } : {}) as { poster?: string | null },
|
|
30
|
+
}))
|
|
17
31
|
---
|
|
18
32
|
|
|
19
33
|
<div class={`gl-video-gallery ${className ?? ''}`.trim()}>
|
|
20
|
-
{
|
|
21
|
-
<VideoPreview
|
|
22
|
-
videoId={item.videoId}
|
|
23
|
-
title={item.title}
|
|
24
|
-
href={item.href}
|
|
25
|
-
durationSeconds={item.durationSeconds}
|
|
26
|
-
/>
|
|
34
|
+
{previewItems.map(({ common, posterProps }) => (
|
|
35
|
+
<VideoPreview {...common} {...posterProps} />
|
|
27
36
|
))}
|
|
28
37
|
</div>
|
|
29
38
|
|
|
@@ -7,18 +7,26 @@ interface Props {
|
|
|
7
7
|
title: string
|
|
8
8
|
href: string
|
|
9
9
|
durationSeconds?: number
|
|
10
|
+
poster?: string | null
|
|
10
11
|
class?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const { videoId, title, href, durationSeconds, class: className } = Astro.props
|
|
14
|
-
|
|
15
|
+
// Convention-URL fallback fires only when the caller omits `poster` entirely.
|
|
16
|
+
// Explicit `poster={null | undefined | ""}` is treated as "no poster" so that
|
|
17
|
+
// gallery items passing through video metadata with a missing posterUrl don't
|
|
18
|
+
// render a 404'd /cdn-cgi/image/.../poster.jpg <img> (broken-image icon).
|
|
19
|
+
const poster =
|
|
20
|
+
'poster' in Astro.props
|
|
21
|
+
? Astro.props.poster || undefined
|
|
22
|
+
: posterUrl(config.publicDomain, videoId)
|
|
15
23
|
const durationLabel = durationSeconds
|
|
16
24
|
? `${Math.floor(durationSeconds / 60)}:${String(durationSeconds % 60).padStart(2, '0')}`
|
|
17
25
|
: null
|
|
18
26
|
---
|
|
19
27
|
|
|
20
28
|
<a class={`gl-video-preview ${className ?? ''}`.trim()} href={href} aria-label={title}>
|
|
21
|
-
<img src={poster} alt="" loading="lazy" decoding="async" />
|
|
29
|
+
{poster && <img src={poster} alt="" loading="lazy" decoding="async" />}
|
|
22
30
|
<span aria-hidden="true" class="gl-video-preview-play">▶</span>
|
|
23
31
|
<span class="gl-video-preview-title">{title}</span>
|
|
24
32
|
{durationLabel ? <span class="gl-video-preview-duration">{durationLabel}</span> : null}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface PremiumPlayerLike {
|
|
2
|
+
dataset: { videoId?: string; sessionRoute?: string; pendingSrc?: string }
|
|
3
|
+
src?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface BootstrapOptions {
|
|
7
|
+
fetch?: typeof fetch
|
|
8
|
+
emitAccessDenied?: (videoId: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function bootstrapPremiumPlayer(
|
|
12
|
+
player: PremiumPlayerLike,
|
|
13
|
+
opts: BootstrapOptions = {},
|
|
14
|
+
): Promise<'authorized' | 'denied' | 'skipped'> {
|
|
15
|
+
const { videoId, sessionRoute, pendingSrc } = player.dataset
|
|
16
|
+
if (!videoId || !sessionRoute) return 'skipped'
|
|
17
|
+
|
|
18
|
+
const f = opts.fetch ?? fetch
|
|
19
|
+
let ok = false
|
|
20
|
+
try {
|
|
21
|
+
const res = await f(sessionRoute, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
credentials: 'same-origin',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ videoId }),
|
|
26
|
+
})
|
|
27
|
+
ok = res.ok
|
|
28
|
+
} catch {
|
|
29
|
+
ok = false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (ok) {
|
|
33
|
+
if (pendingSrc) player.src = pendingSrc
|
|
34
|
+
return 'authorized'
|
|
35
|
+
}
|
|
36
|
+
opts.emitAccessDenied?.(videoId)
|
|
37
|
+
return 'denied'
|
|
38
|
+
}
|