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.
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#
- 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.
- Embed as usualUse the normal iframe. Nothing is added to the embed URL.
- Load the script on your page
https://embed.wolvy.stream/sdk/wolvy-player.jsdefinesWolvyPlayer. 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> import { useEffect, useRef } from 'react';
// Load https://embed.wolvy.stream/sdk/wolvy-player.js once, in your page <head>.
export function Lesson({ embedUrl, onProgress }) {
const frame = useRef(null);
useEffect(() => {
const player = new window.WolvyPlayer(frame.current);
player.on('progress', onProgress);
return () => player.destroy();
}, [embedUrl, onProgress]);
return (
<iframe
ref={frame}
src={embedUrl}
allow="autoplay; fullscreen; encrypted-media; picture-in-picture"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
/>
);
} 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)andplayer.once(event, fn)subscribe to events.player.destroy()stops listening. Call it when you remove the iframe.
Methods#
Control
| Method | Resolves with | Notes |
|---|---|---|
play() | true | Rejects with play_blocked when the browser wants a click first (see Limits). |
pause() | true | |
seek(seconds) | the position it moved to | A 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 volume | A number from 0 to 1. It does not unmute; call unmute() for that. |
setPlaybackRate(rate) | the rate | One 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 null | A language code from getCaptions(), or "off". |
Read
| Method | Resolves 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 } |
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#
| Event | Data | When |
|---|---|---|
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.
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?”
percentcounts 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.secondsis that count;durationis 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.
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.
| Code | Where | What it means |
|---|---|---|
not_available | ready() and every method | The 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_blocked | play() | The browser refused to start playback without a click. Mute first, or call play() from a click handler. |
invalid_argument | any setter | The 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_supported | setQuality() | This device’s player chooses the quality itself (Safari on Mac, every browser on iPhone and iPad). |
not_ready | seek() | The video’s length is not known yet. Wait for ready. |
timeout | any method | The player answered ready earlier but not this call, usually because the iframe was reloaded or removed. |
network_error | the error event, then every control method | The video could not be loaded. |
playback_error | the error event, then every control method | The video could not be played on this device. |
device_not_supported | the error event | App-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. |
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:
idis yours: a number, or a string of up to 64 characters. The answer repeats it.methodis one of the names above, without brackets:"seek","getDuration". Its argument goes invalue.- 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.
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.originandevent.sourcethe 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#
| Limit | Why | What to do |
|---|---|---|
play() needs a click, unless muted | Every browser blocks sound that the viewer did not ask for | Call it from a click handler, or mute() first |
| No quality list on Safari, iPhone and iPad | Apple’s own player picks the quality | Hide your quality control when getQualities().available is empty |
setVolume() has no effect on iPhone and iPad | iOS leaves volume to the hardware buttons | mute() and unmute() still work |
| No fullscreen method | Browsers only allow fullscreen from a click inside the player | Viewers use the player’s fullscreen button; listen to fullscreenchange |
One player per WolvyPlayer | Each iframe is its own player | Create 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.
Something wrong or unclear on this page? Email contact@wolvy.net — include the page name.