Getting started
Install
Add the package
npm install @munsonlabs/video-player
Mount a video
Import the CSS once alongside the component you use:
<script setup>
import { VideoCard } from '@munsonlabs/video-player'
import '@munsonlabs/video-player/style'
</script>
<template>
<VideoCard
title="Big Buck Bunny"
src="https://cdn.jwplayer.com/videos/O5chtspP-4VHSaSK0.mp4"
poster="https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg"
/>
</template>
Listen for playback events
Every component emits state-change - the single event stream for everything the player does, including timeupdate (live position) and tap (the user tapped the video, even with controls="false"):
<script setup>
function onStateChange(e) {
console.log(e.type, e.currentTime, e.src)
}
</script>
<template>
<VideoCard src="..." @state-change="onStateChange" />
</template>
See the API reference for the full event list.
Live demo
This is @munsonlabs/video-player's actual published web component, loaded from npm at page load - not a mockup:
Loading player from npm…
The platform is auto-detected
There's no separate prop to set which platform a video is on - it's inferred from the shape of src:
| Platform | URL format |
|---|---|
| YouTube | https://www.youtube.com/watch?v=VIDEO_ID |
| Vimeo | https://vimeo.com/VIDEO_ID |
| Dailymotion | https://www.dailymotion.com/video/VIDEO_ID |
| Brightcove | https://players.brightcove.net/ACCOUNT_ID/PLAYER_ID_EMBED/index.html?videoId=VIDEO_ID |
| JW Player | https://cdn.jwplayer.com/videos/... · jwplayer://MEDIA_ID |
| Plain HTML5 | Any direct MP4 / HLS (.m3u8) / DASH (.mpd) URL |
Components
| Component | Use it for |
|---|---|
VideoCard | The common case - one video, lazy by default (shows a poster until clicked) |
VideoPlayer | Same player, but always mounts immediately - use when you don't want the lazy placeholder |
VideoStage | A sticky, full-width player that receives videos from any VideoCard on the page via window events, and minifies to a corner pip when scrolled out of view |
VideoPlaceholder | The poster/loading placeholder VideoCard shows before mount - exported in case you want to reuse it |
app.use(VideoPlayerPlugin) instead of importing them individually.Building a playlist stage
Pass playlist (an array of the same props each VideoCard takes) to let VideoStage track position within it and show next/previous controls:
<script setup>
import { VideoStage, VideoCard } from '@munsonlabs/video-player'
import '@munsonlabs/video-player/style'
const videos = [
{ VideoCard: 'https://...', title: 'One' },
{ VideoCard: 'https://...', title: 'Two' },
]
</script>
<template>
<VideoStage :playlist="videos" />
<VideoCard v-for="video in videos" :key="video.VideoCard" v-bind="video" />
</template>
Auto-advance to the next entry on ended is built in (persisted per-browser, toggled from the controls popup's "Auto" button) - there's no prop for it.
When VideoStage is pinned to a corner, it can end up covering content lower on the page - place a HideMarker just before that content and the stage tucks itself out of the way while the marker is in view. See the API reference for details.
Ads and header bidding
Pass ad-tag-url for a plain VAST/VMAP ad, or header-bidding to run a Prebid.js auction first and fall back to the plain ad tag on no-fill:
<VideoCard
src="https://..."
ad-tag-url="https://pubads.g.doubleclick.net/gampad/ads?..."
:ad-tag-params="{ description_url: 'https://example.com' }"
/>
See the API reference for the full prop list, and the package README.md for the header-bidding config shape.
Auto-play in a scroll-snap feed
Set playInView to auto-play once a video is at least half visible, and auto-pause once it isn't - it works with VideoCard's default lazy placeholder too, so you don't need to disable lazy to use it. If several playInView videos are visible at once, only the last one to cross the threshold plays.
<VideoCard src="..." play-when-in-view />
Autoplay, mute & volume
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.
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/playInView without a gesture is blocked or force-muted by the browser itself. This applies to:
autoplay="true"set directly onVideoCard/VideoPlayer.- A
playInViewvideo actually starting once scrolled into view - re-checked at that exact moment (not just once at mount, since aplayInViewplayer 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.
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).
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:
- Clicking a lazy
VideoCard's placeholder for the first time. VideoStage's idle-click-to-resume andvideo-togglewindow event.
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.
An explicit muted/volume set on a specific VideoCard/VideoPlayer (or a specific playlist VideoEntry) always wins over all of the above, in every case.
Building your own controls
Set controls="false" to render a bare player with no built-in HUD, then build your own from the headless control components (PlayButton, Scrubber, etc.) or drive it entirely via a template ref's imperative methods. useForwardedPlayer lets you forward that same API through your own wrapper component - see the API reference for both.
The built-in controls popup width defaults to min(450px, calc(100% - 32px)) and can be overridden via the --mlv-controls-width CSS variable - see the themeable CSS variables section for the full list.