Wolvy DOCS

Guide · needs your code

Control the player

The Player API lets the page that embeds a Wolvy video drive it — play, pause, seek, speed, quality, captions — and hear what happens: time updates, the end of the video, and how much of it a viewer has really watched.

Script embed.wolvy.stream/sdk/wolvy-player.js Off by default · per account 13 events · control only

Updated View as Markdown

On this page

What the Player API is#

The Wolvy player lives in an iframe, which normally keeps your page and the player apart. With the Player API switched on, the two can talk: your page sends commands, and the player reports events. It is what you need to build:

  • Your own buttons — a “Play lesson” button, a “Jump to chapter 3” link, keyboard shortcuts.
  • Resume where they stopped — remember the position and go back to it.
  • Course completion — mark a lesson done when a student has watched 90% of it.
  • Your own analytics — play, pause and end events, next to the rest of your page’s events.

Nothing changes for viewers: it is the same player, with the same controls, protection and watermark.

Turn it on#

  1. Switch it on for the accountIn the dashboard, open Settings → Player and turn on Player API. It is off by default, and it applies to every video on the account.
  2. Embed as usualUse the normal iframe. Nothing is added to the embed URL.
  3. Load the script on your pagehttps://embed.wolvy.stream/sdk/wolvy-player.js defines WolvyPlayer. It is about 8 KB and has no dependencies.

The player only listens to the page that embeds it — the same page the domain rules checked. Another site, another frame on your page, or a browser extension’s frame gets no answer.

Quick start#

<iframe
  id="lesson"
  src="https://embed.wolvy.stream/a1b2c3d4e5f60718293a/NDI=?vt=…"
  allow="autoplay; fullscreen; encrypted-media; picture-in-picture"
  allowfullscreen
  referrerpolicy="strict-origin-when-cross-origin"
></iframe>
<button id="play">Play</button>

<script src="https://embed.wolvy.stream/sdk/wolvy-player.js"></script>
<script>
  const player = new WolvyPlayer('#lesson');

  player.on('ready', ({ duration }) => {
    console.log('Player ready, the video is', duration, 'seconds long');
  });

  // play() must come from a click unless the player is muted
  document.querySelector('#play').addEventListener('click', () => {
    player.play().catch((error) => console.warn(error.code));
  });
</script>
  • new WolvyPlayer(iframe) takes the iframe element or a CSS selector. Create it any time: before or after the iframe has loaded.
  • Every method returns a promise. Calls made before the player is ready wait for it.
  • player.ready() resolves once the player can take commands, with its starting state.
  • player.on(event, fn), player.off(event, fn) and player.once(event, fn) subscribe to events.
  • player.destroy() stops listening. Call it when you remove the iframe.

Methods#

Control

MethodResolves withNotes
play()trueRejects with play_blocked when the browser wants a click first (see Limits).
pause()true
seek(seconds)the position it moved toA number, 0 or more. Past the end is moved to the end. not_ready before the video has loaded.
mute() · unmute()true
setVolume(volume)the volumeA number from 0 to 1. It does not unmute; call unmute() for that.
setPlaybackRate(rate)the rateOne of 0.5, 0.75, 1, 1.25, 1.5, 2 — the speeds in the player’s own menu.
setQuality(height)the height, or "auto"A height from getQualities() as a number, or "auto". not_supported on Safari and iPhone, where the system player picks the quality.
setCaptions(language)the language, or nullA language code from getCaptions(), or "off".

Read

MethodResolves with
getCurrentTime()seconds
getDuration()seconds (0 until the video has loaded)
getPaused()true or false
getMuted()true or false
getVolume()0 to 1
getPlaybackRate()e.g. 1.5
getQualities(){ available: [1080, 720, …], selected: 720 | "auto", playing: 720 | null }
getCaptions(){ available: [{ language, label }], active: "ar" | null }
getProgress(){ percent, seconds, duration }
Volume, speed, quality, captions
const player = new WolvyPlayer('#lesson');

await player.ready();

