@munsonlabs/video-player 0.2.5 → 0.2.7

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.
Files changed (52) hide show
  1. package/README.md +6 -694
  2. package/dist/chunks/ads-B8PU62NC.mjs +1 -0
  3. package/dist/chunks/brightcove-CPKzsnF0.mjs +1 -0
  4. package/dist/chunks/constants-Buc1xZvW.mjs +1 -0
  5. package/dist/chunks/dailymotion-DHCBjqaZ.mjs +1 -0
  6. package/dist/chunks/embedShared-R2ohosIo.mjs +1 -0
  7. package/dist/chunks/jwplayer-D8oOCmi4.mjs +1 -0
  8. package/dist/chunks/sourceHelpers-BNIZ7kmT.mjs +1 -0
  9. package/dist/chunks/vimeo-DC3q3YWJ.mjs +1 -0
  10. package/dist/chunks/youtube-e1csmnHD.mjs +1 -0
  11. package/dist/elements/chunks/ads-B8PU62NC.mjs +1 -0
  12. package/dist/elements/chunks/brightcove-CPKzsnF0.mjs +1 -0
  13. package/dist/elements/chunks/constants-Buc1xZvW.mjs +1 -0
  14. package/dist/elements/chunks/dailymotion-DHCBjqaZ.mjs +1 -0
  15. package/dist/elements/chunks/embedShared-R2ohosIo.mjs +1 -0
  16. package/dist/elements/chunks/font-KFHs3p_4-KFHs3p_4.mjs +1 -0
  17. package/dist/elements/chunks/jwplayer-D8oOCmi4.mjs +1 -0
  18. package/dist/elements/chunks/sourceHelpers-BNIZ7kmT.mjs +1 -0
  19. package/dist/elements/chunks/svg-BLBI83Ed-BjsPx0LR.mjs +1 -0
  20. package/dist/elements/chunks/vimeo-DC3q3YWJ.mjs +1 -0
  21. package/dist/elements/chunks/youtube-e1csmnHD.mjs +1 -0
  22. package/dist/elements/controls.css +1 -1
  23. package/dist/elements/controls.d.mts +23 -17
  24. package/dist/elements/controls.mjs +1 -1
  25. package/dist/elements/core.css +1 -1
  26. package/dist/elements/core.d.mts +32 -23
  27. package/dist/elements/core.mjs +1 -1
  28. package/dist/elements/index.d.mts +53 -40
  29. package/dist/elements/index.mjs +1 -1
  30. package/dist/elements/style.css +1 -1
  31. package/dist/index.d.mts +138 -88
  32. package/dist/index.mjs +1 -1
  33. package/dist/style.css +1 -1
  34. package/package.json +7 -6
  35. package/dist/chunks/ads-Dph3guS9.mjs +0 -1
  36. package/dist/chunks/brightcove-DiJF_74z.mjs +0 -1
  37. package/dist/chunks/constants-35ZnLsDv.mjs +0 -1
  38. package/dist/chunks/dailymotion-DTooszoV.mjs +0 -1
  39. package/dist/chunks/embedShared-CbArTtuR.mjs +0 -1
  40. package/dist/chunks/jwplayer-Dhq65VxK.mjs +0 -1
  41. package/dist/chunks/sourceHelpers-B0tH-grj.mjs +0 -1
  42. package/dist/chunks/vimeo-D0GAjId0.mjs +0 -1
  43. package/dist/chunks/youtube-B8AT2F-4.mjs +0 -1
  44. package/dist/elements/chunks/ads-Dph3guS9.mjs +0 -1
  45. package/dist/elements/chunks/brightcove-DiJF_74z.mjs +0 -1
  46. package/dist/elements/chunks/constants-35ZnLsDv.mjs +0 -1
  47. package/dist/elements/chunks/dailymotion-DTooszoV.mjs +0 -1
  48. package/dist/elements/chunks/embedShared-CbArTtuR.mjs +0 -1
  49. package/dist/elements/chunks/jwplayer-Dhq65VxK.mjs +0 -1
  50. package/dist/elements/chunks/sourceHelpers-B0tH-grj.mjs +0 -1
  51. package/dist/elements/chunks/vimeo-D0GAjId0.mjs +0 -1
  52. package/dist/elements/chunks/youtube-B8AT2F-4.mjs +0 -1
package/README.md CHANGED
@@ -8,9 +8,7 @@ A Vue 3 video player supporting YouTube, Vimeo, Dailymotion, Brightcove, JW Play
8
8
  npm install @munsonlabs/video-player
