Events Reference
The player raises an event whenever something happens: playback starts, the viewer seeks, an ad plays, a link is clicked, an error is recovered. Each event has a stable, dotted id such as media.playing or timedLink.click, and listen() is how you subscribe to it.
const stop = vpPlayer().listen("media.playing", (e) => {
console.log(e.eventId, e.currentTime);
});
// later, when the subscription is no longer needed
stop();
Subscribing with listen()
listen(id, handler, options) is available from player version 2.10.0. Older versions do not have it, so a page that has to run against an earlier release should use on() from the Legacy Listeners section instead.
It behaves the same way for every event:
- The handler receives exactly one object, and that object always carries
eventId. This holds for native media events too. - It returns a function that removes the subscription, so there is no need to keep the handler reference around.
- It does not stop the event from propagating, so your page's own document-level listeners keep working.
- An unknown id throws instead of registering a listener that never fires.
vpPlayer().listenableEvents()returns every id the player accepts. listen("*", handler)receives every event that has an id. Checke.eventIdto tell them apart.
| OPTION | DESCRIPTION |
|---|---|
once | true removes the subscription after the first call. |
signal | An AbortSignal. Aborting it removes the subscription, so one controller can tear down several at once. |
const controller = new AbortController();
vpPlayer().listen("media.playing", onPlay, { signal: controller.signal });
vpPlayer().listen("media.paused", onPause, { signal: controller.signal });
vpPlayer().listen("media.completed", onComplete, { once: true });
// removes the playing and paused subscriptions together
controller.abort();
listen() needs a player that has finished setting up. Calling it earlier throws. Either call it inside the promise returned by setup(), or wait for the vp-listen-ready event on window:
vpPlayer("vp-player").setup(config).then(() => {
vpPlayer("vp-player").listen("media.ready", (e) => {
console.log(`Ready: ${e.title}, ${e.duration}s`);
});
});
Media Events
| ID | FIRES WHEN | PAYLOAD |
|---|---|---|
media.ready | The player has initialized and is ready for API calls. This is the earliest point at which any method should be called. | currentTime, id, duration, title |
media.firstFrame | The first video frame has rendered. Content playback has genuinely begun. | |
media.playing | The player entered the playing state. One of the flags says why: a fresh start, a resume after pause, or a resume after buffering. | currentTime, autoStart, begin, afterBuffer, resume, afterPause |
media.paused | The player entered the paused state. | currentTime, reason |
media.buffering | Playback stopped to buffer. This is the browser's own waiting event. | |
media.bufferChanged | More of the video was buffered. buffered is the seconds buffered ahead, to two decimals. | currentTime, buffered |
media.bufferFull | The whole video is buffered. | currentTime |
media.bufferStalled | Playback ran out of buffer on an HLS stream. Fires once per stall. | currentTime |
media.timeUpdate | The playback position advanced. Fires about every 5 seconds while playing. | currentTime, videoDuration, state |
media.seeking | A seek started. | currentTime |
media.seeked | The playback position was moved. currentTime is the position after the seek. skipAmount and direction (forward or rewind) are present when the seek came from a skip button or key. | currentTime, skipAmount, direction |
media.beforeComplete | The video is about to complete. The player has not yet shown the replay screen or advanced the playlist, so this is the moment to act on the ending. | currentTime |
media.completed | The video finished playing. | currentTime |
media.error | A critical playback error occurred. | currentTime, code, details |
Player Events
| ID | FIRES WHEN | PAYLOAD |
|---|---|---|
player.fullscreenChanged | The player entered or left fullscreen. | currentTime, fullscreen |
player.floatingChanged | The player entered or left the floating mode. | currentTime, floating |
player.viewableChanged | The player entered or left the viewport. | currentTime, viewable |
player.resized | The player container changed size or crossed a layout breakpoint. | width, height, breakpoint |
player.interacted | The viewer clicked, tapped or pressed a key on the player. source is click, touch or keyboard. | currentTime, source |
volume.muteChanged | The player was muted or unmuted. | currentTime, muted |
quality.changed | The quality level changed. level is the level index, or null when auto quality has not settled yet, and reason is manual or auto. | currentTime, level, reason |
quality.switched | Adaptive streaming moved to another level on its own. Differs from quality.changed, which also covers the viewer's own pick. | currentTime, level |
quality.levelsUpdated | The list of available quality levels changed. levels is an array of { index, height, bitrate }. | levels |
drm.stateChanged | The DRM session changed state. state is initializing, active, key_expired or error; error is present in the error state. | state, error |
cast.stateChanged | A property of the Chromecast session changed, for example field playerState with value PLAYING. event is the raw Cast SDK object and is read-only. | field, value, event |
subtitle.changed | Subtitles were switched on or off. reason is manual or inversion. | state, reason |
playlist.itemChanged | The playlist moved to another video. Fires before the new video starts. | id, title, file, duration |
UI Events
| ID | FIRES WHEN | PAYLOAD |
|---|---|---|
ui.nextClick | The viewer clicked the next video button. | currentTime |
ui.relatedOpened | The related videos panel opened. reasonForOpen is click or pause. | currentTime, reasonForOpen |
ui.sharingOpened | The share panel opened. | currentTime |
Timed Link Events
| ID | FIRES WHEN | PAYLOAD |
|---|---|---|
timedLink.show | A timed link became visible or switched to another link. | title, url, time, duration, currentTime |
timedLink.click | The viewer clicked a timed link. | title, url, time, duration, currentTime |
time is the link's configured cue point in seconds and never changes, while currentTime is the playback position at the moment the event fired. A link cued at 30 that the viewer clicks at 0:44 reports time: 30 and currentTime: 44. On a livestream time is always 0 and currentTime is a position on the stream's own timeline, so it is not comparable across viewers.
Ad Events
| ID | FIRES WHEN | PAYLOAD |
|---|---|---|
ad.ready | The ad manager loaded an ad and it is about to play. | videoCurrentTime, adCurrentTime, adDuration, adTitle, adId, adPlayId, adPosition, muted |
ad.impression | The ad reported its impression, once it started rendering. | currentTime, adDuration, adTitle, adId, adPlayId, adPosition |
ad.playing | An ad started playing or resumed. begin marks the start, resume an unpause. | videoCurrentTime, adCurrentTime, adDuration, adTitle, adId, adPlayId, adPosition, muted, begin, resume |
ad.paused | An ad was paused. | videoCurrentTime, adCurrentTime, adDuration, adTitle, adId, adPlayId, adPosition, muted |
ad.timeUpdate | Ad playback progressed. | currentTime, adPlayId |
ad.skippable | The ad can now be skipped. | currentTime, adPlayId |
ad.firstQuartile, ad.secondQuartile, ad.thirdQuartile | The ad reached a quarter, half and three quarters of its duration. The payload is the analytics body described under Analytics Events. | userId, uniqueViewId, adId, videoId, adScheduleId, projectId, playerId, playlistId, eventType, event, subtitles, duration, videoProjectId, isLive, autoStart, embedOrigin |
ad.skipped | The viewer skipped the ad. | videoCurrentTime, adCurrentTime, adDuration, adTitle, adId, adPlayId, adPosition, muted |
ad.click | The viewer clicked the ad to open its landing page. | currentTime, adPlayId |
ad.muteChanged | The ad was muted or unmuted. | videoCurrentTime, adCurrentTime, adDuration, adTitle, adId, adPlayId, adPosition, muted |
ad.completed | The ad finished playing. | videoCurrentTime, adCurrentTime, adDuration, adTitle, adId, adPlayId, adPosition, muted |
ad.bidReturned | A header bidding auction returned. type is edgeroll or midroll and position is the ad break index or the midroll time. | success, type, position |
ad.error | An error stopped the ad. Content resumes. errorType and vastErrorCode come from the IMA SDK and are absent under the vp engine. | currentTime, adPosition, currentAdIndex, errorCode, errorMessage, errorType, vastErrorCode |
In ad.playing, ad.paused, ad.muteChanged, ad.skipped and ad.completed, videoCurrentTime is the position of the content video and adCurrentTime the position inside the ad.
Playback Health Events
These events report whether playback worked, so integrations can monitor stream health from the page. Only unexpected failures are reported. Geo-blocked videos, videos that are not published yet, password prompts and paywall locks are intentional outcomes and are not counted as failures.
| ID | FIRES WHEN |
|---|---|
playback.attempt | Once per view when playback is first requested, either by autoplay or by the viewer. Adds autoStart (true when the player started playback on its own) to the shared payload below. |
playback.fatalError | At most once per view, when the player gives up on the video and shows the error screen. A fatal error always implies an attempt, so the player backfills playback.attempt if it never fired. |
playback.errorRecovered | Every time an error that would have been fatal was survived by a fallback and playback resumed. Adds recoveryReason, recoveryMethod and timeUntilRecovery to the shared payload below. |
playback.autoplayBlocked | The browser blocked autoplay, after both the unmuted and the muted attempt were rejected. This is a browser policy, not a playback failure, so no fatal error is reported. Payload: autoStart. |
playback.interrupted | Once when a livestream already in playback is disrupted by a sustained buffer stall or a network or media error. Payload: reason (bufferStall, networkError, mediaError or otherError), multiStream, currentTime. Livestreams only. |
playback.recovered | Once after a livestream recovers from an interruption. Payload: currentTime. Livestreams only. |
playback.locked | A paywall, registration or general lock stopped playback. Payload: currentTime, reason (paid, registration or general), playerId. |
playback.accessDenied | Playback was refused for a policy reason such as geo-blocking or an unpublished video. Payload: videoId, reason. |
playback.preparing | The video is still being encoded, the preparing screen is shown and the player polls for it. Payload: videoId. |
playback.prepared | The prepared video became available and playback continues. Payload: videoId. Giving up is reported as playback.fatalError with reason preparingFailed. |
A view is one watch of one video: moving to the next video in a playlist, setting a new video through the API, and replaying a finished video each start a new view. All events from the same view share the same uniqueViewId, which is the property to join them on. Ads belong to the view of the video they play in and never produce an attempt of their own.
playback.attempt, playback.fatalError and playback.errorRecovered share the following payload:
| PROPERTY | DESCRIPTION |
|---|---|
event | attempt, fatal-error or error-recovered. |
uniqueViewId | The identifier shared by all events of the current view. |
userId | The viewer identifier used by analytics. |
videoId | The ID of the current video. |
playerId | The configuration ID of the player. |
projectId | The project the player belongs to. |
file | The source URL of the current video, with the query string removed. |
isLive | Whether the current video is a live stream. |
currentTime | The playback position when the event was fired. |
playerSdkVersion | The version of the player that produced the event, so attempt and error rows can be grouped by player version. |
playback.fatalError adds the following properties:
| PROPERTY | DESCRIPTION |
|---|---|
category | The broad class of the failure: network, media, drm, unsupported or other. |
fatalReason | The specific reason, see the table below. |
location | A numeric code matching fatalReason, useful for grouping in dashboards. |
phase | start when playback had not begun yet, playback when the video was already playing. |
details | The underlying error detail as reported by the streaming library or the browser. |
httpCode | The HTTP status of the failed request, when the failure came from a network response. |
extraDetails | A JSON string with additional context, including recoveredErrors, the number of errors recovered in this view. |
The reasons and their codes are grouped by the nature of the failure: 1xx the source could not be reached, 2xx the content was unusable, 3xx the browser could not decode it, 4xx encryption or DRM, 5xx the environment or anything else.
| REASON | CODE | MEANING |
|---|---|---|
fileMissing | 101 | No source URL was provided for the video. |
fileUnreachable | 102 | The manifest or file could not be loaded. |
levelUnreachable | 103 | A quality rendition could not be loaded. |
preparingFailed | 104 | The video never became available while the preparing screen was polling. |
failoverExhausted | 105 | Every configured stream was tried and none of them played. |
networkError | 106 | A network failure without a more specific reason. |
manifestEmpty | 201 | The manifest loaded but contained no streams. |
streamEmpty | 202 | The stream loaded but contained no segments. |
segmentError | 203 | A segment failed to load or parse. |
codecUnsupported | 301 | The browser cannot decode the codecs in the stream. |
decodeError | 302 | The browser failed to decode the media. |
mediaRecoveryExhausted | 303 | Media recovery was attempted repeatedly and playback still did not resume. |
keyError | 401 | An encryption key could not be loaded or applied. |
drmError | 402 | The DRM license could not be obtained or was rejected. |
browserUnsupported | 501 | The browser cannot play the source at all. |
playRejected | 502 | The browser rejected the play request for a reason other than its autoplay policy. |
otherError | 503 | An unrecoverable failure that fits none of the above. |
playback.errorRecovered reports recoveryReason (networkError, mediaError, bufferStall or otherError) together with the recoveryMethod that worked (streamSwitch, recoverMediaError, codecSwap, levelDrop or retry) and timeUntilRecovery, the milliseconds from the first recovery attempt until media was flowing again.
Live streams keep retrying instead of giving up on network failures, so the reasons a live stream can report are not the same as the ones a VOD video can report. When you build health dashboards, split them on isLive. The two are not comparable. playback.interrupted and playback.recovered cover live streams that break and come back while already playing.
Analytics Events
These mirror the rows the player posts to the analytics endpoint, so a page can observe exactly what is being reported. They all share one payload: userId, uniqueViewId, adId, videoId, adScheduleId, projectId, playerId, playlistId, eventType, event, subtitles, duration, videoProjectId, isLive, autoStart, embedOrigin.
| ID | FIRES WHEN |
|---|---|
analytics.embedLoaded | The player embed loaded on the page. |
analytics.started | The video started. |
analytics.firstQuartile | The video reached a quarter of its duration. |
analytics.secondQuartile | The video reached half of its duration. |
analytics.thirdQuartile | The video reached three quarters of its duration. |
analytics.completed | The video completed. |
analytics.twentyView | 20 seconds of the video have been watched. |
analytics.trueView | The view counts as a true view, meaning the viewer interacted with the player. |
analytics.watchState | Every 10 seconds while playing. Adds playerState, muted, currentTime, segmentWatchTime, volume and fullscreen, all as strings because the watch state endpoint expects them that way. |
analytics.watchedTime | The accumulated watch time was reported. Payload is id, type and value only. |
Legacy Listeners
Integrations written before listen() use on() and its companions. They keep working, and every name they ever accepted still resolves to the right event. New code should use listen(). Every event the player raises now has an id, so nothing requires on() any more.
| LISTENER | DESCRIPTION | EXAMPLE |
|---|---|---|
| on(event) | Listens for an event on a player. If the player is removed and set up again, the listener has to be added again. | vpPlayer().on(event, callback) |
| once(event) | Listens for an event a single time. The listener removes itself once it has fired. | vpPlayer().once(event, callback) |
| off(event) | Removes a listener added with on(). Pass the same callback reference you passed to on(). | vpPlayer().off(event, callback) |
| offAll(event) | Removes every listener registered for an event on this player. | vpPlayer().offAll(event) |
on() accepts the id, the older vp-* name and the camelCase alias interchangeably, so on("media.playing"), on("vp-state-playing") and on("play") all subscribe to the same event. The aliases are:
| ID | ALIASES |
|---|---|
media.ready | ready, vp-ready |
media.firstFrame | firstFrame, vp-first-frame |
media.playing | play, vp-state-playing |
media.paused | pause, vp-state-paused |
media.buffering | waiting |
media.bufferChanged | bufferChange, vp-buffer-change |
media.bufferFull | bufferFull, vp-buffer-full |
media.timeUpdate | time, vp-time |
media.seeking | seek, vp-seeking |
media.seeked | seeked, vp-seeked |
media.beforeComplete | beforeComplete, vp-before-complete |
media.completed | vp-video-completed |
media.error | error, vp-playback-error |
player.fullscreenChanged | fullscreen, vp-fullscreen |
player.floatingChanged | float, vp-floating |
player.viewableChanged | viewable, vp-viewable |
volume.muteChanged | mute, vp-muted |
quality.changed | levelsChanged, vp-quality-change |
quality.switched | visualQuality, hlsLevelSwitched |
cast.stateChanged | cast, vp-casting |
subtitle.changed | vp-subtitle-change |
playlist.itemChanged | playlistItem, vp-playlist-next |
ui.nextClick | nextClick, vp-next-click |
ui.relatedOpened | relatedOpen, vp-related-open |
ui.sharingOpened | sharingOpen, vp-sharing-open |
timedLink.show | timedLinkShow, vp-timed-link-show |
timedLink.click | timedLinkClick, vp-timed-link-click |
ad.ready | adReady, ad-ready |
ad.impression | adImpression, vp-ad-impression |
ad.firstQuartile | analytic-ad-25%-completed |
ad.secondQuartile | analytic-ad-50%-completed |
ad.thirdQuartile | analytic-ad-75%-completed |
ad.playing | adPlay, vp-ad-play |
ad.paused | adPause, vp-ad-pause |
ad.timeUpdate | adTime, vp-ad-progress |
ad.skippable | adSkippable, vp-ad-skippable |
ad.skipped | adSkipped, adSkip, vp-ad-skip |
ad.click | adClick, vp-ad-click |
ad.muteChanged | adMute, vp-ad-mute |
ad.completed | adComplete, vp-ad-complete |
ad.error | adError, vp-ad-error |
playback.attempt | playAttempt, vp-play-attempt |
playback.fatalError | fatalError, vp-fatal-error |
playback.errorRecovered | errorRecovered, vp-error-recovered |
playback.autoplayBlocked | autoplayBlocked, vp-autoplay-blocked |
playback.interrupted | interrupted, vp-video-interrupted |
playback.recovered | recovered, vp-video-recovered |
playback.locked | videoLocked, vp-video-locked |
playback.accessDenied | vp-video-access-denied |
playback.preparing | vp-video-preparing |
playback.prepared | vp-video-prepared |
media.bufferStalled | vp-buffer-stalled |
drm.stateChanged | drmState, vp-drm-state |
quality.levelsUpdated | vp-levels-updated |
player.resized | vp-resize |
player.interacted | user-interaction |
ad.bidReturned | vp-bid-return |
analytics.embedLoaded | embed-loaded |
analytics.started | started, video-started |
analytics.firstQuartile | analytics-25%-completed |
analytics.secondQuartile | analytics-50%-completed |
analytics.thirdQuartile | analytics-75%-completed |
analytics.completed | complete, video-completed |
analytics.twentyView | twenty-view |
analytics.trueView | true-view |
analytics.watchState | video-state |
analytics.watchedTime | watchedtime |
The vp-event catch-all
on("vp-event", ...) receives every event the player raises. The payload carries eventName, the camelCase alias from the table above, and for events that have an id also eventId. listen("*", ...) is the equivalent for events with an id, including the native media.buffering, and delivers a copy of the payload per subscriber.
vpPlayer().on("vp-event", (e) => {
if (e.eventId === "media.playing") {
console.log("Playback started");
}
});
The ids are also the DOM event names the player dispatches on its container. The events bubble, so a listener on document or window receives them as well. Until 2.10.0 the container dispatched the legacy names from the table above, and pages that attached addEventListener("vp-fatal-error", ...) to the container, document or window received those events even though the DOM name was never part of the documented API. From 2.10.0 the player dispatches the id instead for the events it raises itself, and from 2.10.1 for all of them, so such a listener stops firing without an error.
If your page does this, switch to listen("playback.fatalError", ...). It works on any element the player is set up in, survives the rename, and is the API we test and support. If you must keep a DOM listener, use the id as the DOM name: addEventListener("playback.fatalError", ...). The vp-event catch-all is unchanged and still bubbles under its old name, with eventName carrying the legacy name and eventId the id.
Do not rely on the legacy DOM names continuing to fire. vp-video-locked is currently dispatched alongside playback.locked as a courtesy for existing integrations, and it is the only one. It will be removed in a future release.