@munsonlabs/video-player 0.2.0 → 0.2.1

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 CHANGED
@@ -58,27 +58,19 @@ import '@munsonlabs/video-player/style'
58
58
 
59
59
  ### Playlists
60
60
 
61
- Pass `playlist` (a `VideoEntry[]`, the same shape as a `VideoItem`'s props) to let the stage track the currently-playing video's position within it. This works alongside the existing `video-select`/`video-toggle` window events, not instead of them: position is derived by matching the current video's URL against the playlist on every read, so an external selection (e.g. clicking a `VideoItem` elsewhere on the page) naturally updates "where we are in the playlist" too, with nothing extra to keep in sync.
61
+ Pass `playlist` (a `VideoEntry[]`, the same shape as a `VideoItem`'s props) to let the stage track the currently-playing video's position within it. Position updates automatically when a user clicks a `VideoItem` 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 playlist entry automatically once the current one fires `ended`, with no wraparound, so playback simply stops after the last entry) is fully owned by `VideoStage`: it's remembered in `localStorage` and toggled only via the controls popup's built-in "Auto" button (and, if enabled, the `action="autoplay"` HUD button, see [Actions](#actions)). There's no prop for it: every consumer gets the same persisted, in-player toggle with zero wiring.
68
- - Whenever `playlist` is set, the controls popup automatically shows a skip-to-next button (only while there's a next entry) and the "Auto" toggle described above, with no extra markup needed.
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
- <script setup>
73
- import { ref } from 'vue'
74
- const stage = ref(null)
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
@@ -101,8 +93,8 @@ 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: `--mvp-stage-tuck` (default `32px`).
105
- - `HideMarker` renders a single, unstyled, full-width `div` with `min-height: 1px` - it has no visible chrome of its own, and no slot for content. Because it's only 1px tall, it triggers briefly as the page scrolls past that exact line rather than staying "hidden" for as long as some section below it remains on screen; place it precisely where you want the stage to tuck away.
96
+ - The sliver width is a CSS custom property: `--mvp-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
 
@@ -136,23 +128,7 @@ import { VideoStage, HideMarker } from '@munsonlabs/video-player'
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`, updated whenever a viewer actually presses a mute button or drags a volume slider) - a newly-mounted or completely independent player starts at whatever level the viewer last chose anywhere else, instead of always resetting to full volume/unmuted.
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
 
@@ -186,27 +162,10 @@ 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
- **Embed** (`embed: true`) - the platform owns its own player (YouTube, Twitch, Vimeo). `createAdapter` is a factory returning a full `PlaybackAdapter`: every method the player needs (`play`/`pause`/`currentTime`/captions/quality/PiP/fullscreen/`on`/`off`/`dispose`), translating your SDK's own events into the ones the player listens for (`play`, `pause`, `ended`, `timeupdate`, `durationchange`, etc.).
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
198
-
199
- What happens between setting `video-url` and an embed (YouTube/Vimeo/Dailymotion/a custom platform) actually appearing on screen, end to end:
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.
200
167
 
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
 
@@ -262,7 +221,7 @@ Or, `<label for>`-style, skip the ref and point at an element id instead:
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 as playback progresses:
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
227
  <VideoPlayer id="my-player" video-url="..." />
@@ -276,12 +235,10 @@ Or, `<label for>`-style, skip the ref and point at an element id instead:
276
235
  />
277
236
  ```
278
237
 
279
- - Unlike captions/quality, this works on **every** platform (YouTube/Vimeo/Dailymotion included) - it only needs the current time and `seek`, which every adapter has.
280
- - `end` is optional: when set, nothing is highlighted between that cue's `end` and the next cue's start (a silence/music gap); when omitted, a cue stays highlighted until the next one starts.
281
- - Auto-scroll pauses while the pointer is over the list, so it never fights the user's own scrolling.
282
- - A scoped slot customises each cue's markup: `#default="{ cue, index, isActive, formatTime }"`.
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 }"`.
283
241
  - 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.
285
242
 
286
243
  ### Building your own wrapper component
287
244
 
@@ -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 a method call before it reaches the underlying player - return `false` to swallow it, or run a side effect (e.g. mounting a lazy placeholder first) before returning `true`.
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.
304
261
 
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.
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.
306
263
 
307
- ---
264
+ ### Exposing a plain Vue player to independent, non-Vue code
308
265
 
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.
360
-
361
- `stageopen` / `stageclose` fire from `VideoStage` only, when the stage mounts its inner player (the first video arrives) and when it tears it down again. Like `controlsopen`/`controlsclose`, they arrive on the same `state-change` stream rather than as a separate event.
362
-
363
- `captionchange` / `qualitychange` fire on every `setCaptionTrack()`/`setQuality()` call (whichever menu triggered it) and whenever the adapter detects the active track/level changed on its own (e.g. hls.js's/dash.js's own ABR switching), but never before playback has started (so the initial caption/quality detection at mount doesn't get reported as a "change"). See [Captions](#captions-webvtt) and [Video Quality](#video-quality-hls--dash).
364
-
365
- `tap` fires whenever the full-video tap-to-reveal-controls overlay is tapped/clicked (see `disableTapCapture`), regardless of `controls`. With the built-in HUD (`controls: true`) this is the same gesture that reveals it; with `controls: false` it's the only signal a headless consumer gets for "the user tapped the video", useful for showing/hiding your own custom HUD in response.
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>`/`<VideoItem>`/`<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 `<muns-controls-*>` custom element from a separate bundle):
374
267
 
375
268
  ```vue
376
269
  <script setup>
377
- import { ref } from 'vue'
378
- const player = ref(null)
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
- <VideoPlayer ref="player" video-url="..." />
383
- <button @click="player.retry()">Retry</button>
279
+ <div id="my-player" ref="wrapperEl">
280
+ <VideoPlayer ref="playerRef" video-url="..." />
281
+ </div>
384
282
  </template>
385
283
  ```
386
284
 
387
- ```vue
388
- <VideoItem video-url="..." @state-change="onStateChange" />
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
+ <muns-controls-transcript for="my-player" cues='[{"time":0,"text":"..."}]'></muns-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 `VideoItem`, `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
@@ -406,26 +344,10 @@ The `action` prop adds a button to the player HUD. Pass a built-in string or a c
406
344
 
407
345
  ### Custom action
408
346
 
409
- ```ts
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
- <script setup>
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
+ <VideoItem :action="{ icon: '<svg .../>', label: 'Save', onClick: () => console.log('saved') }" ... />
429
351
  ```
430
352
 
431
353
  ---
@@ -445,35 +367,24 @@ const saveAction = {
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}&rdid={rdid}&cust_params={customParameters}`), not ready-to-use URLs. `adTagParams` fills these in on **whichever** ad tag URL ends up in use, prop-supplied or auto-discovered, without needing to know or duplicate where the base URL came from:
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. `adTagParams` fills these in on **whichever** ad tag URL ends up in use, prop-supplied or auto-discovered:
449
371
 
450
372
  ```vue
451
- <VideoItem
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
+ <VideoItem video-url="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, not the query param it happens to sit inside (`vid={referenceId}` means the key is `referenceId`, not `vid`). A key with no matching `{macro}` in the URL is a no-op: this only fills in existing macros, it doesn't add new query params, so it has no effect on a non-templated ad tag URL (JW, or one you built by hand).
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: check for `window.pbjs` in the console. If it's not there, this isn't something to set up from this package; talk to whoever manages ad ops or your header-bidding wrapper vendor, since Prebid deployments are custom builds specific to which bidders you use (see below for why this player doesn't load Prebid itself).
471
- 2. Get a Prebid **video ad unit** config from ad ops/your bidder: an object with `code`, `mediaTypes.video` (player size, mime types, etc.), and a `bids` array with your bidder's specific params (e.g. AppNexus's `placementId`). This is the same object your org's existing Prebid setup already uses elsewhere for video slots, if one exists.
472
- 3. Get the GAM ad unit path (`iu`) to target: same source as step 1 in [Ads](#ads-ima--vast--vmap) above.
473
- 4. Pass both as `headerBidding: { adUnit, params: { iu: '...' } }`, on the same `VideoItem`/`VideoPlayer` that already has a plain `adTagUrl`/`adTagParams` set as the fallback (step 2 of the Ads section). Never rely on `headerBidding` alone with nothing to fall back to.
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`/`adTagParams` 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 it's ready by the time the ad is actually requested (on the viewer's first play); otherwise the ad request goes out with whatever `adTagUrl`/`adTagParams` already resolved to, same as if `headerBidding` weren't set at all:
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
390
  <VideoItem
@@ -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, the same object you'd otherwise hand to `pbjs.addAdUnits()`. `params` is passed through to `buildVideoUrl()` (GAM ad tag params, e.g. `iu`). `timeoutMs` (default `1000`) bounds the Prebid auction itself. Every failure mode (no `window.pbjs`, the auction timing out, no bid won, `buildVideoUrl()` throwing) fails open to whatever `adTagUrl`/`adTagParams` would have resolved to; combined with never being awaited, a header-bidding hiccup can't block or break anything else in the player, ever.
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`/`adTagParams` 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 (checked on `visibilitychange`/`blur`, and once up front when the ad starts, so a mid-roll beginning on an already-backgrounded tab is caught too): no manual setup needed, and it doesn't auto-resume, since the point is to stop burning an impression on nobody watching. If Picture-in-Picture is active when an ad starts, it's exited too (skipped on iOS, where the ad creative renders directly into the same `<video>` element PiP is already mirroring, so there's nothing to lose by staying in PiP there).
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
 
@@ -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
- ```ts
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 at all, and it's unsupported on YouTube, Vimeo, and Dailymotion. Brightcove and JW Player get it for free, same reasoning as captions above.
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
- A manual DASH quality switch takes effect immediately, even mid-playback: dash.js is told to discard whatever it's already buffered ahead at the old quality and refetch at the new one, rather than only affecting segments requested after the switch (which could otherwise be a long way ahead of the playhead on a fast connection).
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,8 +462,6 @@ 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
@@ -576,68 +473,37 @@ Set the variables on any ancestor of the player (e.g. a wrapper `<div>` or `:roo
576
473
 
577
474
  Icons use `fill="currentColor"`, so `--mvp-btn-color` recolours the icon along with `--mvp-btn-bg` for the background. Hover state is a `filter: brightness()` lift on top of `--mvp-btn-bg`, so it works for both the default translucent look and a solid theme colour.
578
475
 
579
- ### Controls popup position
476
+ ### Full controls popup
580
477
 
581
- The expanded controls popup is centred over the video by default. Set `--mvp-popup-align: flex-end` to anchor it to the bottom edge instead:
478
+ | Variable | Default | Description |
479
+ | ---------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
480
+ | `--mvp-controls-width` | `min(450px, calc(100% - 32px))` | Controls popup width |
481
+ | `--mvp-popup-align` | `center` | Horizontal alignment of the popup (`center` or `flex-end` to dock at the bottom) |
482
+ | `--mvp-btn-bg` | (frosted glass) | Background of action, play/pause, and show-controls buttons |
483
+ | `--mvp-btn-color` | (frosted glass) | Icon/text colour of the same buttons |
484
+ | `--mvp-stage-tuck` | `32px` | Width of the sliver left visible when `HideMarker` tucks the pinned stage off-screen |
485
+ | `--mvp-radius` | `12px` | Border-radius of the pinned corner box |
486
+ | `--mvp-accent` | — | Active-state highlight colour for the loop/autoplay toggle buttons in the controls popup |
582
487
 
583
- ```css
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 `--mvp-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 behaves like a standard native/YouTube-style player instead: it auto-shows on activity (mouse move, tap) and auto-hides after a short period of inactivity, with no explicit open/close step needed.
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 elements render into the light DOM, not a shadow root (`shadowRoot: false`: YouTube/Vimeo/Dailymotion's SDKs mount by resolving a plain element ID, which can't see into a shadow root), so styling has nowhere encapsulated to live. Vue is an external dependency: use an import map to provide it:
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 (the built-in player plus all headless controls). Links its own styles onto the page automatically the moment it's imported, nothing else to remember there, whether you load it via a bundler, unpkg, jsdelivr, or esm.sh.
635
- - `@munsonlabs/video-player/element/core`: just `<muns-video-player>`/`<muns-video-stage>`/`<muns-video-item>`/`<muns-video-placeholder>`, for pages that only ever use the built-in controls. Doesn't inject any CSS itself, so also import `@munsonlabs/video-player/style/core` (or your own equivalent styles).
636
- - `@munsonlabs/video-player/element/controls`: just the `<muns-controls-*>` primitives, for building a custom HUD without the built-in player shell. Also doesn't inject any CSS, so import `@munsonlabs/video-player/style/controls` alongside it (or your own equivalent styles).
502
+ - `@munsonlabs/video-player/element`: everything (player + all headless controls). Auto-injects its own CSS.
503
+ - `@munsonlabs/video-player/element/core`: just `<muns-video-player>`/`<muns-video-stage>`/`<muns-video-item>`/`<muns-video-placeholder>`. Import `@munsonlabs/video-player/style/core` yourself.
504
+ - `@munsonlabs/video-player/element/controls`: just the `<muns-controls-*>` primitives. Import `@munsonlabs/video-player/style/controls` alongside it.
637
505
 
638
- The `core`/`controls` bundles leave styling entirely up to you: defer the import, swap in your own stylesheet, or skip it if you're providing your own styles. Only the combined `element` bundle is zero-config.
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">
@@ -702,7 +568,7 @@ Controls (in `/element` and `/element/controls`), see [Headless Controls](#headl
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: Vue's custom element runtime parses them automatically:
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
  <muns-video-item video-url="https://..." ad-tag-url="https://..." payload='{"articleId":"123"}'></muns-video-item>
@@ -749,9 +615,9 @@ console.log(player.isPlaying)
749
615
  </script>
750
616
  ```
751
617
 
752
- One thing worth knowing that doesn't come up in the Vue case - **timing**: importing `/element`/`/element/core` calls `customElements.define()`, which synchronously upgrades any matching tag already sitting in the DOM as part of that same call - and a `<script type="module">` defers until after the whole document has been parsed, so a _statically-declared_ `<muns-video-player>` tag is already sitting in the DOM by the time your script runs, and gets upgraded (running `usePlayer`'s adapter resolution) as soon as the import above evaluates, before your own `registerPlatform` call on the next line has a chance to run. Creating the tag from script, after registering, as above sidesteps this entirely.
618
+ **Timing note:** importing `/element`/`/element/core` calls `customElements.define()`, which synchronously upgrades any matching tag already in the DOM. A statically-declared `<muns-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 in the markup (so you can't create them after registering), load `/element/core` with a **`?defer`** query instead: it skips the automatic `customElements.define()` on import, so you register your platform first and then call the exported `defineElements()` yourself - the static tags upgrade at that point, with your platform already in the registry:
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">
@@ -763,10 +629,7 @@ If your tags are statically declared in the markup (so you can't create them aft
763
629
  <muns-video-player video-url="https://acme.tv/watch/123"></muns-video-player>
764
630
  ```
765
631
 
766
- - `?defer` is read from the `/element/core` module's own URL, so import `/element/core` directly with the query (or point an import-map entry at a URL that includes it). The combined `/element` bundle always registers immediately - it imports the core module internally without the query.
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
 
@@ -0,0 +1 @@
1
+ import{c as e,d as t,f as n}from"./constants-DRD0PsRw.mjs";import{n as r}from"./platform-Dg9JBh8M.mjs";import{t as i}from"./loadScript-Du6JsTc2.mjs";function a(){return window.google?.ima?Promise.resolve():i(e,`ima`)}function o(e,i,o,s){let c=document.createElement(`div`);c.className=`ima-ad-container`,e.appendChild(c);let l=o,u=null,d=null,f=null,p=!1,m=!1,h=!1,g=!1,_=!1,v=!1,y=1,b=!1,x=``,S=new ResizeObserver(([e])=>{let t=window.google?.ima;if(!f||!t)return;let{width:n,height:r}=e.contentRect;f.resize(n,r,t.ViewMode.NORMAL)});S.observe(i);function C(){d?.contentComplete()}function w(r){g=r,e.classList.toggle(n,r),r||(_=!1,e.classList.remove(t))}function T(){if(p||!m||!h||!u||!d||!l)return;p=!0;let e=window.google?.ima;if(!e)return;u.initialize();let t=new e.AdsRequest;t.adTagUrl=l,d.requestAds(t)}function E(e){l=e,T()}a().then(()=>{if(v)return;let n=window.google?.ima;n&&(u=new n.AdDisplayContainer(c,i),d=new n.AdsLoader(u),m=!0,d.addEventListener(n.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,a=>{if(v)return;let o=new n.AdsRenderingSettings;o.restoreCustomPlaybackStateOnAdBreakComplete=!0,f=a.getAdsManager(i,o),i.addEventListener(`ended`,C),x=i.currentSrc,f.addEventListener(n.AdEvent.Type.CONTENT_PAUSE_REQUESTED,()=>{i.removeEventListener(`ended`,C),i.ended&&r()&&(b=!0),i.pause(),w(!0),s.onAdStart();let e=f?.getVolume()??1;e>0&&(y=e),s.onAdMuteChange(e===0)}),f.addEventListener(n.AdEvent.Type.CONTENT_RESUME_REQUESTED,()=>{if(i.addEventListener(`ended`,C),w(!1),s.onAdEnd(),b){b=!1,x&&i.currentSrc!==x&&(i.src=x);let e=i.duration;e&&isFinite(e)&&(i.currentTime=e-.5);return}i.ended||i.play()}),f.addEventListener(n.AdEvent.Type.PAUSED,()=>{_=!0,e.classList.add(t),s.onAdPauseChange(!0)}),f.addEventListener(n.AdEvent.Type.RESUMED,()=>{_=!1,e.classList.remove(t),s.onAdPauseChange(!1)}),f.addEventListener(n.AdEvent.Type.AD_PROGRESS,()=>{s.onAdProgress(f?.getRemainingTime()??0)}),f.addEventListener(n.AdErrorEvent.Type.AD_ERROR,e=>{w(!1),s.onError(e.getError().getMessage()),i.play()});try{f.init(i.clientWidth,i.clientHeight,n.ViewMode.NORMAL),f.start()}catch(e){s.onError(e instanceof Error?e.message:`Ad playback failed to start.`)}}),d.addEventListener(n.AdErrorEvent.Type.AD_ERROR,e=>{s.onError(e.getError().getMessage())}),T())}).catch(e=>s.onError(e.message));function D(){h=!0,T()}function O(){if(!f)return;let e=f.getVolume();e>0?(y=e,f.setVolume(0)):f.setVolume(y||1),s.onAdMuteChange(f.getVolume()===0)}return{isAdPlaying:()=>g,isAdPaused:()=>_,requestAdsOnFirstPlay:D,setAdTagUrl:E,pauseAd:()=>f?.pause(),resumeAd:()=>f?.resume(),toggleAdMute:O,dispose:()=>{v=!0,b=!1,x=``,S.disconnect(),i.removeEventListener(`ended`,C),f?.destroy(),d?.destroy(),c.remove(),e.classList.remove(n,t)}}}export{o as attachAds};
@@ -1 +1 @@
1
- import{p as e}from"./constants-DRD0PsRw.mjs";import{r as t}from"./embedShared-B1B7ZH_3.mjs";import{t as n}from"./loadScript-Du6JsTc2.mjs";function r(e){let t=e.match(/[?&]video=([a-zA-Z0-9]+)/);return t?t[1]:e.match(/(?:dailymotion\.com\/(?:embed\/)?video\/|dai\.ly\/)([a-zA-Z0-9]+)/)?.[1]??null}function i(i,a){let o=null;return t(i,a,{cssClass:e,connect:async({techId:e,emitter:t,state:i,isDisposed:s,consumeQueuedPlay:c})=>{let l=r(a.src);if(!l){i.errorState={code:4,message:`Could not parse video ID from source URL`},t.trigger(`error`);return}let u=!1;try{let r=typeof a.customVars?.playerId==`string`?a.customVars.playerId:void 0,d=await n(r?`https://geo.dailymotion.com/libs/player/${r}.js`:`https://geo.dailymotion.com/libs/player.js`,r||`dailymotion-sdk`).then(()=>window.dailymotion);if(!d||s())return;let f=await d.createPlayer(e,{...r&&{player:r},video:l,params:{autoplay:!!a.autoplay,mute:!!a.muted}});o=f,a.volume!==void 0&&f.setVolume(a.volume);let p=d.events;c(()=>o?.play()),f.on(p.PLAYER_CRITICALPATHREADY,e=>{let n=e.videoThumbnails;if(!n)return;let r=Object.keys(n).map(Number).sort((e,t)=>t-e)[0],i=r===void 0?void 0:n[r];i&&t.trigger(`posterchange`,i)}),f.on(p.VIDEO_PLAY,()=>{i.paused=!1,t.trigger(`play`),t.trigger(`playing`),!a.autoplay&&!u&&(u=!0,f.pause())}),f.on(p.VIDEO_PLAYING,()=>{i.paused=!1,t.trigger(`play`),t.trigger(`playing`)}),f.on(p.VIDEO_PAUSE,()=>{i.paused=!0,t.trigger(`pause`)}),p.VIDEO_BUFFERING&&f.on(p.VIDEO_BUFFERING,()=>t.trigger(`waiting`)),f.on(p.VIDEO_END,()=>{i.paused=!0,t.trigger(`ended`)}),f.on(p.VIDEO_TIMECHANGE,e=>{i.currentTime=e.videoTime??i.currentTime,i.duration=e.videoDuration??i.duration,t.trigger(`timeupdate`)}),f.on(p.VIDEO_DURATIONCHANGE,e=>{i.duration=e.videoDuration??i.duration,t.trigger(`durationchange`)}),f.on(p.VIDEO_SEEKSTART,()=>t.trigger(`seeking`)),f.on(p.VIDEO_SEEKEND,e=>{i.currentTime=e.videoTime??i.currentTime,t.trigger(`seeked`)}),f.on(p.PLAYER_VOLUMECHANGE,e=>{i.volume=e.playerVolume??i.volume,i.muted=e.playerIsMuted??i.muted,t.trigger(`volumechange`)}),f.on(p.PLAYER_ERROR,()=>t.trigger(`error`)),p.AD_START&&f.on(p.AD_START,()=>{i.paused=!1,t.trigger(`play`),t.trigger(`adstart`)}),p.AD_END&&f.on(p.AD_END,()=>t.trigger(`adend`))}catch(e){i.errorState={code:4,message:e instanceof Error?e.message:`This video could not be played.`},t.trigger(`error`)}},hasPlayer:()=>!!o,play:()=>o?.play(),pause:()=>o?.pause(),seekToSdk:e=>o?.seek(e),volumeToSdk:e=>o?.setVolume(e),muteToSdk:(e,t)=>o?.setMute(t),sdkFullscreenEnter:()=>o?.setFullscreen(!0),sdkFullscreenExit:()=>o?.setFullscreen(!1),destroyPlayer:()=>{let e=o;o=null,e?.destroy?.()}})}export{i as createDailymotionAdapter};
1
+ import{p as e}from"./constants-DRD0PsRw.mjs";import{r as t}from"./embedShared-Cq4Qj9Kt.mjs";import{t as n}from"./loadScript-Du6JsTc2.mjs";function r(e){let t=e.match(/[?&]video=([a-zA-Z0-9]+)/);return t?t[1]:e.match(/(?:dailymotion\.com\/(?:embed\/)?video\/|dai\.ly\/)([a-zA-Z0-9]+)/)?.[1]??null}function i(i,a){let o=null;return t(i,a,{cssClass:e,connect:async({techId:e,emitter:t,state:i,isDisposed:s,consumeQueuedPlay:c})=>{let l=r(a.src);if(!l){i.errorState={code:4,message:`Could not parse video ID from source URL`},t.trigger(`error`);return}let u=!1;try{let r=typeof a.customVars?.playerId==`string`?a.customVars.playerId:void 0,d=await n(r?`https://geo.dailymotion.com/libs/player/${r}.js`:`https://geo.dailymotion.com/libs/player.js`,r||`dailymotion-sdk`).then(()=>window.dailymotion);if(!d||s())return;let f=await d.createPlayer(e,{...r&&{player:r},video:l,params:{autoplay:!!a.autoplay,mute:!!a.muted}});o=f,a.volume!==void 0&&f.setVolume(a.volume);let p=d.events;c(()=>o?.play()),f.on(p.PLAYER_CRITICALPATHREADY,e=>{let n=e.videoThumbnails;if(!n)return;let r=Object.keys(n).map(Number).sort((e,t)=>t-e)[0],i=r===void 0?void 0:n[r];i&&t.trigger(`posterchange`,i)}),f.on(p.VIDEO_PLAY,()=>{i.paused=!1,t.trigger(`play`),t.trigger(`playing`),!a.autoplay&&!u&&(u=!0,f.pause())}),f.on(p.VIDEO_PLAYING,()=>{i.paused=!1,t.trigger(`play`),t.trigger(`playing`)}),f.on(p.VIDEO_PAUSE,()=>{i.paused=!0,t.trigger(`pause`)}),p.VIDEO_BUFFERING&&f.on(p.VIDEO_BUFFERING,()=>t.trigger(`waiting`)),f.on(p.VIDEO_END,()=>{i.paused=!0,t.trigger(`ended`)}),f.on(p.VIDEO_TIMECHANGE,e=>{i.currentTime=e.videoTime??i.currentTime,i.duration=e.videoDuration??i.duration,t.trigger(`timeupdate`)}),f.on(p.VIDEO_DURATIONCHANGE,e=>{i.duration=e.videoDuration??i.duration,t.trigger(`durationchange`)}),f.on(p.VIDEO_SEEKSTART,()=>t.trigger(`seeking`)),f.on(p.VIDEO_SEEKEND,e=>{i.currentTime=e.videoTime??i.currentTime,t.trigger(`seeked`)}),f.on(p.PLAYER_VOLUMECHANGE,e=>{i.volume=e.playerVolume??i.volume,i.muted=e.playerIsMuted??i.muted,t.trigger(`volumechange`)}),f.on(p.PLAYER_ERROR,()=>t.trigger(`error`)),p.AD_START&&f.on(p.AD_START,()=>{i.paused=!1,t.trigger(`play`),t.trigger(`adstart`)}),p.AD_END&&f.on(p.AD_END,()=>t.trigger(`adend`))}catch(e){i.errorState={code:4,message:e instanceof Error?e.message:`This video could not be played.`},t.trigger(`error`)}},hasPlayer:()=>!!o,play:()=>o?.play(),pause:()=>o?.pause(),seekToSdk:e=>o?.seek(e),volumeToSdk:e=>o?.setVolume(e),muteToSdk:(e,t)=>o?.setMute(t),sdkFullscreenEnter:()=>o?.setFullscreen(!0),sdkFullscreenExit:()=>o?.setFullscreen(!1),destroyPlayer:()=>{let e=o;o=null,e?.destroy?.()}})}export{i as createDailymotionAdapter};
@@ -0,0 +1 @@
1
+ import{g as e,h as t,m as n}from"./constants-DRD0PsRw.mjs";import{n as r,r as i,t as a}from"./platform-Dg9JBh8M.mjs";function o(){let e=new Map;function t(t,n){for(let r of Array.isArray(t)?t:[t])e.has(r)||e.set(r,new Set),e.get(r)?.add(n)}function n(t,n){e.get(t)?.delete(n)}function r(t,...n){e.get(t)?.forEach(e=>e(...n))}function i(){e.clear()}return{on:t,off:n,trigger:r,dispose:i}}const s={supportsCaptions:()=>!1,getCaptionTracks:()=>[],setCaptionTrack:()=>{},getActiveCaptionTrack:()=>null,supportsQuality:()=>!1,getQualityLevels:()=>[],getCurrentQuality:()=>null,isAutoQuality:()=>!0,setQuality:()=>{},supportsPip:()=>!1,isPipActive:()=>!1,togglePip:()=>{}};let c=0;function l(t,n){t.parentElement?.classList.add(n);let r=`${n}-${++c}`,i=document.createElement(`div`);i.id=r,i.className=e,i.style.cssText=`width:100%;height:100%;top:0;left:0;position:absolute`;let a=document.createElement(`div`);return a.style.cssText=`width:100%;height:100%;position:absolute;inset:0;opacity:0;pointer-events:none;transition:opacity 0.2s`,a.appendChild(i),t.parentElement?.insertBefore(a,t),{techId:r,wrapper:a,innerDiv:i}}function u(e,t){e.style.display=`none`,t.style.opacity=`1`,t.style.pointerEvents=``}function d(e,t,n){t.remove(),e.parentElement?.classList.remove(n),e.style.display=``}function f(e){return e.closest(`.player__shell`)}function p(e,t){if(r()&&t()!==!1)return;let n=f(e);n&&i(n)}function m(){a()}function h(){let e=new Set;return{schedule(t,n=0){let r=setTimeout(()=>{e.delete(r),t()},n);e.add(r)},clearAll(){e.forEach(clearTimeout),e.clear()}}}function g(e){e.trigger(n);let r=document.createElement(`div`);r.style.cssText=`position:fixed;inset:0;z-index:99999;opacity:0;pointer-events:none`,document.body.appendChild(r);let i=!1;function a(){i||e.trigger(t)}function o(n){i||(i=!0,e.trigger(t),r.remove(),n?.())}return{overlay:r,reveal:a,finish:o}}function _(e,t,n){let i=o(),{techId:c,wrapper:u}=l(e,n.cssClass),{schedule:f,clearAll:m}=h(),g={currentTime:0,duration:0,volume:t.volume??1,muted:!1,paused:!0,playQueued:!1,errorState:null},_=!1,v={techId:c,wrapper:u,emitter:i,state:g,schedule:f,isDisposed:()=>_,consumeQueuedPlay(e){g.playQueued&&(g.playQueued=!1,e())}};n.connect(v);function y(){p(e,()=>{if(!n.sdkFullscreenEnter)return!1;n.sdkFullscreenEnter()})}function b(){if(r()&&n.sdkFullscreenExit){n.sdkFullscreenExit();return}a()}return{el:u,play:()=>{n.hasPlayer()?n.play():g.playQueued=!0},pause:()=>n.pause(),paused:()=>g.paused,currentTime:()=>g.currentTime,setCurrentTime:e=>{if(n.setCurrentTime){n.setCurrentTime(v,e);return}g.currentTime=e,n.seekToSdk?.(e),i.trigger(`seeking`)},duration:()=>g.duration,volume:()=>g.volume,setVolume:e=>{g.volume=e,n.volumeToSdk(e)},muted:()=>g.muted,setMuted:e=>{g.muted=e,n.muteToSdk(v,e)},...n.rate?n.rate(v):{playbackRate:()=>1,setPlaybackRate:()=>{}},bufferedEnd:()=>g.currentTime,error:()=>g.errorState,setSrc:()=>{},supportsPlaybackRate:()=>!1,...s,enterFullscreen:y,exitFullscreen:b,on:i.on,off:i.off,dispose:()=>{_=!0,m(),n.destroyPlayer(),d(e,u,n.cssClass),i.dispose()}}}export{p as a,u as c,o as d,h as i,g as l,l as n,m as o,_ as r,f as s,s as t,d as u};
@@ -0,0 +1 @@
1
+ function e(){return/iPad|iPhone|iPod/.test(navigator.userAgent)||/Mac/.test(navigator.userAgent)&&navigator.maxTouchPoints>1}function t(e){(e.requestFullscreen||e.webkitRequestFullscreen)?.call(e)}function n(){(document.exitFullscreen||document.webkitExitFullscreen)?.call(document)}export{e as n,t as r,n as t};
@@ -1 +1 @@
1
- import{D as e,_ as t}from"./constants-DRD0PsRw.mjs";import{r as n}from"./embedShared-B1B7ZH_3.mjs";import{t as r}from"./loadScript-Du6JsTc2.mjs";function i(e){return e.match(/(?:vimeo\.com\/(?:video\/|channels\/[^/]+\/|groups\/[^/]+\/videos\/)?|player\.vimeo\.com\/video\/)(\d+)/)?.[1]??null}function a(a,o){let s=null,c=1;return n(a,o,{cssClass:t,connect:async({techId:t,emitter:n,state:a,isDisposed:c,consumeQueuedPlay:l})=>{let u=i(o.src);if(!u||(await r(e,`vimeo`),c()||!window.Vimeo))return;let d={id:Number(u),byline:!1,portrait:!1,title:!1,transparent:!0,controls:!!o.nativeUi,autopause:!1,...o.autoplay!==void 0&&{autoplay:o.autoplay},...o.muted!==void 0&&{muted:o.muted}};s=new window.Vimeo.Player(t,d),o.autoplay&&(a.playQueued=!0),s.ready().then(()=>{o.muted&&s?.setMuted(!0).catch(()=>{}),o.volume!==void 0&&s?.setVolume(o.volume).catch(()=>{}),l(()=>void s?.play().catch(()=>{}))}).catch(e=>{a.errorState={code:4,message:e?.message||`This video is unavailable or cannot be embedded here.`},n.trigger(`error`)}),s.on(`play`,()=>{a.paused=!1,n.trigger(`play`),n.trigger(`playing`)}),s.on(`pause`,()=>{a.paused=!0,n.trigger(`pause`)}),s.on(`ended`,()=>{a.paused=!0,n.trigger(`ended`)});let f=!1;s.on(`timeupdate`,e=>{let{seconds:t,duration:r}=e;a.currentTime=t,a.duration=r,n.trigger(`timeupdate`),r&&!f&&(f=!0,n.trigger(`durationchange`))}),s.on(`loaded`,()=>{f=!1,n.trigger(`loadedmetadata`),n.trigger(`durationchange`)}),s.on(`seeking`,e=>{a.currentTime=e.seconds,n.trigger(`seeking`)}),s.on(`seeked`,e=>{a.currentTime=e.seconds,n.trigger(`seeked`),n.trigger(`timeupdate`)}),s.on(`bufferstart`,()=>n.trigger(`waiting`)),s.on(`bufferend`,()=>n.trigger(`canplay`)),s.on(`volumechange`,e=>{a.volume=e.volume,n.trigger(`volumechange`)}),s.on(`error`,()=>n.trigger(`error`))},hasPlayer:()=>!!s,play:()=>void s?.play().catch(()=>{}),pause:()=>void s?.pause().catch(()=>{}),rate:({emitter:e})=>({playbackRate:()=>c,setPlaybackRate:t=>{c=t,s?.setPlaybackRate(t).catch(()=>{}),e.trigger(`ratechange`)}}),setCurrentTime:({emitter:e},t)=>{s?.setCurrentTime(t).catch(()=>{}),e.trigger(`timeupdate`),e.trigger(`seeking`)},volumeToSdk:e=>void s?.setVolume(e).catch(()=>{}),muteToSdk:({emitter:e,schedule:t},n)=>{s?.setMuted(n).then(()=>t(()=>e.trigger(`volumechange`),50)).catch(()=>{})},sdkFullscreenEnter:()=>void s?.requestFullscreen().catch(()=>{}),destroyPlayer:()=>{let e=s;s=null,e?.destroy().catch(()=>{})}})}export{a as createVimeoAdapter};
1
+ import{D as e,_ as t}from"./constants-DRD0PsRw.mjs";import{r as n}from"./embedShared-Cq4Qj9Kt.mjs";import{t as r}from"./loadScript-Du6JsTc2.mjs";function i(e){return e.match(/(?:vimeo\.com\/(?:video\/|channels\/[^/]+\/|groups\/[^/]+\/videos\/)?|player\.vimeo\.com\/video\/)(\d+)/)?.[1]??null}function a(a,o){let s=null,c=1;return n(a,o,{cssClass:t,connect:async({techId:t,emitter:n,state:a,isDisposed:c,consumeQueuedPlay:l})=>{let u=i(o.src);if(!u||(await r(e,`vimeo`),c()||!window.Vimeo))return;let d={id:Number(u),byline:!1,portrait:!1,title:!1,transparent:!0,controls:!!o.nativeUi,autopause:!1,...o.autoplay!==void 0&&{autoplay:o.autoplay},...o.muted!==void 0&&{muted:o.muted}};s=new window.Vimeo.Player(t,d),o.autoplay&&(a.playQueued=!0),s.ready().then(()=>{o.muted&&s?.setMuted(!0).catch(()=>{}),o.volume!==void 0&&s?.setVolume(o.volume).catch(()=>{}),l(()=>void s?.play().catch(()=>{}))}).catch(e=>{a.errorState={code:4,message:e?.message||`This video is unavailable or cannot be embedded here.`},n.trigger(`error`)}),s.on(`play`,()=>{a.paused=!1,n.trigger(`play`),n.trigger(`playing`)}),s.on(`pause`,()=>{a.paused=!0,n.trigger(`pause`)}),s.on(`ended`,()=>{a.paused=!0,n.trigger(`ended`)});let f=!1;s.on(`timeupdate`,e=>{let{seconds:t,duration:r}=e;a.currentTime=t,a.duration=r,n.trigger(`timeupdate`),r&&!f&&(f=!0,n.trigger(`durationchange`))}),s.on(`loaded`,()=>{f=!1,n.trigger(`loadedmetadata`),n.trigger(`durationchange`)}),s.on(`seeking`,e=>{a.currentTime=e.seconds,n.trigger(`seeking`)}),s.on(`seeked`,e=>{a.currentTime=e.seconds,n.trigger(`seeked`),n.trigger(`timeupdate`)}),s.on(`bufferstart`,()=>n.trigger(`waiting`)),s.on(`bufferend`,()=>n.trigger(`canplay`)),s.on(`volumechange`,e=>{a.volume=e.volume,n.trigger(`volumechange`)}),s.on(`error`,()=>n.trigger(`error`))},hasPlayer:()=>!!s,play:()=>void s?.play().catch(()=>{}),pause:()=>void s?.pause().catch(()=>{}),rate:({emitter:e})=>({playbackRate:()=>c,setPlaybackRate:t=>{c=t,s?.setPlaybackRate(t).catch(()=>{}),e.trigger(`ratechange`)}}),setCurrentTime:({emitter:e},t)=>{s?.setCurrentTime(t).catch(()=>{}),e.trigger(`timeupdate`),e.trigger(`seeking`)},volumeToSdk:e=>void s?.setVolume(e).catch(()=>{}),muteToSdk:({emitter:e,schedule:t},n)=>{s?.setMuted(n).then(()=>t(()=>e.trigger(`volumechange`),50)).catch(()=>{})},sdkFullscreenEnter:()=>void s?.requestFullscreen().catch(()=>{}),destroyPlayer:()=>{let e=s;s=null,e?.destroy().catch(()=>{})}})}export{a as createVimeoAdapter};