@munsonlabs/video-player 0.2.0 → 0.2.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/README.md +180 -317
- package/dist/chunks/ads-CF9PWPJJ.mjs +1 -0
- package/dist/{elements/chunks/brightcove-CIqE1989.mjs → chunks/brightcove-DUITad7n.mjs} +1 -1
- package/dist/{elements/chunks/constants-DRD0PsRw.mjs → chunks/constants-BUQXC_ZT.mjs} +1 -1
- package/dist/{elements/chunks/dailymotion-wD02TK0C.mjs → chunks/dailymotion-CwCwD6rN.mjs} +1 -1
- package/dist/chunks/embedShared-DgFZ58OV.mjs +1 -0
- package/dist/chunks/{jwplayer-Dv826deA.mjs → jwplayer-BxpEGj9-.mjs} +1 -1
- package/dist/chunks/platform-Dg9JBh8M.mjs +1 -0
- package/dist/chunks/{sourceHelpers-DlkCZiTJ.mjs → sourceHelpers-CAGxjbbu.mjs} +1 -1
- package/dist/chunks/{vimeo-SrHhodcx.mjs → vimeo-BNsFVvtc.mjs} +1 -1
- package/dist/chunks/{youtube-Boz9Goek.mjs → youtube-B7s74nf6.mjs} +1 -1
- package/dist/elements/chunks/ads-CF9PWPJJ.mjs +1 -0
- package/dist/{chunks/brightcove-CIqE1989.mjs → elements/chunks/brightcove-DUITad7n.mjs} +1 -1
- package/dist/{chunks/constants-DRD0PsRw.mjs → elements/chunks/constants-BUQXC_ZT.mjs} +1 -1
- package/dist/{chunks/dailymotion-wD02TK0C.mjs → elements/chunks/dailymotion-CwCwD6rN.mjs} +1 -1
- package/dist/elements/chunks/embedShared-DgFZ58OV.mjs +1 -0
- package/dist/elements/chunks/{jwplayer-Dv826deA.mjs → jwplayer-BxpEGj9-.mjs} +1 -1
- package/dist/elements/chunks/platform-Dg9JBh8M.mjs +1 -0
- package/dist/elements/chunks/{sourceHelpers-DlkCZiTJ.mjs → sourceHelpers-CAGxjbbu.mjs} +1 -1
- package/dist/elements/chunks/{vimeo-SrHhodcx.mjs → vimeo-BNsFVvtc.mjs} +1 -1
- package/dist/elements/chunks/{youtube-Boz9Goek.mjs → youtube-B7s74nf6.mjs} +1 -1
- package/dist/elements/controls.css +1 -1
- package/dist/elements/controls.mjs +1 -1
- package/dist/elements/core.css +1 -1
- package/dist/elements/core.d.mts +7 -7
- package/dist/elements/core.mjs +1 -1
- package/dist/elements/index.d.mts +7 -7
- package/dist/elements/index.mjs +1 -1
- package/dist/elements/style.css +1 -1
- package/dist/index.d.mts +14 -11
- package/dist/index.mjs +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/dist/chunks/ads-msms49DK.mjs +0 -1
- package/dist/chunks/embedShared-B1B7ZH_3.mjs +0 -1
- package/dist/elements/chunks/ads-msms49DK.mjs +0 -1
- package/dist/elements/chunks/embedShared-B1B7ZH_3.mjs +0 -1
package/README.md
CHANGED
|
@@ -14,14 +14,14 @@ npm install @munsonlabs/video-player
|
|
|
14
14
|
|
|
15
15
|
```vue
|
|
16
16
|
<script setup>
|
|
17
|
-
import {
|
|
17
|
+
import { VideoCard } from '@munsonlabs/video-player'
|
|
18
18
|
import '@munsonlabs/video-player/style'
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
21
|
<template>
|
|
22
|
-
<
|
|
22
|
+
<VideoCard
|
|
23
23
|
title="Big Buck Bunny"
|
|
24
|
-
|
|
24
|
+
src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4"
|
|
25
25
|
poster="https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg"
|
|
26
26
|
/>
|
|
27
27
|
</template>
|
|
@@ -29,61 +29,53 @@ import '@munsonlabs/video-player/style'
|
|
|
29
29
|
|
|
30
30
|
### Pinning a single player
|
|
31
31
|
|
|
32
|
-
Set `pin
|
|
32
|
+
Set `pin` on a standalone `VideoCard`/`VideoPlayer` (no `VideoStage` involved) to get the same corner-pinning mini-player behaviour `VideoStage` has, scoped to just that one player: once it scrolls out of view while playing, it pins to the given corner instead of auto-pausing, and shrinks back to its inline size once scrolled back into view.
|
|
33
33
|
|
|
34
34
|
```vue
|
|
35
|
-
<
|
|
35
|
+
<VideoCard src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4" pin="bottom-right" />
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
- Only `'bottom-right'`, `'bottom-left'`, `'top-right'`, `'top-left'` are supported here (unlike `VideoStage`'s `
|
|
38
|
+
- Only `'bottom-right'`, `'bottom-left'`, `'top-right'`, `'top-left'` are supported here (unlike `VideoStage`'s `pin`, there's no `'full-width'` option for a single player).
|
|
39
39
|
- Pausing while pinned does **not** unpin it - like a mini-player, it stays put, paused, in the corner until scrolled back into view. A dismiss button and a scroll-to-player button are shown on the pinned box.
|
|
40
|
-
- Omit `pin
|
|
40
|
+
- Omit `pin` to disable pinning entirely - the player then just auto-pauses offscreen as usual.
|
|
41
41
|
- `HideMarker` (see [Hiding the pinned stage over content](#hiding-the-pinned-stage-over-content)) works the same way here too, since there's only ever one pinned/tucked element on a page at a time.
|
|
42
42
|
|
|
43
43
|
### Stage player
|
|
44
44
|
|
|
45
|
-
`VideoStage` is a sticky full-width player that receives videos from `
|
|
45
|
+
`VideoStage` is a sticky full-width player that receives videos from `VideoCard` components anywhere on the page via window events. When the stage scrolls out of view it minifies to a pip in the bottom-right corner.
|
|
46
46
|
|
|
47
47
|
```vue
|
|
48
48
|
<script setup>
|
|
49
|
-
import { VideoStage,
|
|
49
|
+
import { VideoStage, VideoCard } from '@munsonlabs/video-player'
|
|
50
50
|
import '@munsonlabs/video-player/style'
|
|
51
51
|
</script>
|
|
52
52
|
|
|
53
53
|
<template>
|
|
54
54
|
<VideoStage @state-change="onStateChange" />
|
|
55
|
-
<
|
|
55
|
+
<VideoCard v-for="video in videos" :key="video.src" v-bind="video" />
|
|
56
56
|
</template>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
### Playlists
|
|
60
60
|
|
|
61
|
-
Pass `playlist` (a `VideoEntry[]`, the same shape as a `
|
|
61
|
+
Pass `playlist` (a `VideoEntry[]`, the same shape as a `VideoCard`'s props) to let the stage track the currently-playing video's position within it. Position updates automatically when a user clicks a `VideoCard` elsewhere on the page, with nothing extra to keep in sync.
|
|
62
62
|
|
|
63
63
|
```vue
|
|
64
64
|
<VideoStage :playlist="videos" @state-change="onStateChange" />
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
- Auto-advance (playing the next
|
|
68
|
-
-
|
|
69
|
-
- A template ref on `VideoStage` exposes `playNext()`, `playPrevious()`, `hasNext`, and `hasPrevious` too, for building your own next/previous buttons outside the player's own controls:
|
|
67
|
+
- Auto-advance (playing the next entry on `ended`, no wraparound) is toggled via the controls popup's "Auto" button and remembered in `localStorage` - there's no prop for it.
|
|
68
|
+
- A template ref on `VideoStage` exposes `playNext()`, `playPrevious()`, `hasNext`, and `hasPrevious`:
|
|
70
69
|
|
|
71
70
|
```vue
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</script>
|
|
76
|
-
|
|
77
|
-
<template>
|
|
78
|
-
<VideoStage ref="stage" :playlist="videos" />
|
|
79
|
-
<button :disabled="!stage?.hasPrevious" @click="stage.playPrevious()">Previous</button>
|
|
80
|
-
<button :disabled="!stage?.hasNext" @click="stage.playNext()">Next</button>
|
|
81
|
-
</template>
|
|
71
|
+
<VideoStage ref="stage" :playlist="videos" />
|
|
72
|
+
<button :disabled="!stage?.hasPrevious" @click="stage.playPrevious()">Previous</button>
|
|
73
|
+
<button :disabled="!stage?.hasNext" @click="stage.playNext()">Next</button>
|
|
82
74
|
```
|
|
83
75
|
|
|
84
76
|
### Hiding the pinned stage over content
|
|
85
77
|
|
|
86
|
-
When `VideoStage` is pinned to a corner (any `
|
|
78
|
+
When `VideoStage` is pinned to a corner (any `pin` other than `'full-width'`), it can sit over content you'd rather it not cover - a footer, a signup form, a comments section. `HideMarker` is a plain sentinel element: place it just before that content, and the stage slides mostly off-screen (leaving a small sliver) for as long as the marker is in the viewport, sliding back once it isn't.
|
|
87
79
|
|
|
88
80
|
```vue
|
|
89
81
|
<script setup>
|
|
@@ -91,7 +83,7 @@ import { VideoStage, HideMarker } from '@munsonlabs/video-player'
|
|
|
91
83
|
</script>
|
|
92
84
|
|
|
93
85
|
<template>
|
|
94
|
-
<VideoStage pin
|
|
86
|
+
<VideoStage pin="bottom-right" :playlist="videos" />
|
|
95
87
|
|
|
96
88
|
<!-- ...page content... -->
|
|
97
89
|
|
|
@@ -101,16 +93,16 @@ import { VideoStage, HideMarker } from '@munsonlabs/video-player'
|
|
|
101
93
|
```
|
|
102
94
|
|
|
103
95
|
- There's no prop linking `HideMarker` to a specific `VideoStage` - there's only ever one stage on a page, so it always applies to whichever one is mounted.
|
|
104
|
-
- The sliver width is a CSS custom property: `--
|
|
105
|
-
-
|
|
96
|
+
- The sliver width is a CSS custom property: `--mlv-stage-tuck` (default `32px`), see [theming variables](#theming-the-hud-buttons).
|
|
97
|
+
- Place it precisely where you want the stage to tuck away - it renders a 1px-tall invisible `div`, so it triggers briefly as the page scrolls past that exact line.
|
|
106
98
|
|
|
107
99
|
---
|
|
108
100
|
|
|
109
|
-
##
|
|
101
|
+
## VideoCard / VideoPlayer Props
|
|
110
102
|
|
|
111
103
|
| Prop | Type | Default | Description |
|
|
112
104
|
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
113
|
-
| `
|
|
105
|
+
| `src` | `string` | - | **Required.** The video URL or platform-specific URI (the platform is auto-detected from its shape, see [Platform URLs](#platform-urls)) |
|
|
114
106
|
| `title` | `string` | `''` | Video title |
|
|
115
107
|
| `poster` | `string` | `''` | Poster image URL |
|
|
116
108
|
| `aspectRatio` | `string` | `'16:9'` | e.g. `'16:9'`, `'9:16'`, `'4:3'` |
|
|
@@ -122,41 +114,25 @@ import { VideoStage, HideMarker } from '@munsonlabs/video-player'
|
|
|
122
114
|
| `autoStage` | `boolean` | `false` | Immediately send this video to the stage on mount |
|
|
123
115
|
| `playbackRate` | `number` | `1` | Initial playback rate |
|
|
124
116
|
| `adTagUrl` | `string` | `''` | VAST or VMAP ad tag URL |
|
|
125
|
-
| `
|
|
117
|
+
| `adMacroParams` | `object` | - | Fills `{macro}` tokens on whichever ad tag URL ends up in use, see [Ads](#ads-ima--vast--vmap) |
|
|
126
118
|
| `headerBidding` | `object` | - | Runs a Prebid.js auction before the ad plays, see [Header bidding](#header-bidding-prebidjs) |
|
|
127
119
|
| `tracks` | `array` | - | WebVTT caption/subtitle tracks, see [Captions](#captions-webvtt) |
|
|
128
120
|
| `payload` | `object` | `{}` | Arbitrary data attached to every `state-change` event |
|
|
129
121
|
| `action` | `PlayerAction` | `null` | Button shown in the player HUD, see [Actions](#actions) |
|
|
130
122
|
| `disableTapCapture` | `boolean` | `false` | Disables the full-video tap-to-reveal-controls overlay (tap/click still reaches embed platform UI underneath, e.g. YouTube's own) |
|
|
131
123
|
| `controls` | `boolean` | `true` | Set `false` to render a bare `<video>` with no built-in HUD - drive playback with your own UI instead, see [Headless Controls](#headless-controls) |
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
124
|
+
| `playInView` | `boolean` | `false` | Auto-play once at least half the player is visible, auto-pause once it isn't - e.g. for a scroll-snap feed, see [Autoplay, mute & volume](#autoplay-mute--volume) |
|
|
125
|
+
| `pin` | `PinCorner` | - | Pins the player to this screen corner once it scrolls out of view while playing, instead of auto-pausing - see [Pinning a single player](#pinning-a-single-player) |
|
|
134
126
|
|
|
135
|
-
\* `lazy` only has an effect on `
|
|
127
|
+
\* `lazy` only has an effect on `VideoCard` (default `true`), which is what actually implements the placeholder-until-clicked behaviour. `VideoPlayer` accepts the prop for type compatibility but defaults to `false` and never reads it: using `<VideoPlayer>` directly always mounts the real player immediately, regardless of `lazy`.
|
|
136
128
|
|
|
137
129
|
### Autoplay, mute & volume
|
|
138
130
|
|
|
139
|
-
Unless a video sets `muted`/`volume` explicitly, every player on the page shares one persisted audio preference (`localStorage
|
|
140
|
-
|
|
141
|
-
Volume is never restricted - it always follows the shared preference. Mute has one hard exception layered on top: **autoplay-ish playback with no user gesture behind it must start muted**, full stop, regardless of the stored preference. This isn't a library choice, it's the browser enforcing its autoplay-with-sound policy - unmuted `autoplay`/`playWhenInView` without a gesture is blocked or force-muted by the browser itself. This applies to:
|
|
142
|
-
|
|
143
|
-
- `autoplay="true"` set directly on `VideoItem`/`VideoPlayer`.
|
|
144
|
-
- A `playWhenInView` video actually starting once scrolled into view - re-checked at that exact moment (not just once at mount, since a `playWhenInView` player can sit mounted-but-paused for a long time before it actually plays), so if you unmute something else in the meantime, it plays unmuted once it becomes visible instead of forcing itself muted again.
|
|
145
|
-
|
|
146
|
-
That policy is scoped to the current page load, though, not to `localStorage`: once a real gesture has resulted in unmuted playback during this session - a click that happens to land unmuted (see below), or an explicit press of a mute button/volume slider - browsers generally allow further unmuted JS-triggered playback for the rest of it, so the forced-mute rule above stops applying from that point on (a fresh reload resets it - browsers don't know or care what a past session did).
|
|
147
|
-
|
|
148
|
-
A **real user gesture** is exempt from the policy entirely, and always follows the shared preference immediately, even before any gesture-less autoplay would've been allowed to (the gesture itself is what a browser needs, and it's happening right here) - and if that preference happens to be unmuted, this is itself one of the triggers above that lifts the forced-mute rule for the rest of the session, no explicit mute-button press required:
|
|
149
|
-
|
|
150
|
-
- Clicking a lazy `VideoItem`'s placeholder for the first time.
|
|
151
|
-
- `VideoStage`'s idle-click-to-resume and `video-toggle` window event.
|
|
152
|
-
|
|
153
|
-
**`VideoStage`'s playlist skip/auto-advance is the one exception to "follow the shared preference"**: `playNext`/`playPrevious` carry the _outgoing_ video's own live mute/volume state forward to the next one, rather than the shared preference. This is the feed convention (TikTok/Reels-style), not the single-video-click convention: within one continuous playlist, audio should stick to whatever's already playing - skipping past a video you never unmuted shouldn't spontaneously turn sound on just because nothing's ever been saved globally, and skipping past one you did unmute shouldn't silently mute the next one either.
|
|
154
|
-
|
|
155
|
-
An explicit `muted`/`volume` set on a specific `VideoItem`/`VideoPlayer` (or a specific playlist `VideoEntry`) always wins over all of the above, in every case.
|
|
131
|
+
Unless a video sets `muted`/`volume` explicitly, every player on the page shares one persisted audio preference (`localStorage`) - a newly-mounted player starts at whatever level the viewer last chose anywhere else. **Autoplay without a user gesture always starts muted** (browser policy, not a library choice). A real user gesture (clicking a lazy placeholder, `video-toggle`) follows the shared preference immediately. `VideoStage`'s playlist skip/auto-advance carries the outgoing video's own mute/volume state forward, not the shared preference. An explicit `muted`/`volume` always wins.
|
|
156
132
|
|
|
157
133
|
### Platform URLs
|
|
158
134
|
|
|
159
|
-
The platform is detected automatically from the shape of `
|
|
135
|
+
The platform is detected automatically from the shape of `src`; there's no separate prop to set it.
|
|
160
136
|
|
|
161
137
|
| Platform | URL format |
|
|
162
138
|
| ------------- | -------------------------------------------------------------------------------------------------------- |
|
|
@@ -186,33 +162,16 @@ registerPlatform({
|
|
|
186
162
|
|
|
187
163
|
Call it once, before mounting any player that might see the new URL - resolution reads the registry fresh every time, so ordering (not caching) is the only thing that matters.
|
|
188
164
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
**Source** (`embed: false`) - the platform just hosts a file/manifest behind an opaque URL or ID (JW Player, Brightcove). `resolveSource` (optional - skip it if the URL is already a direct, playable link) is a function returning `{ src, type?, poster?, adTagUrl? }`; the library's own native `<video>`/hls.js/dash.js path handles playback for you, nothing else to implement.
|
|
192
|
-
|
|
193
|
-
`registerPlatform` is `registerMatcher` + `registerEmbedAdapter`/`registerSourceResolver` in one call - each is exported separately too, for the rare case you only need one (e.g. adding a resolver to a platform something else already registered a matcher for).
|
|
194
|
-
|
|
195
|
-
This works identically from the [web component bundles](#web-component-usage) - see [Registering a platform from a web component](#registering-a-platform-from-a-web-component) for the two extra things worth knowing there (a timing subtlety around `customElements.define()`, and the `?defer` import mode that sidesteps it for statically-declared tags).
|
|
196
|
-
|
|
197
|
-
### How a URL becomes a playing embed
|
|
165
|
+
- **`embed: true`** - the platform owns its own player (YouTube, Twitch, Vimeo). `createAdapter` returns a full `PlaybackAdapter`: `play`/`pause`/`currentTime`/captions/quality/PiP/fullscreen/`on`/`off`/`dispose`, translating SDK events into the common set.
|
|
166
|
+
- **`embed: false`** - the platform hosts a file/manifest behind an opaque URL. `resolveSource` (optional) returns `{ src, type?, poster?, adTagUrl? }`; the native path handles playback.
|
|
198
167
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
1. **Platform detection.** On mount, `usePlayer` calls `resolvePlatform(videoUrl)`, which checks the URL against every registered matcher (the six built-ins plus anything added via `registerPlatform`) and returns a platform key plus whether it's `embed` or a plain source.
|
|
202
|
-
2. **Adapter construction.** That result goes to `mountAdapter`, which either resolves a source URL and builds a native `<video>`/hls.js/dash.js adapter, or - for embeds - looks up the platform's registered factory (e.g. `createYoutubeAdapter`) and calls it.
|
|
203
|
-
3. **Hidden mount.** The embed factory calls `createEmbedMount`, which inserts a wrapper `<div>` next to the `<video>` element and starts it at `opacity: 0; pointer-events: none`. The `<video>` (showing its `poster`) stays the visible thing for now - this is what masks the embed SDK's own iframe loading chrome (spinners, its own poster flash) until the SDK is actually ready. The adapter's `el` is this wrapper, not the `<video>` itself (unlike the native path, where `el` _is_ the `<video>`).
|
|
204
|
-
4. **SDK boot + event translation.** The factory loads the platform's SDK (or reuses it if another instance already has), builds the player inside the wrapper, and translates the SDK's own events into the common set every adapter emits (`play`, `pause`, `timeupdate`, `durationchange`, etc.).
|
|
205
|
-
5. **Wiring.** Back in `usePlayer`, `finalizeAdapter` stores the adapter and calls `attachPlayerEvents`, which subscribes all the reactive player state (`isPlaying`, `current`, `supportsCaptions`, ...) to those translated events.
|
|
206
|
-
6. **Reveal.** Once the adapter actually starts playing for the first time (`hasStarted` flips true), a `watch` swaps visibility: the `<video>` is hidden and the wrapper fades to `opacity: 1` with pointer events restored - this is the moment the embed becomes the visible thing.
|
|
207
|
-
7. **Teardown.** On unmount, the adapter's `dispose()` removes the wrapper, restores the `<video>` element's normal display, and detaches the platform's own event listeners.
|
|
208
|
-
|
|
209
|
-
The native (non-embed) path skips steps 3 and 6 entirely - the `<video>` element is the adapter's `el` from the start, so there's nothing separate to reveal.
|
|
168
|
+
This works identically from the [web component bundles](#web-component-usage) - see [Registering a platform from a web component](#registering-a-platform-from-a-web-component) for the `?defer` import mode that sidesteps a timing subtlety with statically-declared tags.
|
|
210
169
|
|
|
211
170
|
---
|
|
212
171
|
|
|
213
172
|
## Headless Controls
|
|
214
173
|
|
|
215
|
-
Unstyled control primitives for building your own HUD instead of the built-in one (set `:controls="false"` on the player) - each is published both as a Vue component and as a `
|
|
174
|
+
Unstyled control primitives for building your own HUD instead of the built-in one (set `:controls="false"` on the player) - each is published both as a Vue component and as a `ml-controls-*` custom element from the exact same source:
|
|
216
175
|
|
|
217
176
|
Building a custom HUD out of these and never rendering `VideoPlayer`'s own built-in controls? Import `@munsonlabs/video-player/style/controls` instead of the full `/style` bundle to skip the built-in HUD/popup CSS - see [Web Component Usage](#web-component-usage) for the full breakdown of the three style bundles.
|
|
218
177
|
|
|
@@ -234,7 +193,7 @@ import {
|
|
|
234
193
|
} from '@munsonlabs/video-player'
|
|
235
194
|
```
|
|
236
195
|
|
|
237
|
-
Every control takes a `player` prop pointing at the same object `VideoPlayer`/`
|
|
196
|
+
Every control takes a `player` prop pointing at the same object `VideoPlayer`/`VideoCard`/`VideoStage` expose via a template ref:
|
|
238
197
|
|
|
239
198
|
```vue
|
|
240
199
|
<script setup>
|
|
@@ -244,7 +203,7 @@ const player = ref(null)
|
|
|
244
203
|
</script>
|
|
245
204
|
|
|
246
205
|
<template>
|
|
247
|
-
<VideoPlayer ref="player"
|
|
206
|
+
<VideoPlayer ref="player" src="..." :controls="false" />
|
|
248
207
|
<PlayButton :player="player" />
|
|
249
208
|
<MuteButton :player="player" />
|
|
250
209
|
<Scrubber :player="player" />
|
|
@@ -254,18 +213,18 @@ const player = ref(null)
|
|
|
254
213
|
Or, `<label for>`-style, skip the ref and point at an element id instead:
|
|
255
214
|
|
|
256
215
|
```vue
|
|
257
|
-
<VideoPlayer id="my-player"
|
|
216
|
+
<VideoPlayer id="my-player" src="..." :controls="false" />
|
|
258
217
|
<PlayButton for="my-player" />
|
|
259
218
|
```
|
|
260
219
|
|
|
261
|
-
`player` and `for` are both optional - if both are given, `player` wins. Each button's own CSS uses `:where()` (zero specificity), so a single class you add always wins, and most expose their state via a scoped slot for full custom markup (e.g. `PlayButton`'s `#default="{ isPlaying }"`). See [Web Component Usage](#web-component-usage) for using these as raw custom elements (`<
|
|
220
|
+
`player` and `for` are both optional - if both are given, `player` wins. Each button's own CSS uses `:where()` (zero specificity), so a single class you add always wins, and most expose their state via a scoped slot for full custom markup (e.g. `PlayButton`'s `#default="{ isPlaying }"`). See [Web Component Usage](#web-component-usage) for using these as raw custom elements (`<ml-controls-play-button>` etc.) outside Vue.
|
|
262
221
|
|
|
263
222
|
### Transcript
|
|
264
223
|
|
|
265
|
-
`Transcript` renders a clickable transcript for the linked player: pass `cues` as `{ time, end?, text }[]` (times in seconds), clicking a cue seeks to its timestamp (starting playback first if paused), and the cue at the playhead is highlighted and kept scrolled into view
|
|
224
|
+
`Transcript` renders a clickable transcript for the linked player: pass `cues` as `{ time, end?, text }[]` (times in seconds), clicking a cue seeks to its timestamp (starting playback first if paused), and the cue at the playhead is highlighted and kept scrolled into view:
|
|
266
225
|
|
|
267
226
|
```vue
|
|
268
|
-
<VideoPlayer id="my-player"
|
|
227
|
+
<VideoPlayer id="my-player" src="..." />
|
|
269
228
|
<Transcript
|
|
270
229
|
for="my-player"
|
|
271
230
|
:cues="[
|
|
@@ -276,16 +235,14 @@ Or, `<label for>`-style, skip the ref and point at an element id instead:
|
|
|
276
235
|
/>
|
|
277
236
|
```
|
|
278
237
|
|
|
279
|
-
-
|
|
280
|
-
- `end` is optional: when set, nothing is highlighted between that cue's `end` and the next cue's start
|
|
281
|
-
- Auto-scroll pauses while the pointer is over the list
|
|
282
|
-
-
|
|
283
|
-
- As a custom element (`<muns-controls-transcript>`), pass cues as an inline JSON string attribute: `cues='[{"time":0,"text":"..."}]'`.
|
|
284
|
-
- Clicking a cue before the video has loaded (a lazy `VideoItem`, an unstarted player) starts playback instead - jumping needs the duration, which isn't known yet at that point.
|
|
238
|
+
- Works on **every** platform (YouTube/Vimeo/Dailymotion included) - only needs current time and `seek`.
|
|
239
|
+
- `end` is optional: when set, nothing is highlighted between that cue's `end` and the next cue's start.
|
|
240
|
+
- Auto-scroll pauses while the pointer is over the list. A scoped slot customises each cue's markup: `#default="{ cue, index, isActive, formatTime }"`.
|
|
241
|
+
- As a custom element (`<ml-controls-transcript>`), pass cues as an inline JSON string attribute: `cues='[{"time":0,"text":"..."}]'`.
|
|
285
242
|
|
|
286
243
|
### Building your own wrapper component
|
|
287
244
|
|
|
288
|
-
`useForwardedPlayer` is what `
|
|
245
|
+
`useForwardedPlayer` is what `VideoCard`/`VideoStage` themselves use to forward a template-ref'd `VideoPlayer`'s controls/state onto their own `defineExpose` - the same mechanism is available for building a custom wrapper component of your own:
|
|
289
246
|
|
|
290
247
|
```vue
|
|
291
248
|
<script setup>
|
|
@@ -300,94 +257,75 @@ defineExpose(forwarded)
|
|
|
300
257
|
</template>
|
|
301
258
|
```
|
|
302
259
|
|
|
303
|
-
`useForwardedPlayer` owns the ref itself - bind `playerRef` on the wrapped `VideoPlayer`, and spread (or pass) `forwarded` into your own `defineExpose`, so callers holding a ref to your wrapper get the same imperative API `VideoPlayer` exposes. Pass a `guard` callback to intercept
|
|
304
|
-
|
|
305
|
-
Because it's wired up via `defineExpose`, this works the same way whether your wrapper is used as a Vue component (a template ref) or as a custom element ([Web Component Usage](#web-component-usage)) - Vue copies `defineExpose`d properties onto the element itself, so `document.querySelector('muns-video-item').togglePlay()` calls the same forwarded method a Vue template ref would.
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
## Events
|
|
310
|
-
|
|
311
|
-
### `state-change`
|
|
312
|
-
|
|
313
|
-
Emitted by `VideoItem`, `VideoPlayer`, and `VideoStage`.
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
interface StateChangeEvent {
|
|
317
|
-
type:
|
|
318
|
-
| 'play'
|
|
319
|
-
| 'pause'
|
|
320
|
-
| 'ended'
|
|
321
|
-
| 'seeked'
|
|
322
|
-
| 'error'
|
|
323
|
-
| 'adstart'
|
|
324
|
-
| 'adend'
|
|
325
|
-
| 'volumechange'
|
|
326
|
-
| 'ratechange'
|
|
327
|
-
| 'captionchange'
|
|
328
|
-
| 'qualitychange'
|
|
329
|
-
| 'pipchange'
|
|
330
|
-
| 'loopchange'
|
|
331
|
-
| 'firstQuartile'
|
|
332
|
-
| 'midpoint'
|
|
333
|
-
| 'thirdQuartile'
|
|
334
|
-
| 'controlsopen'
|
|
335
|
-
| 'controlsclose'
|
|
336
|
-
| 'stageopen'
|
|
337
|
-
| 'stageclose'
|
|
338
|
-
| 'bufferstart'
|
|
339
|
-
| 'bufferend'
|
|
340
|
-
| 'timeupdate'
|
|
341
|
-
| 'tap'
|
|
342
|
-
currentTime: number
|
|
343
|
-
duration: number
|
|
344
|
-
src: string
|
|
345
|
-
error?: { code: number; message: string } | null
|
|
346
|
-
isMuted?: boolean // volumechange only
|
|
347
|
-
playbackRate?: number // ratechange only
|
|
348
|
-
captionIndex?: number | null // captionchange only; null means captions are off
|
|
349
|
-
qualityIndex?: number | null // qualitychange only; null means Auto
|
|
350
|
-
isPipActive?: boolean // pipchange only
|
|
351
|
-
isLooping?: boolean // loopchange only
|
|
352
|
-
element?: HTMLElement | null // controlsopen/controlsclose only
|
|
353
|
-
payload?: Record<string, unknown>
|
|
354
|
-
}
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
`firstQuartile` / `midpoint` / `thirdQuartile` fire once each as playback crosses 25%, 50%, and 75% of the video's duration. This is useful for analytics integrations that expect IAB-style progress milestones. They reset and can fire again on loop restart.
|
|
358
|
-
|
|
359
|
-
`controlsopen` / `controlsclose` fire when the expanded controls popup mounts/unmounts (e.g. via the "show controls" button), with `element` set to the popup's root DOM node: handy for positioning your own UI relative to it, or measuring it. On `controlsclose`, `element` is a snapshot of the closing node; it's detached from the DOM immediately after.
|
|
260
|
+
`useForwardedPlayer` owns the ref itself - bind `playerRef` on the wrapped `VideoPlayer`, and spread (or pass) `forwarded` into your own `defineExpose`, so callers holding a ref to your wrapper get the same imperative API `VideoPlayer` exposes. Pass a `guard` callback to intercept method calls before they reach the underlying player.
|
|
360
261
|
|
|
361
|
-
|
|
262
|
+
Because it's wired up via `defineExpose`, this works the same way whether your wrapper is used as a Vue component (a template ref) or as a custom element ([Web Component Usage](#web-component-usage)) - Vue copies `defineExpose`d properties onto the element itself.
|
|
362
263
|
|
|
363
|
-
|
|
264
|
+
### Exposing a plain Vue player to independent, non-Vue code
|
|
364
265
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
`pipchange` fires whenever Picture-in-Picture is entered or exited, whether that came from the player's own PiP button or the user closing the browser's floating PiP window directly.
|
|
368
|
-
|
|
369
|
-
`loopchange` fires whenever `toggleLoop()` is called (e.g. from the "Loop" row in the controls menu), never before playback has started.
|
|
370
|
-
|
|
371
|
-
`bufferstart` fires when the player has been stalled on `waiting` for longer than the buffering-spinner delay; `bufferend` fires once `playing`/`canplay` resolves it (or on `ended`). Useful for tracking rebuffer count/ratio.
|
|
372
|
-
|
|
373
|
-
On `error`, the player shows a "Retry" button that re-attempts loading the current source. You can also trigger this yourself via a template ref on `VideoPlayer`, which exposes a `retry()` method along with `togglePlay()`, `seek()`, `toggleMute()`, `setVolume()`, `toggleFullscreen()`, `toggleLoop()`, `setPlaybackRate()`, `setCaptionTrack()`, `setQuality()`, and `togglePip()`. Note this is on `VideoPlayer` specifically: `VideoItem` doesn't forward a ref to it, since it may be showing a lazy placeholder instead:
|
|
266
|
+
A plain `<VideoPlayer>`/`<VideoCard>`/`<VideoStage>` used the normal Vue way doesn't expose its API onto the DOM - its state lives on the Vue component instance, reachable only via a template ref inside your own app. `exposePlayerOnElement` bridges that gap for code with no access to your Vue app (a third party's own script, a `<ml-controls-*>` custom element from a separate bundle):
|
|
374
267
|
|
|
375
268
|
```vue
|
|
376
269
|
<script setup>
|
|
377
|
-
import { ref } from 'vue'
|
|
378
|
-
|
|
270
|
+
import { ref, onMounted } from 'vue'
|
|
271
|
+
import { VideoPlayer, exposePlayerOnElement } from '@munsonlabs/video-player'
|
|
272
|
+
|
|
273
|
+
const playerRef = ref(null)
|
|
274
|
+
const wrapperEl = ref(null)
|
|
275
|
+
onMounted(() => exposePlayerOnElement(wrapperEl.value, playerRef.value))
|
|
379
276
|
</script>
|
|
380
277
|
|
|
381
278
|
<template>
|
|
382
|
-
<
|
|
383
|
-
|
|
279
|
+
<div id="my-player" ref="wrapperEl">
|
|
280
|
+
<VideoPlayer ref="playerRef" src="..." />
|
|
281
|
+
</div>
|
|
384
282
|
</template>
|
|
385
283
|
```
|
|
386
284
|
|
|
387
|
-
```
|
|
388
|
-
|
|
285
|
+
```html
|
|
286
|
+
<!-- a completely separate script/bundle, e.g. a third party's own -->
|
|
287
|
+
<script type="module">
|
|
288
|
+
import '@munsonlabs/video-player/element/controls'
|
|
289
|
+
</script>
|
|
290
|
+
<ml-controls-transcript for="my-player" cues='[{"time":0,"text":"..."}]'></ml-controls-transcript>
|
|
389
291
|
```
|
|
390
292
|
|
|
293
|
+
State fields are copied as live getters (not a one-time snapshot) and methods directly. `el` doesn't need to be the player's own root element - a wrapper `<div>` works the same.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Events
|
|
298
|
+
|
|
299
|
+
### `state-change`
|
|
300
|
+
|
|
301
|
+
Emitted by `VideoCard`, `VideoPlayer`, and `VideoStage`. Every event includes `currentTime`, `duration`, `src`, and `payload` (arbitrary user data).
|
|
302
|
+
|
|
303
|
+
| Event | When it fires | Extra fields |
|
|
304
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
305
|
+
| `play` | Playback starts or resumes | — |
|
|
306
|
+
| `pause` | Playback pauses | — |
|
|
307
|
+
| `ended` | Playback reaches the end | — |
|
|
308
|
+
| `seeked` | Seek completes | — |
|
|
309
|
+
| `error` | Source fails to load/play; player shows a "Retry" button | `error: { code, message }` |
|
|
310
|
+
| `adstart` | Ad begins playing | — |
|
|
311
|
+
| `adend` | Ad finishes | — |
|
|
312
|
+
| `volumechange` | Volume or mute state changes | `isMuted` |
|
|
313
|
+
| `ratechange` | Playback rate changes | `playbackRate` |
|
|
314
|
+
| `captionchange` | Active caption track changes (fires on `setCaptionTrack()` and ABR-driven changes, but not at mount) | `captionIndex` (null = off) |
|
|
315
|
+
| `qualitychange` | Active quality level changes (fires on `setQuality()` and ABR-driven changes, but not at mount) | `qualityIndex` (null = Auto) |
|
|
316
|
+
| `pipchange` | PiP entered or exited (from player button or browser floating window) | `isPipActive` |
|
|
317
|
+
| `loopchange` | Loop toggled (not before playback starts) | `isLooping` |
|
|
318
|
+
| `firstQuartile` | Playback crosses 25% of duration | — |
|
|
319
|
+
| `midpoint` | Playback crosses 50% of duration | — |
|
|
320
|
+
| `thirdQuartile` | Playback crosses 75% of duration | — |
|
|
321
|
+
| `controlsopen` / `controlsclose` | Expanded controls popup mounts/unmounts | `element` (popup root node; on close, a detached snapshot) |
|
|
322
|
+
| `stageopen` / `stageclose` | Stage mounts its inner player / tears it down (stage only) | — |
|
|
323
|
+
| `bufferstart` / `bufferend` | Buffering begins (after stall delay) / resolves | — |
|
|
324
|
+
| `timeupdate` | Regular time update during playback | — |
|
|
325
|
+
| `tap` | Full-video tap-to-reveal overlay is tapped; only signal for `controls: false` consumers | — |
|
|
326
|
+
|
|
327
|
+
`firstQuartile`/`midpoint`/`thirdQuartile` reset and fire again on loop restart. `captionchange`/`qualitychange` never fire before playback starts.
|
|
328
|
+
|
|
391
329
|
---
|
|
392
330
|
|
|
393
331
|
## Actions
|
|
@@ -397,35 +335,19 @@ The `action` prop adds a button to the player HUD. Pass a built-in string or a c
|
|
|
397
335
|
### Built-in actions
|
|
398
336
|
|
|
399
337
|
```vue
|
|
400
|
-
<
|
|
401
|
-
<
|
|
402
|
-
<
|
|
338
|
+
<VideoCard action="mute" ... />
|
|
339
|
+
<VideoCard action="loop" ... />
|
|
340
|
+
<VideoCard action="autoplay" ... />
|
|
403
341
|
```
|
|
404
342
|
|
|
405
343
|
`action="autoplay"` toggles [auto-advance](#playlists) and only makes sense when rendered inside a `VideoStage` with a `playlist` set: it reads and flips the same `autoAdvance` state as the controls popup's built-in "Auto" button, just as a HUD button instead.
|
|
406
344
|
|
|
407
345
|
### Custom action
|
|
408
346
|
|
|
409
|
-
|
|
410
|
-
interface CustomAction {
|
|
411
|
-
icon: string // SVG string
|
|
412
|
-
label: string
|
|
413
|
-
onClick: () => void
|
|
414
|
-
}
|
|
415
|
-
```
|
|
347
|
+
Pass an object with `icon` (SVG string), `label`, and `onClick`:
|
|
416
348
|
|
|
417
349
|
```vue
|
|
418
|
-
<
|
|
419
|
-
const saveAction = {
|
|
420
|
-
icon: `<svg .../>`,
|
|
421
|
-
label: 'Save',
|
|
422
|
-
onClick: () => console.log('saved'),
|
|
423
|
-
}
|
|
424
|
-
</script>
|
|
425
|
-
|
|
426
|
-
<template>
|
|
427
|
-
<VideoItem :action="saveAction" ... />
|
|
428
|
-
</template>
|
|
350
|
+
<VideoCard :action="{ icon: '<svg .../>', label: 'Save', onClick: () => console.log('saved') }" ... />
|
|
429
351
|
```
|
|
430
352
|
|
|
431
353
|
---
|
|
@@ -435,49 +357,38 @@ const saveAction = {
|
|
|
435
357
|
**Step by step:**
|
|
436
358
|
|
|
437
359
|
1. Get a VAST or VMAP ad tag URL. For testing, use [Google's public IMA sample tags](https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags); for production, this comes from your ad server (Google Ad Manager): ask whoever manages ad ops for the tag, or build it yourself if you already know the ad unit path.
|
|
438
|
-
2. Pass it as `adTagUrl` on `
|
|
360
|
+
2. Pass it as `adTagUrl` on `VideoCard`/`VideoPlayer`. That's it for a basic pre-roll/VMAP schedule: the Google IMA SDK is loaded automatically on demand, nothing else to configure.
|
|
439
361
|
3. Confirm it's working: watch for `adstart`/`adend` on the `state-change` event, or check the Network tab for a request to `pubads.g.doubleclick.net` (or wherever your ad server lives) when the video plays.
|
|
440
|
-
4. Only if your ad tag is a **macro template** (contains literal `{tokenName}` placeholders, as Brightcove's auto-discovered ad tags usually do, see below) do you need `
|
|
362
|
+
4. Only if your ad tag is a **macro template** (contains literal `{tokenName}` placeholders, as Brightcove's auto-discovered ad tags usually do, see below) do you need `adMacroParams` too. Otherwise skip it entirely.
|
|
441
363
|
|
|
442
364
|
```vue
|
|
443
|
-
<
|
|
365
|
+
<VideoCard src="https://..." ad-tag-url="https://pubads.g.doubleclick.net/..." />
|
|
444
366
|
```
|
|
445
367
|
|
|
446
368
|
Ads are not supported on YouTube, Vimeo, or Dailymotion.
|
|
447
369
|
|
|
448
|
-
Some platforms (currently Brightcove) auto-discover their own ad tag URL: the platform's own wins if you don't pass `adTagUrl` yourself. Brightcove's ad tag URLs are typically macro templates (e.g. `...&iu={adUnit}&vid={referenceId}&
|
|
370
|
+
Some platforms (currently Brightcove) auto-discover their own ad tag URL: the platform's own wins if you don't pass `adTagUrl` yourself. Brightcove's ad tag URLs are typically macro templates (e.g. `...&iu={adUnit}&vid={referenceId}&cust_params={customParameters}`), not ready-to-use URLs. `adMacroParams` fills these in on **whichever** ad tag URL ends up in use, prop-supplied or auto-discovered:
|
|
449
371
|
|
|
450
372
|
```vue
|
|
451
|
-
<
|
|
452
|
-
video-url="https://players.brightcove.net/..."
|
|
453
|
-
:ad-tag-params="{
|
|
454
|
-
adUnit: 'network/section',
|
|
455
|
-
referenceId: videoId,
|
|
456
|
-
rdid: deviceId,
|
|
457
|
-
idtype: 'idfa',
|
|
458
|
-
is_lat: 0,
|
|
459
|
-
customParameters: 'pageType=article&contentId=123',
|
|
460
|
-
}"
|
|
461
|
-
/>
|
|
373
|
+
<VideoCard src="https://players.brightcove.net/..." :ad-tag-params="{ adUnit: 'network/section', referenceId: videoId, rdid: deviceId }" />
|
|
462
374
|
```
|
|
463
375
|
|
|
464
|
-
Name each key exactly after the macro it fills
|
|
376
|
+
Name each key exactly after the macro it fills (`vid={referenceId}` means the key is `referenceId`, not `vid`). A key with no matching `{macro}` is a no-op.
|
|
465
377
|
|
|
466
378
|
### Header bidding (Prebid.js)
|
|
467
379
|
|
|
468
380
|
**Step by step:**
|
|
469
381
|
|
|
470
|
-
1. Confirm your organisation already runs Prebid.js somewhere on the page
|
|
471
|
-
2. Get a Prebid **video ad unit** config from ad ops
|
|
472
|
-
3.
|
|
473
|
-
4.
|
|
474
|
-
5. Verify: after playing, run `pbjs.getBidResponsesForAdUnitCode('your-ad-unit-code')` in the console to confirm a bid came back, and check the Network tab for `hb_bidder`/`hb_pb`/etc. in the actual ad request's `cust_params` to confirm the winning bid's targeting made it into the request IMA sent.
|
|
382
|
+
1. Confirm your organisation already runs Prebid.js somewhere on the page (`window.pbjs` in the console). If not, this isn't something to set up from this package.
|
|
383
|
+
2. Get a Prebid **video ad unit** config from ad ops (with `code`, `mediaTypes.video`, and `bids`) and the GAM ad unit path (`iu`).
|
|
384
|
+
3. Pass both as `headerBidding: { adUnit, params: { iu: '...' } }`, alongside a plain `adTagUrl`/`adMacroParams` as fallback. Never rely on `headerBidding` alone.
|
|
385
|
+
4. Verify: run `pbjs.getBidResponsesForAdUnitCode('your-ad-unit-code')` in the console after playing.
|
|
475
386
|
|
|
476
|
-
The auction runs **in the background** and is never awaited: it can't block mounting, playback, or controls. Its result is only used if
|
|
387
|
+
The auction runs **in the background** and is never awaited: it can't block mounting, playback, or controls. Its result is only used if ready by the time the ad is requested (first play); otherwise the plain ad tag is used. Every failure mode (no `pbjs`, timeout, no bid, `buildVideoUrl()` throwing) fails open:
|
|
477
388
|
|
|
478
389
|
```vue
|
|
479
|
-
<
|
|
480
|
-
|
|
390
|
+
<VideoCard
|
|
391
|
+
src="https://players.brightcove.net/..."
|
|
481
392
|
:header-bidding="{
|
|
482
393
|
adUnit: {
|
|
483
394
|
code: 'video-preroll',
|
|
@@ -490,7 +401,7 @@ The auction runs **in the background** and is never awaited: it can't block moun
|
|
|
490
401
|
/>
|
|
491
402
|
```
|
|
492
403
|
|
|
493
|
-
`adUnit` is a standard Prebid video ad unit
|
|
404
|
+
`adUnit` is a standard Prebid video ad unit. `params` is passed through to `buildVideoUrl()` (GAM ad tag params). `timeoutMs` (default `1000`) bounds the auction. Every failure mode fails open to whatever `adTagUrl`/`adMacroParams` would have resolved to.
|
|
494
405
|
|
|
495
406
|
### Ad overlay UI
|
|
496
407
|
|
|
@@ -498,7 +409,7 @@ While an ad is playing, a small overlay shows an "Ad" badge with a countdown to
|
|
|
498
409
|
|
|
499
410
|
### Ad visibility behaviour
|
|
500
411
|
|
|
501
|
-
An ad pauses itself whenever the tab is hidden or the window loses focus
|
|
412
|
+
An ad pauses itself whenever the tab is hidden or the window loses focus, and exits Picture-in-Picture if active (skipped on iOS, where the ad renders into the same `<video>` element PiP mirrors). The player ignores `timeupdate`/`durationchange`/`progress` events while an ad is active, so `current`/`total`/`bufferedDisplay` stay on the content's values throughout. On iOS, after a post-roll ad finishes the player restores the original content source and seeks to the end, since the IMA SDK may not restore it.
|
|
502
413
|
|
|
503
414
|
---
|
|
504
415
|
|
|
@@ -512,8 +423,8 @@ There are two ways a caption track ends up available, and you don't have to pick
|
|
|
512
423
|
- **Everything else** (a plain MP4, or an HLS stream with no subtitle rendition of its own) needs WebVTT tracks supplied explicitly via `tracks`:
|
|
513
424
|
|
|
514
425
|
```vue
|
|
515
|
-
<
|
|
516
|
-
|
|
426
|
+
<VideoCard
|
|
427
|
+
src="https://.../video.mp4"
|
|
517
428
|
:tracks="[
|
|
518
429
|
{ src: '/captions/en.vtt', kind: 'captions', srclang: 'en', label: 'English', default: true },
|
|
519
430
|
{ src: '/captions/fr.vtt', kind: 'captions', srclang: 'fr', label: 'Français' },
|
|
@@ -521,17 +432,7 @@ There are two ways a caption track ends up available, and you don't have to pick
|
|
|
521
432
|
/>
|
|
522
433
|
```
|
|
523
434
|
|
|
524
|
-
|
|
525
|
-
interface CaptionTrackDef {
|
|
526
|
-
src: string // a WebVTT file URL: <track> only understands VTT, not SRT or other subtitle formats
|
|
527
|
-
kind?: 'captions' | 'subtitles'
|
|
528
|
-
srclang?: string
|
|
529
|
-
label?: string
|
|
530
|
-
default?: boolean
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Each entry renders as a native `<track>` element: the browser fetches and parses the file itself, nothing is loaded or parsed by this package. `default: true` has the browser show that track immediately with no extra wiring; the player detects and reports this the same way as a track picked manually via the controls menu.
|
|
435
|
+
`src` must be a WebVTT file URL (`<track>` only understands VTT, not SRT). Each entry renders as a native `<track>` element: the browser fetches and parses the file itself. `default: true` has the browser show that track immediately with no extra wiring.
|
|
535
436
|
|
|
536
437
|
Clicking "Captions" in the controls menu cycles Off → first track → next track → ... → Off. Programmatically, a template ref on `VideoPlayer` exposes `setCaptionTrack(index)` (`null` turns captions off); see [Events](#state-change) for the `captionchange` event this fires.
|
|
537
438
|
|
|
@@ -541,11 +442,9 @@ Clicking "Captions" in the controls menu cycles Off → first track → next tra
|
|
|
541
442
|
|
|
542
443
|
Adaptive HLS and DASH streams automatically expose a "Quality" option in the controls menu once hls.js/dash.js (or Safari's native HLS) has parsed the manifest's resolution variants: there's no prop to set and nothing to configure. Clicking it cycles Auto → highest → ... → lowest → Auto; Auto leaves the streaming engine's adaptive bitrate algorithm in control.
|
|
543
444
|
|
|
544
|
-
This only applies to HLS and DASH: plain MP4/HTML5 sources have no variant levels
|
|
545
|
-
|
|
546
|
-
**Safari plays HLS natively instead of through hls.js** (see [Peer Dependencies](#peer-dependencies)), and native HLS has no manual quality-override hook at all: the Quality option simply won't appear there, since Safari's own adaptive bitrate algorithm is always in control on that path.
|
|
445
|
+
This only applies to HLS and DASH: plain MP4/HTML5 sources have no variant levels, and it's unsupported on YouTube, Vimeo, and Dailymotion. Brightcove and JW Player get it for free, same reasoning as captions above.
|
|
547
446
|
|
|
548
|
-
|
|
447
|
+
**Safari plays HLS natively** (see [Peer Dependencies](#peer-dependencies)), and native HLS has no manual quality-override hook: the Quality option simply won't appear there.
|
|
549
448
|
|
|
550
449
|
Programmatically, a template ref on `VideoPlayer` exposes `setQuality(index)` (`null` re-enables Auto); see [Events](#state-change) for the `qualitychange` event this fires.
|
|
551
450
|
|
|
@@ -563,81 +462,48 @@ Programmatically, a template ref on `VideoPlayer` exposes `togglePip()`; see [Ev
|
|
|
563
462
|
|
|
564
463
|
## Theming the HUD buttons
|
|
565
464
|
|
|
566
|
-
Three buttons read their background and icon colour from two CSS custom properties, falling back to the default frosted-glass look when unset: the action button (mute/loop/custom/save), the central play/pause button, and the "show controls" button. The expanded controls popup (opened via the "show controls" button) is unaffected and keeps its own fixed styling.
|
|
567
|
-
|
|
568
465
|
Set the variables on any ancestor of the player (e.g. a wrapper `<div>` or `:root`) to restyle all three buttons at once:
|
|
569
466
|
|
|
570
467
|
```css
|
|
571
468
|
.my-video-wrapper {
|
|
572
|
-
--
|
|
573
|
-
--
|
|
469
|
+
--mlv-btn-bg: #e11d48;
|
|
470
|
+
--mlv-btn-color: #fff;
|
|
574
471
|
}
|
|
575
472
|
```
|
|
576
473
|
|
|
577
|
-
Icons use `fill="currentColor"`, so `--
|
|
474
|
+
Icons use `fill="currentColor"`, so `--mlv-btn-color` recolours the icon along with `--mlv-btn-bg` for the background. Hover state is a `filter: brightness()` lift on top of `--mlv-btn-bg`, so it works for both the default translucent look and a solid theme colour.
|
|
578
475
|
|
|
579
|
-
###
|
|
476
|
+
### Full controls popup
|
|
580
477
|
|
|
581
|
-
|
|
478
|
+
| Variable | Default | Description |
|
|
479
|
+
| ---------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
480
|
+
| `--mlv-controls-width` | `min(450px, calc(100% - 32px))` | Controls popup width |
|
|
481
|
+
| `--mlv-popup-align` | `center` | Horizontal alignment of the popup (`center` or `flex-end` to dock at the bottom) |
|
|
482
|
+
| `--mlv-btn-bg` | (frosted glass) | Background of action, play/pause, and show-controls buttons |
|
|
483
|
+
| `--mlv-btn-color` | (frosted glass) | Icon/text colour of the same buttons |
|
|
484
|
+
| `--mlv-stage-tuck` | `32px` | Width of the sliver left visible when `HideMarker` tucks the pinned stage off-screen |
|
|
485
|
+
| `--mlv-radius` | `12px` | Border-radius of the pinned corner box |
|
|
486
|
+
| `--mlv-accent` | — | Active-state highlight colour for the loop/autoplay toggle buttons in the controls popup |
|
|
582
487
|
|
|
583
|
-
|
|
584
|
-
.my-video-wrapper {
|
|
585
|
-
--mvp-popup-align: flex-end;
|
|
586
|
-
}
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
Real fullscreen always docks the popup to the bottom regardless of this variable.
|
|
488
|
+
Real fullscreen always docks the popup to the bottom regardless of `--mlv-popup-align`.
|
|
590
489
|
|
|
591
490
|
### Fullscreen controls
|
|
592
491
|
|
|
593
|
-
Outside fullscreen, the full controls bar only appears after clicking "show controls" and stays open until dismissed. In real fullscreen it
|
|
594
|
-
|
|
595
|
-
---
|
|
596
|
-
|
|
597
|
-
## Window Events
|
|
598
|
-
|
|
599
|
-
`VideoItem` and `VideoStage` communicate via window custom events. You can dispatch these yourself to control the stage from outside Vue.
|
|
600
|
-
|
|
601
|
-
| Event | Detail | Description |
|
|
602
|
-
| -------------- | --------------------------------------------------------- | ---------------------------------------------- |
|
|
603
|
-
| `video-select` | `VideoSelectDetail` | Load a video into the stage |
|
|
604
|
-
| `video-toggle` | `{ videoUrl: string }` | Play/pause a specific video in the stage |
|
|
605
|
-
| `video-state` | `{ currentVideoUrl: string \| null, isPlaying: boolean }` | Dispatched by the stage when its state changes |
|
|
606
|
-
|
|
607
|
-
```js
|
|
608
|
-
// Load a video into the stage
|
|
609
|
-
window.dispatchEvent(
|
|
610
|
-
new CustomEvent('video-select', {
|
|
611
|
-
detail: {
|
|
612
|
-
videoUrl: 'https://...',
|
|
613
|
-
title: 'My Video',
|
|
614
|
-
poster: 'https://...',
|
|
615
|
-
autoplay: true,
|
|
616
|
-
},
|
|
617
|
-
}),
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
// Listen for stage state changes
|
|
621
|
-
window.addEventListener('video-state', (e) => {
|
|
622
|
-
console.log(e.detail.currentVideoUrl, e.detail.isPlaying)
|
|
623
|
-
})
|
|
624
|
-
```
|
|
492
|
+
Outside fullscreen, the full controls bar only appears after clicking "show controls" and stays open until dismissed. In real fullscreen it auto-shows on activity and auto-hides after inactivity, like a standard native player.
|
|
625
493
|
|
|
626
494
|
---
|
|
627
495
|
|
|
628
496
|
## Web Component Usage
|
|
629
497
|
|
|
630
|
-
Import an element bundle to register components as native custom elements. These
|
|
498
|
+
Import an element bundle to register components as native custom elements. These render into the light DOM (not shadow roots), so styling has nowhere encapsulated to live. Vue is an external dependency: use an import map to provide it.
|
|
631
499
|
|
|
632
500
|
Three bundles are available: pick exactly one, since importing more than one double-registers any tag they share and throws:
|
|
633
501
|
|
|
634
|
-
- `@munsonlabs/video-player/element`: everything (
|
|
635
|
-
- `@munsonlabs/video-player/element/core`: just `<
|
|
636
|
-
- `@munsonlabs/video-player/element/controls`: just the `<
|
|
502
|
+
- `@munsonlabs/video-player/element`: everything (player + all headless controls). Auto-injects its own CSS.
|
|
503
|
+
- `@munsonlabs/video-player/element/core`: just `<ml-video-player>`/`<ml-video-stage>`/`<ml-video-card>`/`<ml-video-placeholder>`. Import `@munsonlabs/video-player/style/core` yourself.
|
|
504
|
+
- `@munsonlabs/video-player/element/controls`: just the `<ml-controls-*>` primitives. Import `@munsonlabs/video-player/style/controls` alongside it.
|
|
637
505
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
`/element` and `/element/core` (not `/element/controls`, which never touches platform code) each dynamically `import()` a platform's own adapter code the first time a URL for that platform actually mounts, landing in a sibling `chunks/` directory rather than the main file - a page that only ever plays plain MP4/HLS/DASH downloads none of it. Loading from a directory-serving CDN (unpkg, jsdelivr, esm.sh) needs no special handling; self-hosting either bundle means deploying its `chunks/` directory alongside it.
|
|
506
|
+
`/element` and `/element/core` dynamically `import()` platform adapter code only when a URL for that platform mounts (from a sibling `chunks/` directory); self-hosting either bundle means deploying `chunks/` alongside it.
|
|
641
507
|
|
|
642
508
|
```html
|
|
643
509
|
<script type="importmap">
|
|
@@ -649,11 +515,11 @@ The `core`/`controls` bundles leave styling entirely up to you: defer the import
|
|
|
649
515
|
</script>
|
|
650
516
|
<script type="module" src="@munsonlabs/video-player/element"></script>
|
|
651
517
|
|
|
652
|
-
<
|
|
653
|
-
|
|
518
|
+
<ml-video-card
|
|
519
|
+
src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4"
|
|
654
520
|
title="Big Buck Bunny"
|
|
655
521
|
poster="https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg"
|
|
656
|
-
></
|
|
522
|
+
></ml-video-card>
|
|
657
523
|
```
|
|
658
524
|
|
|
659
525
|
If you'll be playing HLS (`.m3u8`) or DASH (`.mpd`) sources, also add an import map entry for `hls.js`/`dashjs`. Each is loaded on demand via a dynamic `import(...)`, which (unlike the Vue usage, where your own bundler resolves it) has no resolution path in a plain browser without one:
|
|
@@ -674,51 +540,51 @@ If you'll be playing HLS (`.m3u8`) or DASH (`.mpd`) sources, also add an import
|
|
|
674
540
|
|
|
675
541
|
Core (in `/element` and `/element/core`):
|
|
676
542
|
|
|
677
|
-
| Element
|
|
678
|
-
|
|
|
679
|
-
| `<
|
|
680
|
-
| `<
|
|
681
|
-
| `<
|
|
682
|
-
| `<
|
|
683
|
-
| `<
|
|
543
|
+
| Element | Vue equivalent |
|
|
544
|
+
| ------------------------ | -------------------- |
|
|
545
|
+
| `<ml-video-card>` | `<VideoCard>` |
|
|
546
|
+
| `<ml-video-stage>` | `<VideoStage>` |
|
|
547
|
+
| `<ml-video-player>` | `<VideoPlayer>` |
|
|
548
|
+
| `<ml-video-placeholder>` | `<VideoPlaceholder>` |
|
|
549
|
+
| `<ml-hide-marker>` | `<HideMarker>` |
|
|
684
550
|
|
|
685
551
|
Controls (in `/element` and `/element/controls`), see [Headless Controls](#headless-controls) for what each does:
|
|
686
552
|
|
|
687
|
-
| Element
|
|
688
|
-
|
|
|
689
|
-
| `<
|
|
690
|
-
| `<
|
|
691
|
-
| `<
|
|
692
|
-
| `<
|
|
693
|
-
| `<
|
|
694
|
-
| `<
|
|
695
|
-
| `<
|
|
696
|
-
| `<
|
|
697
|
-
| `<
|
|
698
|
-
| `<
|
|
699
|
-
| `<
|
|
700
|
-
| `<
|
|
701
|
-
| `<
|
|
553
|
+
| Element | Vue equivalent |
|
|
554
|
+
| ------------------------------------ | ---------------------- |
|
|
555
|
+
| `<ml-controls-play-button>` | `<PlayButton>` |
|
|
556
|
+
| `<ml-controls-mute-button>` | `<MuteButton>` |
|
|
557
|
+
| `<ml-controls-fullscreen-button>` | `<FullscreenButton>` |
|
|
558
|
+
| `<ml-controls-loop-button>` | `<LoopButton>` |
|
|
559
|
+
| `<ml-controls-pip-button>` | `<PipButton>` |
|
|
560
|
+
| `<ml-controls-captions-button>` | `<CaptionsButton>` |
|
|
561
|
+
| `<ml-controls-quality-button>` | `<QualityButton>` |
|
|
562
|
+
| `<ml-controls-playback-rate-button>` | `<PlaybackRateButton>` |
|
|
563
|
+
| `<ml-controls-buffering>` | `<Buffering>` |
|
|
564
|
+
| `<ml-controls-scrubber>` | `<Scrubber>` |
|
|
565
|
+
| `<ml-controls-volume-slider>` | `<VolumeSlider>` |
|
|
566
|
+
| `<ml-controls-time-display>` | `<TimeDisplay>` |
|
|
567
|
+
| `<ml-controls-transcript>` | `<Transcript>` |
|
|
702
568
|
|
|
703
569
|
### Passing objects as web component attributes
|
|
704
570
|
|
|
705
|
-
Boolean and string props map directly to HTML attributes. Object props (`payload`) can be passed as inline JSON strings:
|
|
571
|
+
Boolean and string props map directly to HTML attributes. Object props (`payload`) can be passed as inline JSON strings:
|
|
706
572
|
|
|
707
573
|
```html
|
|
708
|
-
<
|
|
574
|
+
<ml-video-card src="https://..." ad-tag-url="https://..." payload='{"articleId":"123"}'></ml-video-card>
|
|
709
575
|
```
|
|
710
576
|
|
|
711
577
|
For `action` (which contains a function), set it via JavaScript:
|
|
712
578
|
|
|
713
579
|
```js
|
|
714
|
-
const player = document.querySelector('
|
|
580
|
+
const player = document.querySelector('ml-video-card')
|
|
715
581
|
player.action = { icon: '<svg.../>', label: 'Save', onClick: () => {} }
|
|
716
582
|
```
|
|
717
583
|
|
|
718
584
|
### Listening to events
|
|
719
585
|
|
|
720
586
|
```js
|
|
721
|
-
const player = document.querySelector('
|
|
587
|
+
const player = document.querySelector('ml-video-card')
|
|
722
588
|
player.addEventListener('state-change', (e) => {
|
|
723
589
|
console.log(e.detail)
|
|
724
590
|
})
|
|
@@ -726,10 +592,10 @@ player.addEventListener('state-change', (e) => {
|
|
|
726
592
|
|
|
727
593
|
### Calling player methods
|
|
728
594
|
|
|
729
|
-
`<
|
|
595
|
+
`<ml-video-card>`/`<ml-video-stage>` expose the same forwarded API a Vue template ref would (see [Building your own wrapper component](#building-your-own-wrapper-component)) - methods and state land directly on the element itself:
|
|
730
596
|
|
|
731
597
|
```js
|
|
732
|
-
const player = document.querySelector('
|
|
598
|
+
const player = document.querySelector('ml-video-card')
|
|
733
599
|
player.togglePlay()
|
|
734
600
|
console.log(player.isPlaying)
|
|
735
601
|
```
|
|
@@ -743,15 +609,15 @@ console.log(player.isPlaying)
|
|
|
743
609
|
import { registerPlatform } from 'https://esm.sh/@munsonlabs/video-player/element/core'
|
|
744
610
|
registerPlatform({ key: 'acme', test: (url) => url.includes('acme.tv'), embed: true, createAdapter: createAcmeAdapter })
|
|
745
611
|
|
|
746
|
-
const player = document.createElement('
|
|
747
|
-
player.setAttribute('
|
|
612
|
+
const player = document.createElement('ml-video-player')
|
|
613
|
+
player.setAttribute('src', 'https://acme.tv/watch/123')
|
|
748
614
|
document.body.append(player)
|
|
749
615
|
</script>
|
|
750
616
|
```
|
|
751
617
|
|
|
752
|
-
|
|
618
|
+
**Timing note:** importing `/element`/`/element/core` calls `customElements.define()`, which synchronously upgrades any matching tag already in the DOM. A statically-declared `<ml-video-player>` gets upgraded before your `registerPlatform` call on the next line. Creating the tag from script, after registering, as above sidesteps this.
|
|
753
619
|
|
|
754
|
-
If your tags are statically declared
|
|
620
|
+
If your tags are statically declared, load `/element/core` with a **`?defer`** query instead: it skips the automatic `customElements.define()`, so you register your platform first and then call the exported `defineElements()` yourself:
|
|
755
621
|
|
|
756
622
|
```html
|
|
757
623
|
<script type="module">
|
|
@@ -760,13 +626,10 @@ If your tags are statically declared in the markup (so you can't create them aft
|
|
|
760
626
|
defineElements()
|
|
761
627
|
</script>
|
|
762
628
|
|
|
763
|
-
<
|
|
629
|
+
<ml-video-player src="https://acme.tv/watch/123"></ml-video-player>
|
|
764
630
|
```
|
|
765
631
|
|
|
766
|
-
|
|
767
|
-
- `defineElements()` is idempotent: it only defines whichever tags aren't already registered, so calling it when the elements were already defined is a no-op rather than a double-registration error.
|
|
768
|
-
|
|
769
|
-
Note this requires an `import` from wherever `/element`/`/element/core` is loaded - there's no way to reach the registry from a script that doesn't (or can't) import the package itself.
|
|
632
|
+
`?defer` is read from the `/element/core` module's own URL. `defineElements()` is idempotent: it only defines whichever tags aren't already registered.
|
|
770
633
|
|
|
771
634
|
---
|
|
772
635
|
|