YurbaVideoPlayer
Video player for mp4 files and HLS streams, with touch gestures, keyboard shortcuts and playback speed control. It has two modes: classic for long videos with a control bar, and short for vertical clips in a feed. HLS plays through hls.js, which the page includes before the player; the player loads nothing itself.
<link rel="stylesheet" href="/dist/yurba-vp.min.css">
<script src="/dist/yurba-vp.min.js"></script>
The icons are Material Symbols Rounded unless replaced through icons, and the page loads that font itself. Players are created from JavaScript with YurbaVP.create().
const player = YurbaVP.create(document.querySelector('#player'), {
mode: 'classic',
src: 'https://cdn.yurba.one/video/abc/master.m3u8',
poster: 'https://cdn.yurba.one/video/abc/preview.jpg',
autoplay: true,
})
| Mode | For | What it has |
|---|---|---|
classic |
Long videos: a watch page, a video in a modal | Timeline you can press or drag, with buffering and hover time, play, volume slider, time counter, speed menu, wide mode, fullscreen, loading spinner, keyboard shortcuts. The controls fade out while playing. |
short |
Vertical clips, one per screen, in a feed | Tap to pause, a play sign while paused, double tap for a burst, a thin progress bar you can press or drag, and a blurred poster at the sides. A caption at the bottom and a column of actions on the right: the player's own sound and fullscreen buttons, and the page's buttons. |
Live demo
Both players below play the same file. Events from both are logged under them.
Click the player to focus it, then try Space, the arrow keys, M and F.
API
Public methods and properties of the <yurba-vp> element.
const player = YurbaVP.create(parent, config)
Builds the player at once and appends it to parent. Pass null to insert it yourself: the player and its overlay are ready before the player is in the DOM.
| Config key | Type | Default | Description |
|---|---|---|---|
mode | string | 'classic' | 'classic' or 'short'. |
src | string | '' | Video URL. .m3u8 URLs play through hls.js. |
poster | string | '' | Image before playback. In short mode it also fills the sides, blurred. |
ratio | string | '16 / 9' | Aspect ratio of the frame. |
autoplay | boolean | false | Play once loaded. If the browser refuses sound, the player plays muted. |
loop | boolean | false | Start over at the end. |
muted | boolean | from storage | Start muted. |
lazy | boolean | false | Attach the stream only on load() or play(). For feeds, where only the clips near the screen should hold a stream. |
persist | boolean | true | Keep the volume and mute state in localStorage. |
hlsConfig | object | {} | Options for new Hls(). |
ignore | string | '' | Selector for parts of the overlay where a tap should not pause the video. Links, buttons, inputs, the actions column and an expanded caption are always ignored. |
info | Node | string | '' | Short mode: the caption over the bottom of the clip. |
actions | array | ['volume', 'fullscreen'] | Short mode: the column of round buttons, top to bottom. See Info and actions. |
labels | object | English | The player's own texts: { volume, fullscreen, holdHint, unlock }. |
icons | object | built-in | Replace built-in icons, see Icons. Keys left out keep the default. |
burst | string | icons.burst | Short mode: the HTML that pops up where the video is double-tapped. '' turns it off. |
holdSpeed | number | null | 2 | Speed while the video is held. null turns holding off. |
controls | object | see Controls | Classic mode: which buttons are shown. |
speeds | number[] | [0.5, 0.75, 1, 1.25, 1.5, 2] | Classic mode: the speed menu. |
normalLabel | string | 'Normal' | Classic mode: the menu label for speed 1, for translations. |
keyboard | boolean | true | Classic mode: shortcuts while the player has focus. |
hideAfter | number | 1500 | Classic mode: milliseconds before the controls fade while playing. |
| Method | Description |
|---|---|
load() | Attach the stream. Returns a promise. play() calls it for you. |
unload() | Detach the stream and free its buffers. Also runs when the player is removed from the DOM. |
destroy() | Unload the stream and remove the document listeners, for a player that is gone for good. |
setSource(src, poster?) | Switch videos. If the old stream was attached, the new one is attached too. |
setPoster(url) | Change the poster. |
// Keep a stream only on the clip on screen and its neighbours
items.forEach((item, i) => Math.abs(i - active) <= 1 ? item.vp.load() : item.vp.unload())
| Method | Description |
|---|---|
play() | Play. Returns a promise. Falls back to muted and fires yurba-vp.volume with auto: true when the browser refuses sound. |
pause() | Pause. |
togglePlay() | Play or pause. Returns true if it started playing. |
userToggle() | Play or pause on the viewer's behalf. Fires yurba-vp.toggle, so the page can tell the viewer's pauses from its own. |
isPaused() | Whether the video is paused. |
seek(seconds) | Jump to a time. |
skip(seconds) | Move by some seconds (negative goes back). |
toggleFullscreen() / isFullscreen() | Fullscreen for the player. |
toggleWide() | Classic mode: a fixed 500px height instead of the aspect ratio. |
burst(clientX?, clientY?) | Short mode: pop the burst at a point on the screen, or in the middle. |
ripple(html, side?) | Classic mode: flash a sign over the video. side: 'left' or 'right'. |
| Method | Description |
|---|---|
setVolume(0..1) / getVolume() | Volume. 0 mutes. |
setMuted(bool) / isMuted() | Mute. Unmuting at volume 0 brings the volume back to 0.5. |
syncVolume(volume, muted) | Take a volume from elsewhere, without an event and without saving it. |
setSpeed(rate) / getSpeed() | Playback speed. |
A feed with one volume for all its clips sets persist: false, listens to yurba-vp.volume on each clip, and passes the change to the others with syncVolume().
clip.addEventListener('yurba-vp.volume', () => {
clips.forEach((other) => {
if (other != clip) other.syncVolume(clip.getVolume(), clip.video.muted)
})
})
Short mode. The caption lies over the bottom of the clip and lets taps through to the video, except on its links and buttons. The actions stand in a column at the right edge, and the caption keeps clear of them.
| Action entry | Description |
|---|---|
'volume' | The player's sound button: mutes on a press; on a pointer device a slider opens beside it on hover, and the wheel over it changes the volume. |
'fullscreen' | The player's fullscreen button. |
{ id, icon, title?, count?, active?, fill?, onClick? } | A round button with an icon (a Material Symbols name in icon, or markup in html), and a count under it if count is given ('' keeps it hidden until set). Fires yurba-vp.action with { id }. |
{ id, element } / { id } | A slot for the page's own element, such as a dropdown; without one, fill it later through action(id). |
| Method | Description |
|---|---|
setInfo(content) | Replace the caption (a node or HTML). |
expandInfo(bool?) | Unfold the caption, or toggle it. Unfolded, it scrolls, and a tap on it no longer pauses the video. |
action(id) | The element of an action. |
setAction(id, { icon, html, title, count, active, fill, element }) | Update an action. count: null hides the count. |
const clip = YurbaVP.create(feed, {
mode: 'short',
src: url,
info: '<a href="/user/author">@author</a> A caption',
actions: [
{ id: 'like', icon: 'favorite', title: 'Like', count: 12 },
{ id: 'comments', icon: 'chat_bubble', fill: true, count: 3 },
'volume',
{ id: 'share', element: shareButton },
'fullscreen',
],
})
clip.addEventListener('yurba-vp.action', (e) => {
if (e.detail.id == 'like') clip.setAction('like', { active: true, count: 13 })
})
clip.addEventListener('yurba-vp.dblclick', () => like())
player.overlay lies over the video and under the progress bar, and holds the caption and the actions. The page can add more there, such as a cover over the clip. Taps pass through it to the video, except on the elements the page put there. To let a part of that content pass taps through too, give it pointer-events: none. To keep a tappable area from pausing the video, list it in ignore.
const clip = YurbaVP.create(null, {
mode: 'short',
src: url,
ignore: '.sensitive-cover',
})
// A cover over a clip the viewer has to agree to see
const cover = document.createElement('div')
cover.className = 'sensitive-cover'
cover.innerHTML = '<button>Show</button>'
clip.overlay.appendChild(cover)
| Gesture | Classic | Short |
|---|---|---|
| Tap | Play / pause | Play / pause |
| Double tap | Fullscreen | A heart where you tapped, and yurba-vp.dblclick. Every tap right after it adds one more. |
| Hold | Plays at holdSpeed (2x) until you let go, with a sign at the top | |
| Hold, pull down | Keeps the speed after you let go. Tap the sign, or pick another speed, to go back. | |
| Press or drag the progress bar | Seeks, following the pointer or finger | |
A single tap waits 250 ms to make sure no second tap follows, so a double tap never pauses the video on its way. Moving more than 10px before the hold starts cancels it, so a swipe through a feed still scrolls.
Classic mode only, and only while the player has focus, so two players on a page never both react to the same key.
| Key | Action |
|---|---|
Space / K | Play / pause |
← / J | Back 5 seconds |
→ / L | Forward 5 seconds |
↑ / ↓ | Volume up / down |
M | Mute |
F | Fullscreen |
Events
Bubbling CustomEvents on the <yurba-vp> element, named yurba-vp.*. Extra data is in event.detail.
| Event | Detail | Description |
|---|---|---|
yurba-vp.play | Playback started. | |
yurba-vp.pause | Playback paused. | |
yurba-vp.ended | The video ended (not while loop is on). | |
yurba-vp.progress | The playback position changed. | |
yurba-vp.seek | { time } | The viewer let go of the progress bar after pressing or dragging it. |
yurba-vp.toggle | { playing } | The viewer played or paused the video. |
yurba-vp.action | { id } | Short mode: one of the page's action buttons was pressed. |
yurba-vp.dblclick | { x, y } | Short mode: a double tap on the video, and each tap right after it. |
yurba-vp.hold | { holding, locked? } | Holding started or ended; locked when pulled down to keep the speed. |
yurba-vp.volume | { auto? } | Volume or mute changed. auto: the browser refused sound and the player muted itself. |
yurba-vp.speed | Speed changed. | |
yurba-vp.fullscreen | { fullscreen } | Entered or left fullscreen. |
yurba-vp.wide | { wide } | Classic wide mode toggled. |
yurba-vp.error | { reason } | A stream can't play without hls.js on the page ('hls'), or the video failed ('stream', 'media'). |
player.addEventListener('yurba-vp.toggle', (e) => {
// Paused by the viewer: don't resume it when the tab comes back
item.userPaused = !e.detail.playing
})
Customization
Classic mode. Omitted keys keep their default.
YurbaVP.create(slot, {
mode: 'classic',
src: url,
controls: { speed: false, wide: true },
})
| Key | Default | Description |
|---|---|---|
volume | true | Mute button and a volume slider that opens on hover. The mouse wheel over them changes the volume. |
speed | true | Speed menu. |
fullscreen | true | Fullscreen button. |
wide | false | Wide mode button. |
Each value is HTML inserted as given; keys left out keep the built-in icon. The defaults are in YurbaVP.ICONS.
YurbaVP.create(slot, {
src: url,
icons: {
play: '<svg viewBox="0 0 24 24" width="1em" height="1em">...</svg>',
pause: '<svg viewBox="0 0 24 24" width="1em" height="1em">...</svg>',
},
})
| Key | Default | Description |
|---|---|---|
play | play_arrow | Play button while paused, the sign on a paused short clip, the tap and key ripple. |
pause | pause | Play button while playing, the tap and key ripple. |
volumeUp | volume_up | Sound button at half volume or more, the M key ripple. |
volumeDown | volume_down | Sound button below half volume. |
volumeOff | volume_off | Sound button when muted, the M key ripple. |
speed | slow_motion_video | Speed menu button. |
wide | crop_16_9 | Wide mode button. |
fullscreen | fullscreen | Fullscreen button. |
fullscreenExit | fullscreen_exit | Fullscreen button while in fullscreen. |
fastForward | fast_forward | The sign shown while the video is held. |
lock | lock | The same sign once the speed is locked. |
previous | chevron_left | Previous photo in slides. |
next | chevron_right | Next photo in slides. |
burst | favorite | Short mode: the double-tap burst, unless burst is set. |
| Variable | Default | Description |
|---|---|---|
--y-vp-accent | #2289ff | Timeline fill (classic). |
--y-vp-ratio | 16 / 9 | Aspect ratio, set by the ratio option. |
--y-vp-radius | 0px | Corner radius (none in fullscreen). |
--y-vp-bg | #000 | Background behind the video. |
--y-vp-control | #fff | Icons and text. |
--y-vp-active | #ff3b47 | An active action (short). |
State classes: y-vp--classic / y-vp--short, is-paused, is-fullscreen, is-active (classic, controls shown), y-vp--wide, is-holding, is-hold-locking, is-speed-locked.