YurbaVP

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.

Usage
<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,
})
Modes
ModeForWhat 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.

Classic

Click the player to focus it, then try Space, the arrow keys, M and F.

Short
Events will appear here…

API

Public methods and properties of the <yurba-vp> element.

create
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 keyTypeDefaultDescription
modestring'classic''classic' or 'short'.
srcstring''Video URL. .m3u8 URLs play through hls.js.
posterstring''Image before playback. In short mode it also fills the sides, blurred.
ratiostring'16 / 9'Aspect ratio of the frame.
autoplaybooleanfalsePlay once loaded. If the browser refuses sound, the player plays muted.
loopbooleanfalseStart over at the end.
mutedbooleanfrom storageStart muted.
lazybooleanfalseAttach the stream only on load() or play(). For feeds, where only the clips near the screen should hold a stream.
persistbooleantrueKeep the volume and mute state in localStorage.
hlsConfigobject{}Options for new Hls().
ignorestring''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.
infoNode | string''Short mode: the caption over the bottom of the clip.
actionsarray['volume', 'fullscreen']Short mode: the column of round buttons, top to bottom. See Info and actions.
labelsobjectEnglishThe player's own texts: { volume, fullscreen, holdHint, unlock }.
iconsobjectbuilt-inReplace built-in icons, see Icons. Keys left out keep the default.
burststringicons.burstShort mode: the HTML that pops up where the video is double-tapped. '' turns it off.
holdSpeednumber | null2Speed while the video is held. null turns holding off.
controlsobjectsee ControlsClassic mode: which buttons are shown.
speedsnumber[][0.5, 0.75, 1, 1.25, 1.5, 2]Classic mode: the speed menu.
normalLabelstring'Normal'Classic mode: the menu label for speed 1, for translations.
keyboardbooleantrueClassic mode: shortcuts while the player has focus.
hideAfternumber1500Classic mode: milliseconds before the controls fade while playing.
Source
MethodDescription
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())
Playback
MethodDescription
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'.
Volume and speed
MethodDescription
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)
    })
})
Info and actions

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 entryDescription
'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).
MethodDescription
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())
Overlay

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)
Gestures
GestureClassicShort
TapPlay / pausePlay / pause
Double tapFullscreenA heart where you tapped, and yurba-vp.dblclick. Every tap right after it adds one more.
HoldPlays at holdSpeed (2x) until you let go, with a sign at the top
Hold, pull downKeeps the speed after you let go. Tap the sign, or pick another speed, to go back.
Press or drag the progress barSeeks, 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.

Keyboard

Classic mode only, and only while the player has focus, so two players on a page never both react to the same key.

KeyAction
Space / KPlay / pause
← / JBack 5 seconds
→ / LForward 5 seconds
↑ / ↓Volume up / down
MMute
FFullscreen

Events

Bubbling CustomEvents on the <yurba-vp> element, named yurba-vp.*. Extra data is in event.detail.

Overview
EventDetailDescription
yurba-vp.playPlayback started.
yurba-vp.pausePlayback paused.
yurba-vp.endedThe video ended (not while loop is on).
yurba-vp.progressThe 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.speedSpeed 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

Controls

Classic mode. Omitted keys keep their default.

YurbaVP.create(slot, {
    mode: 'classic',
    src:  url,
    controls: { speed: false, wide: true },
})
KeyDefaultDescription
volumetrueMute button and a volume slider that opens on hover. The mouse wheel over them changes the volume.
speedtrueSpeed menu.
fullscreentrueFullscreen button.
widefalseWide mode button.
Icons

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>',
    },
})
KeyDefaultDescription
playplay_arrowPlay button while paused, the sign on a paused short clip, the tap and key ripple.
pausepausePlay button while playing, the tap and key ripple.
volumeUpvolume_upSound button at half volume or more, the M key ripple.
volumeDownvolume_downSound button below half volume.
volumeOffvolume_offSound button when muted, the M key ripple.
speedslow_motion_videoSpeed menu button.
widecrop_16_9Wide mode button.
fullscreenfullscreenFullscreen button.
fullscreenExitfullscreen_exitFullscreen button while in fullscreen.
fastForwardfast_forwardThe sign shown while the video is held.
locklockThe same sign once the speed is locked.
previouschevron_leftPrevious photo in slides.
nextchevron_rightNext photo in slides.
burstfavoriteShort mode: the double-tap burst, unless burst is set.
CSS variables
VariableDefaultDescription
--y-vp-accent#2289ffTimeline fill (classic).
--y-vp-ratio16 / 9Aspect ratio, set by the ratio option.
--y-vp-radius0pxCorner radius (none in fullscreen).
--y-vp-bg#000Background behind the video.
--y-vp-control#fffIcons and text.
--y-vp-active#ff3b47An 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.