await player.setVolume(0.5);          // 0 to 1; does not unmute
await player.setPlaybackRate(1.5);    // 0.5, 0.75, 1, 1.25, 1.5 or 2

const { available } = await player.getQualities();   // e.g. [1080, 720, 480, 360]
if (available.includes(720)) await player.setQuality(720);
await player.setQuality('auto');

const captions = await player.getCaptions();         // { available: [{ language, label }], active }
if (captions.available.some((t) => t.language === 'ar')) await player.setCaptions('ar');
await player.setCaptions('off');

Each control method does exactly what the matching button in the player does, so the player’s own menus stay in step with your page.

Events#

EventDataWhen
ready{ currentTime, duration, paused, muted, volume, playbackRate }The player accepts commands from now on. Sent once.
play{ currentTime }
pause{ currentTime }
ended{}
seeked{ currentTime }After a jump, whoever asked for it.
timeupdate{ currentTime, duration }At most 4 times a second while playing.
progress{ percent, seconds, duration }How much of the video has been played. See Course completion.
volumechange{ volume, muted }
ratechange{ playbackRate }Buffering pauses are not reported as a speed change.
qualitychange{ available, selected, playing }The viewer or your page picked a quality, or the playing height changed.
captionschange{ active }A language code, or null when captions are off.
fullscreenchange{ fullscreen }
error{ code }The player could not play. A code only — see Errors.

Events fire whoever caused them: your page, the viewer using the player’s controls, or the player itself.

Resume where the viewer stopped
const KEY = 'lesson-42-position';
const player = new WolvyPlayer('#lesson');

// Remember where the viewer is (timeupdate arrives at most 4 times a second).
player.on('timeupdate', ({ currentTime }) => {
  localStorage.setItem(KEY, String(Math.floor(currentTime)));
});

// Go back there next time.
player.ready().then(() => {
  const saved = Number(localStorage.getItem(KEY));
  if (saved > 5) player.seek(saved);
});

Course completion#

The progress event answers “how much of this video has this viewer actually watched?”

  • percent counts seconds that were played, each one once. Skipping to the end does not reach 100%, and replaying the first minute ten times counts as one minute.
  • seconds is that count; duration is the video’s length.
  • It is sent as the number changes, at most once a second, and once more when the video ends.
  • It starts from zero on every page load. To add up a lesson across visits, store it on your side.
Mark a lesson complete at 90%
const player = new WolvyPlayer('#lesson');
let reported = false;

player.on('progress', async ({ percent }) => {
  if (reported || percent < 90) return;
  reported = true;

  // Your own endpoint. Decide completion on your server, for the signed-in student.
  await fetch('/lessons/42/complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ percent }),
  });
});

Errors#

A rejected promise carries an Error with a code. The error event carries the same kind of code, and nothing else.

CodeWhereWhat it means
not_availableready() and every methodThe player never answered: the Player API is off for the account, the iframe did not load a Wolvy player (a 403 page, for example), or it is still loading after 15 seconds.
play_blockedplay()The browser refused to start playback without a click. Mute first, or call play() from a click handler.
invalid_argumentany setterThe value is the wrong type or not on offer: a string where a number is expected, a speed that is not in the list, a quality or caption language the video does not have.
not_supportedsetQuality()This device’s player chooses the quality itself (Safari on Mac, every browser on iPhone and iPad).
not_readyseek()The video’s length is not known yet. Wait for ready.
timeoutany methodThe player answered ready earlier but not this call, usually because the iframe was reloaded or removed.
network_errorthe error event, then every control methodThe video could not be loaded.
playback_errorthe error event, then every control methodThe video could not be played on this device.
device_not_supportedthe error eventApp-Only mode is on and this browser has no hardware DRM; the player is showing how to switch to a supported browser or the app.
Handle a refusal
try {
  await player.play();
} catch (error) {
  if (error.code === 'play_blocked') {
    // The browser wants a click first. Either wait for one, or:
    await player.mute();
    await player.play();
  } else {
    console.warn('Player refused:', error.code);
  }
}

