@munsonlabs/video-player 0.1.6 → 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 +212 -210
- package/dist/chunks/ads-Dj9QMKvd.mjs +1 -0
- package/dist/chunks/brightcove-CIqE1989.mjs +1 -0
- package/dist/chunks/constants-DRD0PsRw.mjs +1 -0
- package/dist/chunks/dailymotion-CHtFXCBp.mjs +1 -0
- package/dist/chunks/embedShared-Cq4Qj9Kt.mjs +1 -0
- package/dist/chunks/jwplayer-Dv826deA.mjs +1 -0
- package/dist/chunks/loadScript-Du6JsTc2.mjs +1 -0
- package/dist/chunks/platform-Dg9JBh8M.mjs +1 -0
- package/dist/chunks/prebid-RHDT0DOX.mjs +1 -0
- package/dist/chunks/sourceHelpers-DlkCZiTJ.mjs +1 -0
- package/dist/chunks/vimeo-mHVlA0Ra.mjs +1 -0
- package/dist/chunks/youtube-D8sBVM7Q.mjs +1 -0
- package/dist/elements/chunks/ads-Dj9QMKvd.mjs +1 -0
- package/dist/elements/chunks/brightcove-CIqE1989.mjs +1 -0
- package/dist/elements/chunks/constants-DRD0PsRw.mjs +1 -0
- package/dist/elements/chunks/dailymotion-CHtFXCBp.mjs +1 -0
- package/dist/elements/chunks/embedShared-Cq4Qj9Kt.mjs +1 -0
- package/dist/elements/chunks/jwplayer-Dv826deA.mjs +1 -0
- package/dist/elements/chunks/loadScript-Du6JsTc2.mjs +1 -0
- package/dist/elements/chunks/platform-Dg9JBh8M.mjs +1 -0
- package/dist/elements/chunks/prebid-RHDT0DOX.mjs +1 -0
- package/dist/elements/chunks/sourceHelpers-DlkCZiTJ.mjs +1 -0
- package/dist/elements/chunks/vimeo-mHVlA0Ra.mjs +1 -0
- package/dist/elements/chunks/youtube-D8sBVM7Q.mjs +1 -0
- package/dist/elements/controls.css +1 -1
- package/dist/elements/controls.d.mts +28 -49
- package/dist/elements/controls.mjs +1 -1
- package/dist/elements/core.css +1 -1
- package/dist/elements/core.d.mts +105 -5
- package/dist/elements/core.mjs +1 -1
- package/dist/elements/index.d.mts +118 -52
- package/dist/elements/index.mjs +1 -1
- package/dist/elements/style.css +1 -1
- package/dist/index.d.mts +301 -286
- package/dist/index.mjs +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -27,6 +27,19 @@ import '@munsonlabs/video-player/style'
|
|
|
27
27
|
</template>
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
### Pinning a single player
|
|
31
|
+
|
|
32
|
+
Set `pin-position` on a standalone `VideoItem`/`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
|
+
<VideoItem video-url="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4" pin-position="bottom-right" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Only `'bottom-right'`, `'bottom-left'`, `'top-right'`, `'top-left'` are supported here (unlike `VideoStage`'s `pinPosition`, 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-position` 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
|
+
|
|
30
43
|
### Stage player
|
|
31
44
|
|
|
32
45
|
`VideoStage` is a sticky full-width player that receives videos from `VideoItem` 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.
|
|
@@ -45,27 +58,19 @@ import '@munsonlabs/video-player/style'
|
|
|
45
58
|
|
|
46
59
|
### Playlists
|
|
47
60
|
|
|
48
|
-
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.
|
|
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.
|
|
49
62
|
|
|
50
63
|
```vue
|
|
51
64
|
<VideoStage :playlist="videos" @state-change="onStateChange" />
|
|
52
65
|
```
|
|
53
66
|
|
|
54
|
-
- Auto-advance (playing the next
|
|
55
|
-
-
|
|
56
|
-
- 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`:
|
|
57
69
|
|
|
58
70
|
```vue
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</script>
|
|
63
|
-
|
|
64
|
-
<template>
|
|
65
|
-
<VideoStage ref="stage" :playlist="videos" />
|
|
66
|
-
<button :disabled="!stage?.hasPrevious" @click="stage.playPrevious()">Previous</button>
|
|
67
|
-
<button :disabled="!stage?.hasNext" @click="stage.playNext()">Next</button>
|
|
68
|
-
</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>
|
|
69
74
|
```
|
|
70
75
|
|
|
71
76
|
### Hiding the pinned stage over content
|
|
@@ -88,37 +93,43 @@ import { VideoStage, HideMarker } from '@munsonlabs/video-player'
|
|
|
88
93
|
```
|
|
89
94
|
|
|
90
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.
|
|
91
|
-
- The sliver width is a CSS custom property: `--mvp-stage-tuck` (default `32px`).
|
|
92
|
-
-
|
|
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.
|
|
93
98
|
|
|
94
99
|
---
|
|
95
100
|
|
|
96
101
|
## VideoItem / VideoPlayer Props
|
|
97
102
|
|
|
98
|
-
| Prop | Type | Default | Description
|
|
99
|
-
| ------------------- | -------------- | -------- |
|
|
100
|
-
| `videoUrl` | `string` | - | **Required.** The video URL or platform-specific URI (the platform is auto-detected from its shape, see [Platform URLs](#platform-urls))
|
|
101
|
-
| `title` | `string` | `''` | Video title
|
|
102
|
-
| `poster` | `string` | `''` | Poster image URL
|
|
103
|
-
| `aspectRatio` | `string` | `'16:9'` | e.g. `'16:9'`, `'9:16'`, `'4:3'`
|
|
104
|
-
| `autoplay` | `boolean` | `false` | Autoplay on mount (
|
|
105
|
-
| `muted` | `boolean` |
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
103
|
+
| Prop | Type | Default | Description |
|
|
104
|
+
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
105
|
+
| `videoUrl` | `string` | - | **Required.** The video URL or platform-specific URI (the platform is auto-detected from its shape, see [Platform URLs](#platform-urls)) |
|
|
106
|
+
| `title` | `string` | `''` | Video title |
|
|
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`\* | Show placeholder until clicked |
|
|
113
|
+
| `nativeUi` | `boolean` | `false` | Use the platform's native controls (YouTube, Vimeo, Dailymotion only) |
|
|
114
|
+
| `autoStage` | `boolean` | `false` | Immediately send this video to the stage on mount |
|
|
115
|
+
| `playbackRate` | `number` | `1` | Initial playback rate |
|
|
116
|
+
| `adTagUrl` | `string` | `''` | VAST or VMAP ad tag URL |
|
|
117
|
+
| `adTagParams` | `object` | - | Fills `{macro}` tokens on whichever ad tag URL ends up in use, see [Ads](#ads-ima--vast--vmap) |
|
|
118
|
+
| `headerBidding` | `object` | - | Runs a Prebid.js auction before the ad plays, see [Header bidding](#header-bidding-prebidjs) |
|
|
119
|
+
| `tracks` | `array` | - | WebVTT caption/subtitle tracks, see [Captions](#captions-webvtt) |
|
|
120
|
+
| `payload` | `object` | `{}` | Arbitrary data attached to every `state-change` event |
|
|
121
|
+
| `action` | `PlayerAction` | `null` | Button shown in the player HUD, see [Actions](#actions) |
|
|
122
|
+
| `disableTapCapture` | `boolean` | `false` | Disables the full-video tap-to-reveal-controls overlay (tap/click still reaches embed platform UI underneath, e.g. YouTube's own) |
|
|
123
|
+
| `controls` | `boolean` | `true` | Set `false` to render a bare `<video>` with no built-in HUD - drive playback with your own UI instead, see [Headless Controls](#headless-controls) |
|
|
124
|
+
| `playWhenInView` | `boolean` | `false` | Auto-play once at least half the player is visible, auto-pause once it isn't - e.g. for a scroll-snap feed, see [Autoplay, mute & volume](#autoplay-mute--volume) |
|
|
125
|
+
| `pinPosition` | `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) |
|
|
119
126
|
|
|
120
127
|
\* `lazy` only has an effect on `VideoItem` (default `true`), which is what actually implements the placeholder-until-clicked behaviour. `VideoPlayer` accepts the prop for type compatibility but defaults to `false` and never reads it: using `<VideoPlayer>` directly always mounts the real player immediately, regardless of `lazy`.
|
|
121
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
|
+
|
|
122
133
|
### Platform URLs
|
|
123
134
|
|
|
124
135
|
The platform is detected automatically from the shape of `videoUrl`; there's no separate prop to set it.
|
|
@@ -134,6 +145,28 @@ The platform is detected automatically from the shape of `videoUrl`; there's no
|
|
|
134
145
|
|
|
135
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.
|
|
136
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`/`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
|
+
|
|
137
170
|
---
|
|
138
171
|
|
|
139
172
|
## Headless Controls
|
|
@@ -156,6 +189,7 @@ import {
|
|
|
156
189
|
Scrubber,
|
|
157
190
|
VolumeSlider,
|
|
158
191
|
TimeDisplay,
|
|
192
|
+
Transcript,
|
|
159
193
|
} from '@munsonlabs/video-player'
|
|
160
194
|
```
|
|
161
195
|
|
|
@@ -185,6 +219,27 @@ Or, `<label for>`-style, skip the ref and point at an element id instead:
|
|
|
185
219
|
|
|
186
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 (`<muns-controls-play-button>` etc.) outside Vue.
|
|
187
221
|
|
|
222
|
+
### Transcript
|
|
223
|
+
|
|
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:
|
|
225
|
+
|
|
226
|
+
```vue
|
|
227
|
+
<VideoPlayer id="my-player" video-url="..." />
|
|
228
|
+
<Transcript
|
|
229
|
+
for="my-player"
|
|
230
|
+
:cues="[
|
|
231
|
+
{ time: 0, text: 'Welcome back to the show.' },
|
|
232
|
+
{ time: 12, text: 'Today we look at the new release.' },
|
|
233
|
+
{ time: 47, end: 60, text: 'Here is the demo.' },
|
|
234
|
+
]"
|
|
235
|
+
/>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- Works on **every** platform (YouTube/Vimeo/Dailymotion included) - only needs current time and `seek`.
|
|
239
|
+
- `end` is optional: when set, nothing is highlighted between that cue's `end` and the next cue's start.
|
|
240
|
+
- Auto-scroll pauses while the pointer is over the list. A scoped slot customises each cue's markup: `#default="{ cue, index, isActive, formatTime }"`.
|
|
241
|
+
- As a custom element (`<muns-controls-transcript>`), pass cues as an inline JSON string attribute: `cues='[{"time":0,"text":"..."}]'`.
|
|
242
|
+
|
|
188
243
|
### Building your own wrapper component
|
|
189
244
|
|
|
190
245
|
`useForwardedPlayer` is what `VideoItem`/`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:
|
|
@@ -202,90 +257,75 @@ defineExpose(forwarded)
|
|
|
202
257
|
</template>
|
|
203
258
|
```
|
|
204
259
|
|
|
205
|
-
`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
|
|
206
|
-
|
|
207
|
-
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.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Events
|
|
212
|
-
|
|
213
|
-
### `state-change`
|
|
214
|
-
|
|
215
|
-
Emitted by `VideoItem`, `VideoPlayer`, and `VideoStage`.
|
|
216
|
-
|
|
217
|
-
```ts
|
|
218
|
-
interface StateChangeEvent {
|
|
219
|
-
type:
|
|
220
|
-
| 'play'
|
|
221
|
-
| 'pause'
|
|
222
|
-
| 'ended'
|
|
223
|
-
| 'seeked'
|
|
224
|
-
| 'error'
|
|
225
|
-
| 'adstart'
|
|
226
|
-
| 'adend'
|
|
227
|
-
| 'volumechange'
|
|
228
|
-
| 'ratechange'
|
|
229
|
-
| 'captionchange'
|
|
230
|
-
| 'qualitychange'
|
|
231
|
-
| 'pipchange'
|
|
232
|
-
| 'loopchange'
|
|
233
|
-
| 'firstQuartile'
|
|
234
|
-
| 'midpoint'
|
|
235
|
-
| 'thirdQuartile'
|
|
236
|
-
| 'controlsopen'
|
|
237
|
-
| 'controlsclose'
|
|
238
|
-
| 'bufferstart'
|
|
239
|
-
| 'bufferend'
|
|
240
|
-
| 'timeupdate'
|
|
241
|
-
| 'tap'
|
|
242
|
-
currentTime: number
|
|
243
|
-
duration: number
|
|
244
|
-
src: string
|
|
245
|
-
error?: { code: number; message: string } | null
|
|
246
|
-
isMuted?: boolean // volumechange only
|
|
247
|
-
playbackRate?: number // ratechange only
|
|
248
|
-
captionIndex?: number | null // captionchange only; null means captions are off
|
|
249
|
-
qualityIndex?: number | null // qualitychange only; null means Auto
|
|
250
|
-
isPipActive?: boolean // pipchange only
|
|
251
|
-
isLooping?: boolean // loopchange only
|
|
252
|
-
element?: HTMLElement | null // controlsopen/controlsclose only
|
|
253
|
-
payload?: Record<string, unknown>
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
`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.
|
|
258
|
-
|
|
259
|
-
`controlsopen` / `controlsclose` fire when the expanded controls popup mounts/unmounts (e.g. via the "show controls" button), with `element` set to the popup's root DOM node: handy for positioning your own UI relative to it, or measuring it. On `controlsclose`, `element` is a snapshot of the closing node; it's detached from the DOM immediately after.
|
|
260
|
-
|
|
261
|
-
`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).
|
|
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.
|
|
262
261
|
|
|
263
|
-
|
|
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.
|
|
264
263
|
|
|
265
|
-
|
|
264
|
+
### Exposing a plain Vue player to independent, non-Vue code
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
`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.
|
|
270
|
-
|
|
271
|
-
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):
|
|
272
267
|
|
|
273
268
|
```vue
|
|
274
269
|
<script setup>
|
|
275
|
-
import { ref } from 'vue'
|
|
276
|
-
|
|
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))
|
|
277
276
|
</script>
|
|
278
277
|
|
|
279
278
|
<template>
|
|
280
|
-
<
|
|
281
|
-
|
|
279
|
+
<div id="my-player" ref="wrapperEl">
|
|
280
|
+
<VideoPlayer ref="playerRef" video-url="..." />
|
|
281
|
+
</div>
|
|
282
282
|
</template>
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
-
```
|
|
286
|
-
|
|
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>
|
|
287
291
|
```
|
|
288
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
|
+
|
|
289
329
|
---
|
|
290
330
|
|
|
291
331
|
## Actions
|
|
@@ -304,26 +344,10 @@ The `action` prop adds a button to the player HUD. Pass a built-in string or a c
|
|
|
304
344
|
|
|
305
345
|
### Custom action
|
|
306
346
|
|
|
307
|
-
|
|
308
|
-
interface CustomAction {
|
|
309
|
-
icon: string // SVG string
|
|
310
|
-
label: string
|
|
311
|
-
onClick: () => void
|
|
312
|
-
}
|
|
313
|
-
```
|
|
347
|
+
Pass an object with `icon` (SVG string), `label`, and `onClick`:
|
|
314
348
|
|
|
315
349
|
```vue
|
|
316
|
-
<
|
|
317
|
-
const saveAction = {
|
|
318
|
-
icon: `<svg .../>`,
|
|
319
|
-
label: 'Save',
|
|
320
|
-
onClick: () => console.log('saved'),
|
|
321
|
-
}
|
|
322
|
-
</script>
|
|
323
|
-
|
|
324
|
-
<template>
|
|
325
|
-
<VideoItem :action="saveAction" ... />
|
|
326
|
-
</template>
|
|
350
|
+
<VideoItem :action="{ icon: '<svg .../>', label: 'Save', onClick: () => console.log('saved') }" ... />
|
|
327
351
|
```
|
|
328
352
|
|
|
329
353
|
---
|
|
@@ -343,35 +367,24 @@ const saveAction = {
|
|
|
343
367
|
|
|
344
368
|
Ads are not supported on YouTube, Vimeo, or Dailymotion.
|
|
345
369
|
|
|
346
|
-
Some platforms (currently Brightcove) auto-discover their own ad tag URL: the platform's own wins if you don't pass `adTagUrl` yourself. Brightcove's ad tag URLs are typically macro templates (e.g. `...&iu={adUnit}&vid={referenceId}&
|
|
370
|
+
Some platforms (currently Brightcove) auto-discover their own ad tag URL: the platform's own wins if you don't pass `adTagUrl` yourself. Brightcove's ad tag URLs are typically macro templates (e.g. `...&iu={adUnit}&vid={referenceId}&cust_params={customParameters}`), not ready-to-use URLs. `adTagParams` fills these in on **whichever** ad tag URL ends up in use, prop-supplied or auto-discovered:
|
|
347
371
|
|
|
348
372
|
```vue
|
|
349
|
-
<VideoItem
|
|
350
|
-
video-url="https://players.brightcove.net/..."
|
|
351
|
-
:ad-tag-params="{
|
|
352
|
-
adUnit: 'network/section',
|
|
353
|
-
referenceId: videoId,
|
|
354
|
-
rdid: deviceId,
|
|
355
|
-
idtype: 'idfa',
|
|
356
|
-
is_lat: 0,
|
|
357
|
-
customParameters: 'pageType=article&contentId=123',
|
|
358
|
-
}"
|
|
359
|
-
/>
|
|
373
|
+
<VideoItem video-url="https://players.brightcove.net/..." :ad-tag-params="{ adUnit: 'network/section', referenceId: videoId, rdid: deviceId }" />
|
|
360
374
|
```
|
|
361
375
|
|
|
362
|
-
Name each key exactly after the macro it fills
|
|
376
|
+
Name each key exactly after the macro it fills (`vid={referenceId}` means the key is `referenceId`, not `vid`). A key with no matching `{macro}` is a no-op.
|
|
363
377
|
|
|
364
378
|
### Header bidding (Prebid.js)
|
|
365
379
|
|
|
366
380
|
**Step by step:**
|
|
367
381
|
|
|
368
|
-
1. Confirm your organisation already runs Prebid.js somewhere on the page
|
|
369
|
-
2. Get a Prebid **video ad unit** config from ad ops
|
|
370
|
-
3.
|
|
371
|
-
4.
|
|
372
|
-
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.
|
|
373
386
|
|
|
374
|
-
The auction runs **in the background** and is never awaited: it can't block mounting, playback, or controls. Its result is only used if
|
|
387
|
+
The auction runs **in the background** and is never awaited: it can't block mounting, playback, or controls. Its result is only used if ready by the time the ad is requested (first play); otherwise the plain ad tag is used. Every failure mode (no `pbjs`, timeout, no bid, `buildVideoUrl()` throwing) fails open:
|
|
375
388
|
|
|
376
389
|
```vue
|
|
377
390
|
<VideoItem
|
|
@@ -388,7 +401,7 @@ The auction runs **in the background** and is never awaited: it can't block moun
|
|
|
388
401
|
/>
|
|
389
402
|
```
|
|
390
403
|
|
|
391
|
-
`adUnit` is a standard Prebid video ad unit
|
|
404
|
+
`adUnit` is a standard Prebid video ad unit. `params` is passed through to `buildVideoUrl()` (GAM ad tag params). `timeoutMs` (default `1000`) bounds the auction. Every failure mode fails open to whatever `adTagUrl`/`adTagParams` would have resolved to.
|
|
392
405
|
|
|
393
406
|
### Ad overlay UI
|
|
394
407
|
|
|
@@ -396,7 +409,7 @@ While an ad is playing, a small overlay shows an "Ad" badge with a countdown to
|
|
|
396
409
|
|
|
397
410
|
### Ad visibility behaviour
|
|
398
411
|
|
|
399
|
-
An ad pauses itself whenever the tab is hidden or the window loses focus
|
|
412
|
+
An ad pauses itself whenever the tab is hidden or the window loses focus, and exits Picture-in-Picture if active (skipped on iOS, where the ad renders into the same `<video>` element PiP mirrors). The player ignores `timeupdate`/`durationchange`/`progress` events while an ad is active, so `current`/`total`/`bufferedDisplay` stay on the content's values throughout. On iOS, after a post-roll ad finishes the player restores the original content source and seeks to the end, since the IMA SDK may not restore it.
|
|
400
413
|
|
|
401
414
|
---
|
|
402
415
|
|
|
@@ -419,17 +432,7 @@ There are two ways a caption track ends up available, and you don't have to pick
|
|
|
419
432
|
/>
|
|
420
433
|
```
|
|
421
434
|
|
|
422
|
-
|
|
423
|
-
interface CaptionTrackDef {
|
|
424
|
-
src: string // a WebVTT file URL: <track> only understands VTT, not SRT or other subtitle formats
|
|
425
|
-
kind?: 'captions' | 'subtitles'
|
|
426
|
-
srclang?: string
|
|
427
|
-
label?: string
|
|
428
|
-
default?: boolean
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
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.
|
|
433
436
|
|
|
434
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.
|
|
435
438
|
|
|
@@ -439,11 +442,9 @@ Clicking "Captions" in the controls menu cycles Off → first track → next tra
|
|
|
439
442
|
|
|
440
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.
|
|
441
444
|
|
|
442
|
-
This only applies to HLS and DASH: plain MP4/HTML5 sources have no variant levels
|
|
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.
|
|
443
446
|
|
|
444
|
-
**Safari plays HLS natively
|
|
445
|
-
|
|
446
|
-
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.
|
|
447
448
|
|
|
448
449
|
Programmatically, a template ref on `VideoPlayer` exposes `setQuality(index)` (`null` re-enables Auto); see [Events](#state-change) for the `qualitychange` event this fires.
|
|
449
450
|
|
|
@@ -461,8 +462,6 @@ Programmatically, a template ref on `VideoPlayer` exposes `togglePip()`; see [Ev
|
|
|
461
462
|
|
|
462
463
|
## Theming the HUD buttons
|
|
463
464
|
|
|
464
|
-
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.
|
|
465
|
-
|
|
466
465
|
Set the variables on any ancestor of the player (e.g. a wrapper `<div>` or `:root`) to restyle all three buttons at once:
|
|
467
466
|
|
|
468
467
|
```css
|
|
@@ -474,66 +473,37 @@ Set the variables on any ancestor of the player (e.g. a wrapper `<div>` or `:roo
|
|
|
474
473
|
|
|
475
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.
|
|
476
475
|
|
|
477
|
-
###
|
|
476
|
+
### Full controls popup
|
|
478
477
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 |
|
|
486
487
|
|
|
487
|
-
Real fullscreen always docks the popup to the bottom regardless of
|
|
488
|
+
Real fullscreen always docks the popup to the bottom regardless of `--mvp-popup-align`.
|
|
488
489
|
|
|
489
490
|
### Fullscreen controls
|
|
490
491
|
|
|
491
|
-
Outside fullscreen, the full controls bar only appears after clicking "show controls" and stays open until dismissed. In real fullscreen it
|
|
492
|
-
|
|
493
|
-
---
|
|
494
|
-
|
|
495
|
-
## Window Events
|
|
496
|
-
|
|
497
|
-
`VideoItem` and `VideoStage` communicate via window custom events. You can dispatch these yourself to control the stage from outside Vue.
|
|
498
|
-
|
|
499
|
-
| Event | Detail | Description |
|
|
500
|
-
| -------------- | --------------------------------------------------------- | ---------------------------------------------- |
|
|
501
|
-
| `video-select` | `VideoSelectDetail` | Load a video into the stage |
|
|
502
|
-
| `video-toggle` | `{ videoUrl: string }` | Play/pause a specific video in the stage |
|
|
503
|
-
| `video-state` | `{ currentVideoUrl: string \| null, isPlaying: boolean }` | Dispatched by the stage when its state changes |
|
|
504
|
-
|
|
505
|
-
```js
|
|
506
|
-
// Load a video into the stage
|
|
507
|
-
window.dispatchEvent(
|
|
508
|
-
new CustomEvent('video-select', {
|
|
509
|
-
detail: {
|
|
510
|
-
videoUrl: 'https://...',
|
|
511
|
-
title: 'My Video',
|
|
512
|
-
poster: 'https://...',
|
|
513
|
-
autoplay: true,
|
|
514
|
-
},
|
|
515
|
-
}),
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
// Listen for stage state changes
|
|
519
|
-
window.addEventListener('video-state', (e) => {
|
|
520
|
-
console.log(e.detail.currentVideoUrl, e.detail.isPlaying)
|
|
521
|
-
})
|
|
522
|
-
```
|
|
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.
|
|
523
493
|
|
|
524
494
|
---
|
|
525
495
|
|
|
526
496
|
## Web Component Usage
|
|
527
497
|
|
|
528
|
-
Import an element bundle to register components as native custom elements. These
|
|
498
|
+
Import an element bundle to register components as native custom elements. These render into the light DOM (not shadow roots), so styling has nowhere encapsulated to live. Vue is an external dependency: use an import map to provide it.
|
|
529
499
|
|
|
530
500
|
Three bundles are available: pick exactly one, since importing more than one double-registers any tag they share and throws:
|
|
531
501
|
|
|
532
|
-
- `@munsonlabs/video-player/element`: everything (
|
|
533
|
-
- `@munsonlabs/video-player/element/core`: just `<muns-video-player>`/`<muns-video-stage>`/`<muns-video-item>`/`<muns-video-placeholder
|
|
534
|
-
- `@munsonlabs/video-player/element/controls`: just the `<muns-controls-*>` primitives
|
|
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.
|
|
535
505
|
|
|
536
|
-
|
|
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.
|
|
537
507
|
|
|
538
508
|
```html
|
|
539
509
|
<script type="importmap">
|
|
@@ -594,10 +564,11 @@ Controls (in `/element` and `/element/controls`), see [Headless Controls](#headl
|
|
|
594
564
|
| `<muns-controls-scrubber>` | `<Scrubber>` |
|
|
595
565
|
| `<muns-controls-volume-slider>` | `<VolumeSlider>` |
|
|
596
566
|
| `<muns-controls-time-display>` | `<TimeDisplay>` |
|
|
567
|
+
| `<muns-controls-transcript>` | `<Transcript>` |
|
|
597
568
|
|
|
598
569
|
### Passing objects as web component attributes
|
|
599
570
|
|
|
600
|
-
Boolean and string props map directly to HTML attributes. Object props (`payload`) can be passed as inline JSON strings:
|
|
571
|
+
Boolean and string props map directly to HTML attributes. Object props (`payload`) can be passed as inline JSON strings:
|
|
601
572
|
|
|
602
573
|
```html
|
|
603
574
|
<muns-video-item video-url="https://..." ad-tag-url="https://..." payload='{"articleId":"123"}'></muns-video-item>
|
|
@@ -629,6 +600,37 @@ player.togglePlay()
|
|
|
629
600
|
console.log(player.isPlaying)
|
|
630
601
|
```
|
|
631
602
|
|
|
603
|
+
### Registering a platform from a web component
|
|
604
|
+
|
|
605
|
+
[`registerPlatform`](#adding-a-custom-platform) is exported from `/element` and `/element/core` too:
|
|
606
|
+
|
|
607
|
+
```html
|
|
608
|
+
<script type="module">
|
|
609
|
+
import { registerPlatform } from 'https://esm.sh/@munsonlabs/video-player/element/core'
|
|
610
|
+
registerPlatform({ key: 'acme', test: (url) => url.includes('acme.tv'), embed: true, createAdapter: createAcmeAdapter })
|
|
611
|
+
|
|
612
|
+
const player = document.createElement('muns-video-player')
|
|
613
|
+
player.setAttribute('video-url', 'https://acme.tv/watch/123')
|
|
614
|
+
document.body.append(player)
|
|
615
|
+
</script>
|
|
616
|
+
```
|
|
617
|
+
|
|
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.
|
|
619
|
+
|
|
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:
|
|
621
|
+
|
|
622
|
+
```html
|
|
623
|
+
<script type="module">
|
|
624
|
+
import { registerPlatform, defineElements } from 'https://esm.sh/@munsonlabs/video-player/element/core?defer'
|
|
625
|
+
registerPlatform({ key: 'acme', test: (url) => url.includes('acme.tv'), embed: true, createAdapter: createAcmeAdapter })
|
|
626
|
+
defineElements()
|
|
627
|
+
</script>
|
|
628
|
+
|
|
629
|
+
<muns-video-player video-url="https://acme.tv/watch/123"></muns-video-player>
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`?defer` is read from the `/element/core` module's own URL. `defineElements()` is idempotent: it only defines whichever tags aren't already registered.
|
|
633
|
+
|
|
632
634
|
---
|
|
633
635
|
|
|
634
636
|
## Peer Dependencies
|