Ads
Plays a pre-roll before the content, and plays the content if the ad fails.
import { ads } from "react-video-kit/ads";Adds 1.4 KB to a player2.7 KB on its ownSource
Plays an ad before the first play of each source, through an engine you choose. Press play below. Skip ad becomes available after 3 seconds, and the content starts when the ad ends or the viewer skips it.
"use client";import { ads } from "react-video-kit/ads";import { vast } from "react-video-kit/ads/vast";import { defaultPlugins, VideoPlayer } from "react-video-kit/player";const plugins = [ads({ engine: vast({ tag: "/ads/preroll.xml" }) }), ...defaultPlugins()];export default function Ads() { return <VideoPlayer src="/media/sample-360.mp4" poster="/media/poster.jpg" plugins={plugins} load="visible" />;}Usage
import { ads } from "react-video-kit/ads";import { vast } from "react-video-kit/ads/vast";import { VideoPlayer } from "react-video-kit/player";const plugins = [ads({ engine: vast({ tag: "https://ads.example.com/tag.xml" }) })];<VideoPlayer src="/video.mp4" plugins={plugins} />;timeout is how long to wait for a pre-roll before playing the content, 3000 milliseconds by default. onError hears about failures, since the viewer never sees them. It receives the same error object as onError on the player, with the engine's own error as cause:
ads({ engine: vast({ tag: "https://ads.example.com/tag.xml" }), timeout: 5000, onError: (error) => console.warn("No ad played", error.code, error.message),});To skip ads on some sources, return null from engine:
const tag = "https://ads.example.com/tag.xml";ads({ engine: (source) => (source.src.includes("/free/") ? null : vast({ tag })()) });Each engine is its own import, so you only ship the one you use.
Play VAST tags with vast()
vast() reads the tag itself and plays the ad in a second video element over the content. It needs no third-party script.
- Every
<Ad>in the tag plays in order, and the overlay counts them. - From each ad's
<MediaFiles>, it picks the highest bitrate the browser can play, leaving outdelivery="streaming"files. An ad with nothing playable is skipped. - It follows
<Wrapper>redirects up tomaxRedirects, 5 by default. Every wrapper in a chain adds its impressions, tracking, and click tracking to the ad it leads to. - It pings
<Impression>, and thestart,firstQuartile,midpoint,thirdQuartile,complete, andskiptracking events, by loading each URL as an image. <ClickThrough>becomes the Learn more link, and following it pings<ClickTracking>.- The tag is read with
fetch, so a tag on another origin must allow it through CORS.
This tag is a wrapper around the one in the first demo:
"use client";import { ads } from "react-video-kit/ads";import { vast } from "react-video-kit/ads/vast";import { defaultPlugins, VideoPlayer } from "react-video-kit/player";const plugins = [ads({ engine: vast({ tag: "/ads/wrapper.xml" }) }), ...defaultPlugins()];export default function AdsWrapper() { return <VideoPlayer src="/media/sample-360.mp4" poster="/media/poster.jpg" plugins={plugins} load="visible" />;}vast() does no viewability or verification measurement, so use ima() for ad networks that require them.
Use Google's SDK with ima()
ima() runs Google's Interactive Media Ads SDK, which AdSense and Ad Manager require.
import { ads } from "react-video-kit/ads";import { ima } from "react-video-kit/ads/ima";const plugins = [ads({ engine: ima({ tag: "https://ads.example.com/ima-tag" }) })];- The SDK loads from Google when the source does, because it has to be ready by the first play. Set
sdkUrlto load it from somewhere else. - If the viewer presses play before it's ready,
adsskips the pre-roll for that source and tellsonError. Withload="play"the SDK only starts loading on that press, so use"eager"or"visible"withima(). - The SDK draws its own countdown and skip button, so
AdOverlaystays out of the way. - When the content ends, the plugin tells the SDK, which decides whether anything else plays.
When an ad fails or is blocked
The content plays, and the viewer doesn't see an error.
| What happens | What the player does | onError |
|---|---|---|
| The tag is blocked, or returns an error | Plays the content | Called |
No break starts within timeout | Plays the content, and a late ad can't interrupt it | Called |
| The IMA SDK is blocked or hasn't loaded | Plays the content | Called |
| The IMA SDK reports an error before a break | Plays the content | Called |
| The IMA SDK reports an error during a break | Plays the content | Called |
| The tag has no ads | Plays the content | Not called |
| A VAST ad's file won't play | Moves to the next ad, then the content | Called |
| The browser refuses to play a VAST ad | Moves to the next ad, then the content | Called |
timeout only covers the wait for a break to start. Once vast() has the tag it starts the break, so a slow ad file doesn't count.
This tag has nothing to show, so the content starts straight away:
"use client";import { ads } from "react-video-kit/ads";import { vast } from "react-video-kit/ads/vast";import { defaultPlugins, VideoPlayer } from "react-video-kit/player";// This tag has nothing to show, so the content plays straight away.const plugins = [ads({ engine: vast({ tag: "/ads/empty.xml" }) }), ...defaultPlugins()];export default function AdsEmpty() { return <VideoPlayer src="/media/sample-360.mp4" poster="/media/poster.jpg" plugins={plugins} load="visible" />;}During an ad
- Seeking and the playback rate are locked. The timeline is disabled and the speed menu disappears.
- Pause and volume work, and control the ad.
- The content stays paused, and
media.provideris"ad". The timeline and clock follow the ad, soTimelineandTimeshow its progress. - The root gets
data-ad, and the plugin's state holds the current ad. AdOverlayshows the time left, Learn more when the ad has a link, and a skip button when the ad allows one.
What's supported
vast() supports:
- Linear pre-rolls, and several of them in one break.
- Wrappers.
- Skipping, from a
skipoffsetwritten as a time or a percentage. - Impression, start, quartile, complete, skip, and click tracking.
vast() doesn't support:
- Mid-rolls and post-rolls.
- Non-linear and companion ads.
- Files delivered as streams, or VPAID.
<Error>, and any other tracking event.
The plugin asks for ads on the first play and tells the engine when the content ends. Anything beyond the pre-roll is up to the engine.
Build your own skip button
useVideoState gives you the current ad, and useCommands gives you skip. The command doesn't check the ad's offset, so your control has to:
"use client";import { useCommands, useCurrentTime, useVideoState } from "react-video-kit";import { ads } from "react-video-kit/ads";function SkipAd() { const ad = useVideoState(ads, (state) => state.ad); return ad ? <SkipWhenAllowed after={ad.skipAfter} /> : null;}// Mounted only during ads, since following time renders once a second.function SkipWhenAllowed({ after }: { after: number | null }) { const time = useCurrentTime(); const { skip } = useCommands(ads); if (after === null || time < after) return null; return <button onClick={() => skip()}>Skip ad</button>;}For your own link to the advertiser, render ad.clickThrough and call clicked from the same commands when it's followed, so the engine can report it.
Write your own engine
An engine is an object the plugin creates for each source. prepare runs inside the viewer's click, so it's where to unlock playback. request starts the break, and host tells the player what's going on:
import { ads, type AdEngine, type AdHost } from "react-video-kit/ads";const houseAd = (src: string) => (): AdEngine => { const video = document.createElement("video"); video.setAttribute("data-part", "ad-media"); video.playsInline = true; video.hidden = true; let host: AdHost | null = null; const finish = () => { video.pause(); video.hidden = true; host?.breakEnd(); }; const fail = (error: unknown) => { host?.error(error); finish(); }; return { get time() { return video.currentTime; }, prepare(next) { host = next; next.container.append(video); video.play().catch(() => {}); }, async request() { if (!host) return; video.muted = host.video.muted; video.volume = host.video.volume; video.hidden = false; host.breakStart(); host.adStart({ duration: 15, skipAfter: 5 }); video.addEventListener("ended", finish, { once: true }); video.addEventListener("error", () => fail(new Error("The ad won't play.")), { once: true }); video.src = src; video.play().catch(fail); }, play: () => video.play(), pause: () => video.pause(), skip: finish, setVolume(volume, muted) { video.volume = volume; video.muted = muted; }, destroy() { video.remove(); }, };};const plugins = [ads({ engine: houseAd("/ad.mp4") })];Anything prepare throws or request rejects with goes to onError, and the content plays. Once a break has started, report a failure with host.error, which reaches onError, and end the break yourself. An engine can also define clicked, which runs when the viewer follows the ad's link.
Behaviour
- Each source asks for a pre-roll once. A reload from the recovery plugin or the Try again button doesn't show it again. It can play again after a different source has loaded.
- With
load="play", the source starts loading inside the viewer's first play, sovast()and engines of your own run their pre-roll first. - The default skin positions the ad. In a headless layout, style
[data-part="ad-container"]and[data-part="ad-media"]yourself. - The overlay's labels come from the i18n plugin when it's installed.
API
Options
| Option | Type | Description |
|---|---|---|
| engine | (source: SourceObject) => AdEngine | null | Creates an engine for each source, or returns null for no ads on that one. |
| timeout? | number | Milliseconds to wait for a pre-roll before playing the content instead. |
| onError? | ((error: MediaErrorInfo) => void) | Told about every failure, since the viewer never sees them. Root's onError hears these too. |
State
Read it with useVideoState(ads, (state) => …).
| Field | Type | Description |
|---|---|---|
| active | boolean | True during an ad break. |
| ad | AdInfo | null | The current ad, or null until one starts and after the break. |
| ownsUi | boolean | True when the engine draws its own ad controls, which hides the built-in overlay. |
Commands
Call them with useCommands(ads).
| Command | Type | Description |
|---|---|---|
| skip | () => void | Asks the engine to skip the current ad. |
| clicked | () => void | Tells the engine the viewer followed the ad's link, for its tracking. |
Parts
| Part | Description |
|---|---|
| AdOverlay | Says it's an ad and how long is left, and offers skip when allowed. |
vast()
Plays a VAST tag's linear pre-roll with no SDK, in a second video element over the content. That needs no third-party script, but it does no viewability or verification measurement, so use ima() for ad networks that require them.
(options: VastOptions) => () => AdEngine
| Field | Type | Description |
|---|---|---|
| tag | string | The VAST tag's URL. |
| maxRedirects? | number | How many wrapper redirects to follow. |
ima()
Google's Interactive Media Ads SDK, which AdSense and Ad Manager require. It loads with the source, because it must be ready by the first play's gesture, and it draws its own countdown and skip button. If an ad blocker stops it, the content plays.
(options: ImaOptions) => () => AdEngine
| Field | Type | Description |
|---|---|---|
| tag | string | The ad tag's URL. |
| sdkUrl? | string | Where to load the IMA SDK from. |
AdEngine
A source of ads. ima() uses Google's SDK, and vast() plays VAST tags with none. Anything an engine throws or rejects with means the content simply plays.
| Field | Type | Description |
|---|---|---|
| ownsUi? | boolean | True when the engine draws its own skip button and countdown, as IMA does. |
| time | number | Position in the current ad, in seconds. |
| prepare | (host: AdHost) => void | Runs inside the first play's gesture, before anything has been awaited, so an SDK can unlock playback. Don't wait for anything here. |
| request | () => Promise<void> | Resolves once a pre-roll has started, or it's known there isn't one. |
| play | () => Promise<void> | void | Resumes the current ad. |
| pause | () => void | Pauses the current ad. |
| skip | () => void | Skips the current ad. |
| clicked? | (() => void) | The viewer followed the ad's link. |
| setVolume | (volume: number, muted: boolean) => void | Applies the viewer's volume, from 0 to 1, and muted setting to the ad. |
| contentEnded? | (() => void) | The content ended, so any post-roll can play. |
| destroy | () => void | Stops the engine and removes what it added to the page. |
AdHost
What the player offers an engine while it runs.
| Field | Type | Description |
|---|---|---|
| video | HTMLVideoElement | The content's video element. |
| container | HTMLElement | An element over the video, for the engine to draw into. |
| breakStart | () => void | Pauses the content and locks seeking and rate, until breakEnd. |
| adStart | (ad: AdInfo) => void | Tells the player an ad has begun, so the timeline and overlay follow it. |
| error | (error: unknown) => void | Reports a failure partway through a break. Ending the break is still the engine's job. |
| breakEnd | () => void | Unlocks and resumes the content. |
AdInfo
| Field | Type | Description |
|---|---|---|
| duration | number | Length in seconds. |
| skipAfter | number | null | Seconds before the viewer may skip, or null when they can't. |
| position? | number | This ad's position in its break, from 1. |
| total? | number | How many ads the break has. |
| clickThrough? | string | null | Where "Learn more" goes. |