9
9
  ```
10
10
 
11
- ## Vue Usage
12
-
13
- ### Basic player
11
+ ## Usage
14
12
 
15
13
  ```vue
16
14
  <script setup>
@@ -21,700 +19,14 @@ import '@munsonlabs/video-player/style'
21
19
  <template>
22
20
  <VideoCard
23
21
  label="Big Buck Bunny"
24
- src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4"
25
- poster="https://m.media-amazon.com/images/S/pv-target-images/fb7afef01282cdc2d846b2343f9f3d7a785b7133729776f1aa0da6501a2e1f7b.jpg"
22
+ src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"
23
+ poster="https://img.youtube.com/vi/aqz-KE-bpKQ/0.jpg"
26
24
  />
27
25
  </template>
28
26
  ```
29
27
 
30
- ### Pinning a single player
31
-
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
-
34
- ```vue
35
- <VideoCard src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4" pin="bottom-right" />
36
- ```
37
-
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
- - 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` to disable pinning entirely - the player then just auto-pauses offscreen as usual.
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
-
43
- ### Stage player
44
-
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
-
47
- ```vue
48
- <script setup>
49
- import { VideoStage, VideoCard } from '@munsonlabs/video-player'
50
- import '@munsonlabs/video-player/style'
51
- </script>
52
-
53
- <template>
54
- <VideoStage @state-change="onStateChange" />
55
- <VideoCard v-for="video in videos" :key="video.src" v-bind="video" />
56
- </template>
57
- ```
58
-
59
- ### Playlists
60
-
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
-
63
- ```vue
64
- <VideoStage :playlist="videos" @state-change="onStateChange" />
65
- ```
66
-
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`:
69
-
70
- ```vue
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>
74
- ```
75
-
76
- ### Hiding the pinned stage over content
77
-
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.
79
-
80
- ```vue
81
- <script setup>
82
- import { VideoStage, HideMarker } from '@munsonlabs/video-player'
83
- </script>
84
-
85
- <template>
86
- <VideoStage pin="bottom-right" :playlist="videos" />
87
-
88
- <!-- ...page content... -->
89
-
90
- <HideMarker />
91
- <Footer />
92
- </template>
93
- ```
94
-
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.
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.
98
-
99
- ---
100
-
101
- ## VideoCard / VideoPlayer Props
102
-
103
- | Prop | Type | Default | Description |
104
- | ------------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
105
- | `src` | `string` | - | **Required.** The video URL or platform-specific URI (the platform is auto-detected from its shape, see [Platform URLs](#platform-urls)) |
106
- | `label` | `string` | `''` | Video label, shown on the placeholder and used as the player's accessible name |
107
- | `poster` | `string` | `''` | Poster image URL |
108
- | `aspectRatio` | `string` | `'16:9'` | e.g. `'16:9'`, `'9:16'`, `'4:3'` |
109
- | `autoplay` | `boolean` | `false` | Autoplay on mount, see [Autoplay, mute & volume](#autoplay-mute--volume) |
110
- | `muted` | `boolean` | - | Force a starting mute state - if unset, follows the shared audio preference, see [Autoplay, mute & volume](#autoplay-mute--volume) |
111
- | `volume` | `number` | - | Force a starting volume (0-1) - if unset, follows the shared audio preference, see [Autoplay, mute & volume](#autoplay-mute--volume) |
112
- | `lazy` | `boolean` | `true` | `VideoCard` only: show the placeholder until clicked |
113
- | `nativeUi` | `boolean` | `false` | Use the platform's native controls (YouTube, Vimeo, Dailymotion only) |
114
- | `autoStage` | `boolean` | `false` | `VideoCard` only: send this video to the stage on mount |
115
- | `playbackRate` | `number` | `1` | Initial playback rate |
116
- | `loop` | `boolean` | `false` | Start with looping on - reactive, so flipping it later is the same as calling `toggleLoop()` |
117
- | `preload` | `PreloadMode` | - | `'none' \| 'metadata' \| 'auto'` - the native `<video preload>` attribute. `'none'` defers loading for MP4 and HLS alike (hls.js is held back until the first play), not DASH; other values leave HLS as normal |
118
- | `adTagUrl` | `string` | `''` | VAST or VMAP ad tag URL |
119
- | `adMacroParams` | `object` | - | Fills `{macro}` tokens on whichever ad tag URL ends up in use, see [Ads](#ads-ima--vast--vmap) |
120
- | `headerBidding` | `object` | - | Runs a Prebid.js auction before the ad plays, see [Header bidding](#header-bidding-prebidjs) |
121
- | `tracks` | `array` | - | WebVTT caption/subtitle tracks, see [Captions](#captions-webvtt) |
122
- | `payload` | `object` | `{}` | Arbitrary data attached to every `state-change` event |
123
- | `action` | `PlayerAction` | `null` | Button shown in the player HUD, see [Actions](#actions) |
124
- | `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) |
125
- | `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) |
126
- | `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) |
127
- | `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) |
128
-
129
- ### Autoplay, mute & volume
130
-
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.
132
-
133
- ### Platform URLs
134
-
135
- The platform is detected automatically from the shape of `src`; there's no separate prop to set it.
136
-
137
- | Platform | URL format |
138
- | ------------- | -------------------------------------------------------------------------------------------------------- |
139
- | `youtube` | `https://www.youtube.com/watch?v=VIDEO_ID` |
140
- | `vimeo` | `https://vimeo.com/VIDEO_ID` |
141
- | `dailymotion` | `https://www.dailymotion.com/video/VIDEO_ID` |
142
- | `brightcove` | `https://players.brightcove.net/ACCOUNT_ID/PLAYER_ID_EMBED/index.html?videoId=VIDEO_ID`\* |
143
- | `jwplayer` | `https://cdn.jwplayer.com/videos/...` · `https://cdn.jwplayer.com/manifests/...` · `jwplayer://MEDIA_ID` |
144
- | `html5` | Any direct MP4 / HLS / DASH URL |
145
-
146
- \* `PLAYER_ID_EMBED` is one path segment: `{PLAYER_ID}_{EMBED_NAME}` (e.g. `abc123_default` for the account's default player), which is exactly the URL Brightcove Studio's "Publish" panel gives you for an embed.
147
-
148
- ### Adding a custom platform
149
-
150
- `registerPlatform` teaches the player about a platform beyond the six above, without forking the package:
151
-
152
- ```ts
153
- import { registerPlatform } from '@munsonlabs/video-player'
154
-
155
- registerPlatform({
156
- key: 'acme',
157
- test: (url) => url.includes('acme.tv'),
158
- embed: true, // true: the platform has its own SDK/iframe; false: it resolves to a plain playable file
159
- createAdapter: (videoEl, options) => createAcmeAdapter(videoEl, options),
160
- })
161
- ```
162
-
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.
164
-
165
- - **`embed: true`** - the platform owns its own player (YouTube, Twitch, Vimeo). `createAdapter` returns a full `PlaybackAdapter`: `play` (returns a promise, resolved once the request is issued)/`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.
167
-
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.
169
-
170
- ---
171
-
172
- ## Headless Controls
173
-
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:
175
-
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.
177
-
178
- ```ts
179
- import {
180
- PlayButton,
181
- MuteButton,
182
- FullscreenButton,
183
- LoopButton,
184
- PipButton,
185
- CaptionsButton,
186
- QualityButton,
187
- PlaybackRateButton,
188
- Buffering,
189
- Scrubber,
190
- VolumeSlider,
191
- TimeDisplay,
192
- Transcript,
193
- } from '@munsonlabs/video-player'
194
- ```
195
-
196
- Every control takes a `player` prop pointing at the same object `VideoPlayer`/`VideoCard`/`VideoStage` expose via a template ref:
197
-
198
- ```vue
199
- <script setup>
200
- import { ref } from 'vue'
201
- import { VideoPlayer, PlayButton, MuteButton, Scrubber } from '@munsonlabs/video-player'
202
- const player = ref(null)
203
- </script>
204
-
205
- <template>
206
- <VideoPlayer ref="player" src="..." :controls="false" />
207
- <PlayButton :player="player" />
208
- <MuteButton :player="player" />
209
- <Scrubber :player="player" />
210
- </template>
211
- ```
212
-
213
- Or, `<label for>`-style, skip the ref and point at an element id instead:
214
-
215
- ```vue
216
- <VideoPlayer id="my-player" src="..." :controls="false" />
217
- <PlayButton for="my-player" />
218
- ```
219
-
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.
221
-
222
- ### The player handle
223
-
224
- A template ref on `VideoPlayer`/`VideoCard`/`VideoStage` (or the `player` injected into a headless control) exposes the `PlayerHandle` type. The commonly used part:
225
-
226
- | Member | Notes |
227
- | -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
228
- | `play()` / `pause()` / `togglePlay()` | `play()` returns a promise that resolves once playing and rejects on an autoplay block/error |
229
- | `replay()` | Seeks to 0 and plays; what the HUD button does after `ended` |
230
- | `seekTo(seconds)` / `seek(percent)` | Seconds for transcripts, chapters, deep links; percent for scrubbers |
231
- | `isLoaded` / `isReady` / `isPlaying` | `isLoaded` means the duration is known; `isReady` only means the adapter is attached |
232
- | `hasEnded` / `isError` / `retry()` | `retry()` reloads the current source after an error |
233
- | `current` / `total` / `isLive` | Seconds |
234
- | `isMuted` / `vol` / `toggleMute()` / `setVolume()` | Volume is 0-1 |
235
-
236
- ### Transcript
237
-
238
- `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:
239
-
240
- ```vue
241
- <VideoPlayer id="my-player" src="..." />
242
- <Transcript
243
- for="my-player"
244
- :cues="[
245
- { time: 0, text: 'Welcome back to the show.' },
246
- { time: 12, text: 'Today we look at the new release.' },
247
- { time: 47, end: 60, text: 'Here is the demo.' },
248
- ]"
249
- />
250
- ```
251
-
252
- - Works on **every** platform (YouTube/Vimeo/Dailymotion included) - only needs current time and `seek`.
253
- - `end` is optional: when set, nothing is highlighted between that cue's `end` and the next cue's start.
254
- - Auto-scroll pauses while the pointer is over the list. A scoped slot customises each cue's markup: `#default="{ cue, index, isActive, formatTime }"`.
255
- - As a custom element (`<ml-controls-transcript>`), pass cues as an inline JSON string attribute: `cues='[{"time":0,"text":"..."}]'`.
256
-
257
- ### Building your own wrapper component
258
-
259
- `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:
260
-
261
- ```vue
262
- <script setup>
263
- import { useForwardedPlayer } from '@munsonlabs/video-player'
264
- const { playerRef, forwarded } = useForwardedPlayer()
265
-
266
- defineExpose(forwarded)
267
- </script>
268
-
269
- <template>
270
- <VideoPlayer ref="playerRef" v-bind="$attrs" />
271
- </template>
272
- ```
273
-
274
- `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 `PlayerHandle` API `VideoPlayer` exposes (methods return promises there, since the optional `guard` callback that runs before each call may be async).
275
-
276
- 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.
277
-
278
- ### Exposing a plain Vue player to independent, non-Vue code
279
-
280
- 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):
281
-
282
- ```vue
283
- <script setup>
284
- import { ref, onMounted } from 'vue'
285
- import { VideoPlayer, exposePlayerOnElement } from '@munsonlabs/video-player'
286
-
287
- const playerRef = ref(null)
288
- const wrapperEl = ref(null)
289
- onMounted(() => exposePlayerOnElement(wrapperEl.value, playerRef.value))
290
- </script>
291
-
292
- <template>
293
- <div id="my-player" ref="wrapperEl">
294
- <VideoPlayer ref="playerRef" src="..." />
295
- </div>
296
- </template>
297
- ```
298
-
299
- ```html
300
- <!-- a completely separate script/bundle, e.g. a third party's own -->
301
- <script type="module">
302
- import '@munsonlabs/video-player/element/controls'
303
- </script>
304
- <ml-controls-transcript for="my-player" cues='[{"time":0,"text":"..."}]'></ml-controls-transcript>
305
- ```
306
-
307
- 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.
308
-
309
- ---
310
-
311
- ## Events
312
-
313
- ### `state-change`
314
-
315
- Emitted by `VideoCard`, `VideoPlayer`, and `VideoStage`. Every event includes `currentTime`, `duration`, `src`, and `payload` (arbitrary user data).
316
-
317
- | Event | When it fires | Extra fields |
318
- | -------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
319
- | `loaded` | Duration is known (or the stream is detected as live); `isLoaded` flips true | — |
320
- | `play` | Playback starts or resumes | — |
321
- | `pause` | Playback pauses | — |
322
- | `ended` | Playback reaches the end | — |
323
- | `seeked` | Seek completes | — |
324
- | `error` | Source fails to load/play; player shows a "Retry" button | `error: { code, message }` |
325
- | `adstart` | Ad begins playing | — |
326
- | `adend` | Ad finishes | — |
327
- | `volumechange` | Volume or mute state changes | `isMuted` |
328
- | `ratechange` | Playback rate changes | `playbackRate` |
329
- | `captionchange` | Active caption track changes (fires on `setCaptionTrack()` and ABR-driven changes, but not at mount) | `captionIndex` (null = off) |
330
- | `qualitychange` | Active quality level changes (fires on `setQuality()` and ABR-driven changes, but not at mount) | `qualityIndex` (null = Auto) |
331
- | `pipchange` | PiP entered or exited (from player button or browser floating window) | `isPipActive` |
332
- | `loopchange` | Loop toggled (not before playback starts) | `isLooping` |
333
- | `firstQuartile` | Playback crosses 25% of duration | — |
334
- | `midpoint` | Playback crosses 50% of duration | — |
335
- | `thirdQuartile` | Playback crosses 75% of duration | — |
336
- | `controlsopen` / `controlsclose` | Expanded controls popup mounts/unmounts | `element` (popup root node; on close, a detached snapshot) |
337
- | `stageopen` / `stageclose` | Stage mounts its inner player / tears it down (stage only) | — |
338
- | `bufferstart` / `bufferend` | Buffering begins (after stall delay) / resolves | — |
339
- | `timeupdate` | Regular time update during playback | — |
340
- | `tap` | Full-video tap-to-reveal overlay is tapped; only signal for `controls: false` consumers | — |
341
-
342
- `firstQuartile`/`midpoint`/`thirdQuartile` reset and fire again on loop restart. `captionchange`/`qualitychange` never fire before playback starts.
343
-
344
- ---
345
-
346
- ## Actions
347
-
348
- The `action` prop adds a button to the player HUD. Pass a built-in string or a custom action object.
349
-
350
- ### Built-in actions
351
-
352
- ```vue
353
- <VideoCard action="mute" ... />
354
- <VideoCard action="loop" ... />
355
- <VideoCard action="autoplay" ... />
356
- ```
357
-
358
- `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.
359
-
360
- ### Custom action
361
-
362
- Pass an object with `icon` (SVG string), `label`, and `onClick`:
363
-
364
- ```vue
365
- <VideoCard :action="{ icon: '<svg .../>', label: 'Save', onClick: () => console.log('saved') }" ... />
366
- ```
367
-
368
- ---
369
-
370
- ## Ads (IMA / VAST / VMAP)
371
-
372
- **Step by step:**
373
-
374
- 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.
375
- 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.
376
- 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.
377
- 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.
378
-
379
- ```vue
380
- <VideoCard src="https://..." ad-tag-url="https://pubads.g.doubleclick.net/..." />
381
- ```
382
-
383
- Ads are not supported on YouTube, Vimeo, or Dailymotion.
384
-
385
- 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:
386
-
387
- ```vue
388
- <VideoCard src="https://players.brightcove.net/..." :ad-tag-params="{ adUnit: 'network/section', referenceId: videoId, rdid: deviceId }" />
389
- ```
390
-
391
- 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.
392
-
393
- ### Header bidding (Prebid.js)
394
-
395
- **Step by step:**
396
-
397
- 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.
398
- 2. Get a Prebid **video ad unit** config from ad ops (with `code`, `mediaTypes.video`, and `bids`) and the GAM ad unit path (`iu`).
399
- 3. Pass both as `headerBidding: { adUnit, params: { iu: '...' } }`, alongside a plain `adTagUrl`/`adMacroParams` as fallback. Never rely on `headerBidding` alone.
400
- 4. Verify: run `pbjs.getBidResponsesForAdUnitCode('your-ad-unit-code')` in the console after playing.
401
-
402
- 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:
403
-
404
- ```vue
405
- <VideoCard
406
- src="https://players.brightcove.net/..."
407
- :header-bidding="{
408
- adUnit: {
409
- code: 'video-preroll',
410
- mediaTypes: { video: { context: 'instream', playerSize: [640, 480] } },
411
- bids: [{ bidder: 'appnexus', params: { placementId: 123456 } }],
412
- },
413
- params: { iu: '/network/adunit' },
414
- timeoutMs: 1000,
415
- }"
416
- />
417
- ```
418
-
419
- `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.
420
-
421
- ### Ad overlay UI
422
-
423
- While an ad is playing, a small overlay shows an "Ad" badge with a countdown to the ad's end, a dedicated pause/resume button, and a mute button that controls the ad creative's own independent audio (separate from the content video's volume/mute state: muting the player has no effect on ad audio, and vice versa).
424
-
425
- ### Ad visibility behaviour
426
-
427
- 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.
428
-
429
- ---
430
-
431
- ## Captions (WebVTT)
432
-
433
- Only meaningful for the native `<video>` path (plain MP4/HLS): captions are unsupported on YouTube, Vimeo, and Dailymotion, which render their own captions inside their iframe instead if the source video has them. Brightcove and JW Player get real support for free, since both resolve to an actual `<video>`/HLS source under the hood.
434
-
435
- There are two ways a caption track ends up available, and you don't have to pick: the player supports both at once.
436
-
437
- - **HLS streams that already declare subtitle renditions in their own manifest** get picked up automatically by hls.js/Safari's native HLS, with no `tracks` prop needed at all. A "Captions" option just appears in the controls menu once the manifest has parsed.
438
- - **Everything else** (a plain MP4, or an HLS stream with no subtitle rendition of its own) needs WebVTT tracks supplied explicitly via `tracks`:
439
-
440
- ```vue
441
- <VideoCard
442
- src="https://.../video.mp4"
443
- :tracks="[
444
- { src: '/captions/en.vtt', kind: 'captions', srclang: 'en', label: 'English', default: true },
445
- { src: '/captions/fr.vtt', kind: 'captions', srclang: 'fr', label: 'Français' },
446
- ]"
447
- />
448
- ```
449
-
450
- `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.
451
-
452
- 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.
453
-
454
- ---
455
-
456
- ## Video Quality (HLS / DASH)
457
-
458
- 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.
459
-
460
- 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.
461
-
462
- **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.
463
-
464
- Programmatically, a template ref on `VideoPlayer` exposes `setQuality(index)` (`null` re-enables Auto); see [Events](#state-change) for the `qualitychange` event this fires.
465
-
466
- ---
467
-
468
- ## Picture-in-Picture
469
-
470
- A Picture-in-Picture button appears in the controls automatically wherever the browser supports it (`document.pictureInPictureEnabled`), nothing to configure. It stays in sync whether PiP is toggled from the player's own button or the browser's floating window is closed directly.
471
-
472
- Only supported on the native `<video>` path (plain MP4/HLS): YouTube, Vimeo, and Dailymotion are unsupported here since their iframe content isn't a real `<video>` element this package controls directly. Brightcove and JW Player get it for free, same reasoning as captions/quality above.
473
-
474
- Programmatically, a template ref on `VideoPlayer` exposes `togglePip()`; see [Events](#state-change) for the `pipchange` event this fires.
475
-
476
- ---
477
-
478
- ## Theming the HUD buttons
479
-
480
- Set the variables on any ancestor of the player (e.g. a wrapper `<div>` or `:root`) to restyle all three buttons at once:
481
-
482
- ```css
483
- .my-video-wrapper {
484
- --mlv-btn-bg: #e11d48;
485
- --mlv-btn-color: #fff;
486
- }
487
- ```
488
-
489
- 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.
490
-
491
- ### Full controls popup
492
-
493
- | Variable | Default | Description |
494
- | ---------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
495
- | `--mlv-controls-width` | `min(450px, calc(100% - 32px))` | Controls popup width |
496
- | `--mlv-popup-align` | `center` | Horizontal alignment of the popup (`center` or `flex-end` to dock at the bottom) |
497
- | `--mlv-btn-bg` | (frosted glass) | Background of action, play/pause, and show-controls buttons |
498
- | `--mlv-btn-color` | (frosted glass) | Icon/text colour of the same buttons |
499
- | `--mlv-stage-tuck` | `32px` | Width of the sliver left visible when `HideMarker` tucks the pinned stage off-screen |
500
- | `--mlv-radius` | `12px` | Border-radius of the pinned corner box |
501
- | `--mlv-accent` | — | Active-state highlight colour for the loop/autoplay toggle buttons in the controls popup |
502
-
503
- Real fullscreen always docks the popup to the bottom regardless of `--mlv-popup-align`.
504
-
505
- ### Fullscreen controls
506
-
507
- 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.
508
-
509
- ---
510
-
511
- ## Web Component Usage
512
-
513
- 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.
514
-
515
- Three bundles are available: pick exactly one, since importing more than one double-registers any tag they share and throws:
516
-
517
- - `@munsonlabs/video-player/element`: everything (player + all headless controls). Auto-injects its own CSS.
518
- - `@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.
519
- - `@munsonlabs/video-player/element/controls`: just the `<ml-controls-*>` primitives. Import `@munsonlabs/video-player/style/controls` alongside it.
520
-
521
- `/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.
522
-
523
- ### From a CDN, with no build step
524
-
525
- ```html
526
- <!--
527
- Vue's esm-bundler build expects its compile-time feature flags to be substituted by a
528
- bundler. Nothing does that on a CDN, and `VueElement._mount` reads this one before Vue
529
- can self-default it — so without this line every element registers, appears in the DOM,
530
- and renders nothing, with the ReferenceError buried in `connectedCallback`.
531
-
532
- It must be its own script: `import` is hoisted, so an assignment inside the module
533
- below would run after the bundle had already mounted.
534
- -->
535
- <script>
536
- globalThis.__VUE_PROD_DEVTOOLS__ = false
537
- </script>
538
-
539
- <script type="importmap">
540
- {
541
- "imports": {
542
- "@munsonlabs/video-player/element": "https://esm.sh/@munsonlabs/video-player/element"
543
- }
544
- }
545
- </script>
546
-
547
- <script type="module">
548
- import '@munsonlabs/video-player/element'
549
- </script>
550
-
551
- <ml-video-card
552
- src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4"
553
- label="Big Buck Bunny"
554
- poster="https://m.media-amazon.com/images/S/pv-target-images/fb7afef01282cdc2d846b2343f9f3d7a785b7133729776f1aa0da6501a2e1f7b.jpg"
555
- ></ml-video-card>
556
- ```
557
-
558
- esm.sh rewrites every bare specifier in the bundle to its own URLs — `vue`, and the `hls.js`
559
- and `dashjs` that an HLS or DASH source pulls in on demand — so that one global is the entire
560
- setup. The import map above only aliases the package name for readability; dropping it and
561
- importing the full esm.sh URL directly works just as well.
562
-
563
- ### With a bundler
564
-
565
- None of the above applies — your bundler substitutes Vue's feature flags at build time, so
566
- `import '@munsonlabs/video-player/element'` is all you need.
567
-
568
- ### Supplying your own Vue instead
569
-
570
- If you already have Vue on the page, or want to control which build is used, mark it external
571
- and map it yourself. Vue's `esm-browser` builds have the flags already substituted, so no
572
- global is needed:
573
-
574
- ```html
575
- <script type="importmap">
576
- {
577
- "imports": {
578
- "vue": "https://unpkg.com/vue@3/dist/vue.runtime.esm-browser.prod.js",
579
- "@munsonlabs/video-player/element": "https://esm.sh/@munsonlabs/video-player/element?external=vue"
580
- }
581
- }
582
- </script>
583
- ```
584
-
585
- Without `?external=vue`, esm.sh resolves Vue itself and a bare `vue` import map entry is
586
- never consulted — it has already rewritten the specifier to its own URL.
587
-
588
- `?external=vue` externalises **only** Vue. `hls.js` and `dashjs` stay resolved by esm.sh, so
589
- nothing else changes here.
590
-
591
- ### Self-hosting, or loading raw files
592
-
593
- Serving `dist/` yourself — or loading raw file paths from unpkg/jsdelivr rather than esm.sh —
594
- gives you the bundle byte for byte, with every bare specifier intact. Nothing rewrites them, so
595
- map all three yourself:
596
-
597
- ```html
598
- <script type="importmap">
599
- {
600
- "imports": {
601
- "vue": "https://unpkg.com/vue@3/dist/vue.runtime.esm-browser.prod.js",
602
- "hls.js": "https://cdn.jsdelivr.net/npm/hls.js@1/+esm",
603
- "dashjs": "https://cdn.jsdelivr.net/npm/dashjs@4/+esm"
604
- }
605
- }
606
- </script>
607
- ```
608
-
609
- `hls.js` and `dashjs` are each loaded on demand via a dynamic `import()` when an `.m3u8` or
610
- `.mpd` source mounts, so omitting them fails at that moment rather than at load — skip them
611
- only if you are certain neither format will ever play.
612
-
613
- Platform adapter chunks are relative paths, not bare specifiers, so they need no entry — just
614
- deploy `chunks/` alongside the bundle.
615
-
616
- ### Available elements
617
-
618
- Core (in `/element` and `/element/core`):
619
-
620
- | Element | Vue equivalent |
621
- | ------------------------ | -------------------- |
622
- | `<ml-video-card>` | `<VideoCard>` |
623
- | `<ml-video-stage>` | `<VideoStage>` |
624
- | `<ml-video-player>` | `<VideoPlayer>` |
625
- | `<ml-video-placeholder>` | `<VideoPlaceholder>` |
626
- | `<ml-hide-marker>` | `<HideMarker>` |
627
-
628
- Controls (in `/element` and `/element/controls`), see [Headless Controls](#headless-controls) for what each does:
629
-
630
- | Element | Vue equivalent |
631
- | ------------------------------------ | ---------------------- |
632
- | `<ml-controls-play-button>` | `<PlayButton>` |
633
- | `<ml-controls-mute-button>` | `<MuteButton>` |
634
- | `<ml-controls-fullscreen-button>` | `<FullscreenButton>` |
635
- | `<ml-controls-loop-button>` | `<LoopButton>` |
636
- | `<ml-controls-pip-button>` | `<PipButton>` |
637
- | `<ml-controls-captions-button>` | `<CaptionsButton>` |
638
- | `<ml-controls-quality-button>` | `<QualityButton>` |
639
- | `<ml-controls-playback-rate-button>` | `<PlaybackRateButton>` |
640
- | `<ml-controls-buffering>` | `<Buffering>` |
641
- | `<ml-controls-scrubber>` | `<Scrubber>` |
642
- | `<ml-controls-volume-slider>` | `<VolumeSlider>` |
643
- | `<ml-controls-time-display>` | `<TimeDisplay>` |
644
- | `<ml-controls-transcript>` | `<Transcript>` |
645
-
646
- ### Passing objects as web component attributes
647
-
648
- Boolean and string props map directly to HTML attributes. Object props (`payload`) can be passed as inline JSON strings:
649
-
650
- ```html
651
- <ml-video-card src="https://..." ad-tag-url="https://..." payload='{"articleId":"123"}'></ml-video-card>
652
- ```
653
-
654
- For `action` (which contains a function), set it via JavaScript:
655
-
656
- ```js
657
- const player = document.querySelector('ml-video-card')
658
- player.action = { icon: '<svg.../>', label: 'Save', onClick: () => {} }
659
- ```
660
-
661
- ### Listening to events
662
-
663
- ```js
664
- const player = document.querySelector('ml-video-card')
665
- player.addEventListener('state-change', (e) => {
666
- console.log(e.detail)
667
- })
668
- ```
669
-
670
- ### Calling player methods
671
-
672
- `<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:
673
-
674
- ```js
675
- const player = document.querySelector('ml-video-card')
676
- await player.play() // resolves once playing, rejects on an autoplay block or media error
677
- player.seekTo(30)
678
- console.log(player.isPlaying, player.isLoaded)
679
- ```
680
-
681
- ### Registering a platform from a web component
682
-
683
- [`registerPlatform`](#adding-a-custom-platform) is exported from `/element` and `/element/core` too:
684
-
685
- ```html
686
- <script type="module">
687
- import { registerPlatform } from 'https://esm.sh/@munsonlabs/video-player/element/core'
688
- registerPlatform({ key: 'acme', test: (url) => url.includes('acme.tv'), embed: true, createAdapter: createAcmeAdapter })
689
-
690
- const player = document.createElement('ml-video-player')
691
- player.setAttribute('src', 'https://acme.tv/watch/123')
692
- document.body.append(player)
693
- </script>
694
- ```
695
-
696
- **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.
697
-
698
- 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:
699
-
700
- ```html
701
- <script type="module">
702
- import { registerPlatform, defineElements } from 'https://esm.sh/@munsonlabs/video-player/element/core?defer'
703
- registerPlatform({ key: 'acme', test: (url) => url.includes('acme.tv'), embed: true, createAdapter: createAcmeAdapter })
704
- defineElements()
705
- </script>
706
-
707
- <ml-video-player src="https://acme.tv/watch/123"></ml-video-player>
708
- ```
709
-
710
- `?defer` is read from the `/element/core` module's own URL. `defineElements()` is idempotent: it only defines whichever tags aren't already registered.
711
-
712
- ---
28
+ `VideoPlayer`, `VideoStage`, headless controls, and web component (`ml-video-*`) exports are also available - see the docs for the full component list.
713
29
 
714
- ## Peer Dependencies
30
+ ## Docs
715
31
 
716
- | Package | Version |
717
- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
718
- | `vue` | `>=3` (Vue usage only) |
719
- | `hls.js` | `^1.6.16`, optional, only needed for HLS (`.m3u8`) playback via native `<video>`; see [Web Component Usage](#web-component-usage) for the import map entry if you're not using a bundler |
720
- | `dashjs` | `^4.7.4`, optional, only needed for DASH (`.mpd`) playback via native `<video>`; see [Web Component Usage](#web-component-usage) for the import map entry if you're not using a bundler |
32
+ Full guides for props, events, ads, captions, quality, theming, headless controls, and web component usage: **https://munsonlabs.github.io/packages/video-player/getting-started/introduction**