// Something went wrong inside the player (it is already showing its own message).
player.on('error', ({ code }) => showFallback(code));

After an error event the read methods still answer, and the control methods reject with that error’s code.

Without the script#

The script is optional. Underneath, your page and the player exchange plain postMessage objects:

you → player { type: "wolvy:player", v: 1, id, method, value? }
player → you { type: "wolvy:player", v: 1, id, ok: true, value } // or ok: false, error: "code"
player → you { type: "wolvy:player", v: 1, event, data }
  • id is yours: a number, or a string of up to 64 characters. The answer repeats it.
  • method is one of the names above, without brackets: "seek", "getDuration". Its argument goes in value.
  • A message the player does not recognise gets no answer. So does everything while the Player API is off.
  • Send { method: "ping" } to ask a player that may already be running whether it is ready; it answers with its state.
Raw postMessage
const frame = document.querySelector('#lesson');
const PLAYER = 'https://embed.wolvy.stream';

// 1. Listen. Check BOTH the origin and the window the message came from.
window.addEventListener('message', (event) => {
  if (event.origin !== PLAYER || event.source !== frame.contentWindow) return;
  const message = event.data;
  if (!message || message.type !== 'wolvy:player') return;

  if (message.event) console.log('event', message.event, message.data);
  else console.log('answer to', message.id, message.ok ? message.value : message.error);
});

// 2. Ask. Always name the player's origin as the target, never "*".
frame.contentWindow.postMessage(
  { type: 'wolvy:player', v: 1, id: 1, method: 'seek', value: 120 },
  PLAYER,
);

What it cannot do#

  • It never exposes protected data. No method returns, and no event carries, a DRM licence, a CDN token, a session id, the viewer id, the watermark text, a manifest or segment URL, or a key.
  • It only talks to the embedding page. The player answers the one origin that embedded it, and sends its messages to that origin only. Your own listener should check event.origin and event.source the same way; the script does.
  • It cannot remove the protection. The watermark, the domain rules, App-Only mode and DRM are not reachable from the API.
  • It is opt-in. Off by default for every account, and silent while off.

Limits#

LimitWhyWhat to do
play() needs a click, unless mutedEvery browser blocks sound that the viewer did not ask forCall it from a click handler, or mute() first
No quality list on Safari, iPhone and iPadApple’s own player picks the qualityHide your quality control when getQualities().available is empty
setVolume() has no effect on iPhone and iPadiOS leaves volume to the hardware buttonsmute() and unmute() still work
No fullscreen methodBrowsers only allow fullscreen from a click inside the playerViewers use the player’s fullscreen button; listen to fullscreenchange
One player per WolvyPlayerEach iframe is its own playerCreate one object per iframe

Common questions#

What is the Wolvy Player API?

A way for the page that embeds a Wolvy video to control the player and listen to it: play, pause, seek, volume, speed, quality, captions, and events such as timeupdate, ended and progress. It works over the browser’s postMessage, through a small script, wolvy-player.js.

Is the Player API on by default?

No. It is off for every account until you turn it on in the dashboard. While it is off the player ignores every message and sends nothing, and the script reports not_available.

Can my page read the video URL, the DRM licence or the viewer token through it?

No. The API is control only. The player never sends your page a licence or CDN token, a session or viewer id, a manifest or segment URL, a key, or the watermark text — there is no method that returns them and no event that carries them.

How do I mark a lesson complete when a student has watched it?

Listen to the progress event. Its percent counts the seconds of the video that were actually played, each once, so skipping ahead or replaying one part does not inflate it. When it passes your threshold, tell your own server. See Course completion.

Why does play() fail with play_blocked?

Browsers do not let a page start video with sound until the viewer has clicked something. Call play() from a click handler, or call mute() first — muted playback is always allowed.

Do I have to use wolvy-player.js?

No. The script is a thin wrapper around postMessage; the message format is documented under Without the script. The script saves you the request ids, the timeouts and the origin checks.