Everything the embed can do
The player is a single cross-origin iframe. Nothing to install, nothing to authenticate. Everything below is a URL parameter or a postMessage. That is the entire API surface.
Quick start
Paste this where you want the player. It is a complete, working, responsive embed.
Complete embed<div style="position:relative;padding-bottom:56.25%;height:0"> <iframe src="https://embed.vidrift.net/embed/movie/1440098" style="position:absolute;inset:0;width:100%;height:100%;border:0" allowfullscreen allow="autoplay; fullscreen"></iframe> </div>
Keep referrerpolicy at its default or strict-origin-when-cross-origin. Do not add a sandbox attribute: a sandboxed iframe cannot play. Fetching an /embed/* URL with a script instead of framing it returns 403.
URL structure
Three shapes. Anime is TV content under the hood, so it takes the TV shape with the show's TMDB id, or its AniList id if that is what your site has.
Already embedding embed.vidrift.in? Nothing to change: it serves the same player and sends the same messages, from the https://embed.vidrift.in origin. Switch to embed.vidrift.net whenever convenient, and update any event.origin check at the same time.
https://embed.vidrift.net/embed/movie/{tmdb_id}
https://embed.vidrift.net/embed/tv/{tmdb_id}/{season}/{episode}
https://embed.vidrift.net/embed/anilist/{anilist_id}/{episode}
https://embed.vidrift.net/embed/movie/1440098 Drawn Together
https://embed.vidrift.net/embed/tv/1399/1/1 Game of Thrones S01E01
https://embed.vidrift.net/embed/tv/1429/1/1 Attack on Titan S01E01
https://embed.vidrift.net/embed/anilist/16498/1 Attack on Titan ep 1, by AniList id
https://embed.vidrift.net/embed/anilist/21/150 One Piece ep 150 (absolute, as AniList counts)The AniList shape counts episodes the AniList way, per entry from 1, and anime movies need no episode. It answers with a redirect onto the TMDB URL, so the title plays with every server, not an anime-only one. Add ?dub=1 to start on the English dub. An AniList id with no TMDB mapping yet returns a 404.
Parameters
Every parameter is optional and validated server-side. Anything invalid is ignored rather than treated as an error, so a bad value never breaks playback.
| Parameter | Value | What it does |
|---|---|---|
title | Text | Shown on the loading card while a source is being found. Without it the player looks the title up itself. |
brand | Text, ≤ 28 chars | Your name in the eyebrow above the title on the loading card. |
brandLogo | https URL | Your logo above the title on the loading card, up to 28px tall. PNG or SVG with a transparent background looks best. |
brandColor | 6-digit hex | Accent for the progress bar, eyebrow and switches. With or without the #. |
poster | https URL | Replaces the title's own backdrop art behind the loading card. |
watermark | 1 or https URL | A small logo in the top-right corner during playback, shown only while the controls are hidden. 1 reuses brandLogo. |
showTitle | 1 | Shows the top-left title bar in windowed mode too. By default it appears only in fullscreen. |
exit | 1 | Adds a Back arrow at the left of the title, and an Exit button (an X) in the corner of the "Not out yet" and "won't play" screens. Clicking it sends vidrift:exit to your page so you can close the player screen. |
uiScale | 0.5 – 2.5 | Scales every control, icon and label: the bars, the menus, the Episodes view, the panels and the pause card. 1.5 for a TV screen, 0.85 for a compact box. |
caps | 0 | Sentence case for the small labels (eyebrows, menu headers, the episode line in the top bar), which are uppercase by default. |
controlBg | solid | A flat dark strip behind the controls instead of the gradient, for bright video. |
font | Google Fonts family | Swaps the player's typeface, e.g. font=Inter or font=Bebas%20Neue. Letters, digits and spaces only. |
muted | 1 | Start muted for this load. Sound comes on when the viewer taps the speaker. |
autoplay | 0 | Do not autoplay. The player loads the source and waits for a click. Note that forcing autoplay with sound is not possible in any browser; the default is already the most that browsers allow. |
hide | Comma list | Hides controls: progress, play (the play/pause button), volume, skip, time, title (the title and episode line in the top bar), server, next, episodes, subtitles, settings, lock, pip, cast, fullscreen. Also gradient (the dark shading behind the top and bottom bars) and pausecard (the title card that comes up after eight seconds paused). Cosmetic only: keyboard shortcuts keep working. |
mobileSheets | true | On phones, delegates the Settings, Subtitles and Source panels to your own UI. See Mobile sheet. |
fs | parent | Your page owns fullscreen. The fullscreen button sends vidrift:fullscreen instead of fullscreening the iframe; you fullscreen the box around it and answer vidrift:fullscreen-state. Lets you swap the iframe for the next episode without leaving fullscreen. See Messages. |
dub | 1 | AniList URLs only: start on the English dub server. |
Finding TMDB ids
Ids come from themoviedb.org. They are in the URL of any title page, and available through the TMDB API.
- Movies:
themoviedb.org/movie/1440098→ id is1440098 - TV and anime:
themoviedb.org/tv/1399→ id is1399 - Anime by AniList id:
anilist.co/anime/16498→/embed/anilist/16498/{episode}. No TMDB lookup needed on your side.
The home page builds the finished URL for you. Type an id into it, press Enter, and the real player loads.
Customise
The player is skinned from the URL, so there is nothing to configure anywhere else. The home page has a panel that builds the URL live while you watch the result.
The loading card
While a source is found the player shows a title card: the title's own backdrop art, dimmed, with the title bottom-left, a progress hairline and the servers a viewer can switch between. Most starts never show it. It appears only when a start takes longer than about two seconds, and the art is fetched only then, so a fast start costs nothing extra.
brand: your name in the eyebrow above the title, max 28 characters.brandLogo: an https URL to a PNG, SVG or JPG, shown above the title.brandColor: a 6-digit hex accent for the progress bar, eyebrow and switches.poster: an https image URL that replaces the backdrop art.
Size, contrast, type and start
uiScale:1.5makes every control TV-sized,0.85tightens a small box. Hit targets scale with it.controlBg=solid: a flat dark strip behind the controls, for video with bright edges where the gradient is not enough.font: any Google Fonts family name. It replaces the display and body faces in one go.muted=1andautoplay=0: start silent, or do not start at all until the viewer clicks. By default the player autoplays muted and switches sound on at the first interaction, which is the most any browser allows.
During playback
watermark=1showsbrandLogoas a small corner bug, top-right, only while the controls are hidden, so it never covers the top bar. Pass an https URL instead of1to use a different image.showTitle=1keeps the title bar (top-left) visible in windowed mode, not only in fullscreen.exit=1adds a Back arrow at the left of the title, and an Exit button in the top-right corner of the "Not out yet" and "This one won't play right now" screens. It sendsvidrift:exitto your page and nothing more, so your page decides what leaving means: close a modal, go back, or navigate.hidetakes a comma list of controls to remove:progress,play,volume,skip,time,title,server,next,episodes,subtitles,settings,lock,pip,cast,fullscreen, plusgradientfor the dark shading behind the bars andpausecardfor the title card shown after eight seconds paused. Hiding is cosmetic: keyboard shortcuts, tap-to-seek and the phone gestures keep working, so a viewer is never locked out of playback.
https://embed.vidrift.net/embed/movie/1440098 ?title=Drawn%20Together &brand=Acme%20Stream &brandLogo=https://acme.com/logo.svg &brandColor=%2300d1b2 &watermark=1 &exit=1 &hide=server,cast,pip
window.addEventListener("message", (e) => { if (e.origin !== "https://embed.vidrift.net") return; if (e.data.type === "vidrift:exit") closePlayer(); });
Every value is validated. A non-https URL, a colour that is not a hex triplet, an unknown control name or an image that 404s is ignored, never an error, so a bad value can never break playback.
What viewers control
- Lock. A padlock sits with the top-right controls. Locked, the controls stay hidden and a click or tap does nothing except bring up an Unlock button; keyboard shortcuts, Skip intro and Up Next keep working. It resets on every load.
hide=lockremoves it. - Video size. Fit, Fill and Stretch are in the Settings menu. Fit shows the whole picture, Fill crops it to cover the player, Stretch distorts it to fill. The choice is the viewer's and is remembered in their browser, so there is no parameter for it.
- Server. A server the viewer picks by hand is remembered for that title in their browser and leads on its other episodes, so an anime watched in dub stays in dub. A failover is never remembered, and your
providerparameter still wins. - Touch. On a phone the first tap shows the controls and the centre button plays or pauses. A double tap on either side seeks 10 seconds.
- Pause card. Three seconds into a pause, the title, year, TMDB rating, genres and overview fade in bottom-left over the frame. It never covers the controls, and it goes the moment playback resumes or the viewer seeks. There is no parameter for it.
Autoplay
The player always attempts autoplay, muted, as soon as a source is ready. That is what every browser allows without a user gesture, so there is nothing to enable. Sound comes on the moment the viewer interacts with the player.
Episodes
For TV and anime the control bar has an Episodes button, beside Next. It opens the show's seasons as tabs and the chosen season's episodes as a list, with the one playing marked. A pick swaps this iframe to the new episode and keeps every parameter you set, so there is nothing to wire up. Just before it swaps, the player sends vidrift:episode with the new season and episode, in case your page shows the current episode somewhere and wants to stay in step.
If your page has an episode list of its own and wants to stay in charge, send vidrift:nextup-info (next section) once the iframe has loaded: the in-player list and its Next button step aside and the player follows your page instead. hide=episodes removes the button outright.
window.addEventListener("message", (e) => { if (e.origin !== "https://embed.vidrift.net") return; if (e.data.type === "vidrift:episode") history.replaceState(null, "", `/tv/${e.data.tmdbId}/${e.data.season}/${e.data.episode}`); });
Auto next episode
For TV and anime the player shows a five-second Up Next card with the next episode's still as the credits start (The Intro DB for TV, AniSkip's ending marker for anime, 30 seconds before the end as the fallback), then plays the next one in the same iframe. It knows the next episode from the same season list the Episodes button uses, so by default there is nothing to do.
A page that drives episodes itself takes over with the two messages below. Once your page has sent vidrift:nextup-info, the card and the Next episode button in the control bar follow your page: the countdown or a click sends vidrift:nextup-play and your page swaps the URL. Send next: null on the last episode and the button goes away; hide=next removes it for good.
iframe.contentWindow.postMessage({ type: "vidrift:nextup-info", next: { season: 1, episode: 2, // optional, shown on the card: name: "The Kingsroad", still: "https://image.tmdb.org/t/p/w300/..." } }, "https://embed.vidrift.net");
window.addEventListener("message", (e) => { if (e.origin !== "https://embed.vidrift.net") return; if (e.data.type === "vidrift:nextup-play") { // e.data: { tmdbId, mediaType, season, episode } of the episode that just finished iframe.src = `https://embed.vidrift.net/embed/tv/${e.data.tmdbId}/${e.data.season}/${e.data.episode + 1}`; } });
Messages
Every message is scoped to the https://embed.vidrift.net origin. Always check event.origin before trusting an incoming message, and always pass the origin as the second argument when sending one. All player messages carry tmdbId, mediaType, season and episode. On anime titles they also carry anilist: { id, mal, episode }, the AniList entry actually playing (with its MyAnimeList id) and the episode number inside it (counted from 1, so the next cour reports the next entry), null elsewhere. Key continue watching on it from vidrift:progress if your site thinks in AniList ids.
| Type | Direction | When |
|---|---|---|
vidrift:progress | Player → Parent | Every 5 seconds while playing, after the first 5 seconds. Adds currentTime and duration. |
vidrift:paused | Player → Parent | Playback paused, by the viewer or at the end. Adds currentTime. Use it to show your own UI over the player, such as a nav bar. |
vidrift:unpaused | Player → Parent | Playback started or resumed. Adds currentTime. Hide that UI again. |
vidrift:ui-visible | Player → Parent | The player's own controls (top and bottom bars) came on screen: mouse move, tap, or pause. Show an overlay of yours at the same moment and it reads as part of the player. |
vidrift:ui-hidden | Player → Parent | The controls auto-hid, the pointer left while playing, or the player was locked. Hide that overlay again. Sent once per change, plus once on load for the initial state. |
vidrift:nextup | Player → Parent | Once, as the credits or ending theme start (30 seconds before the end when no skip data exists). |
vidrift:nextup-play | Player → Parent | The Up Next countdown finished, or the viewer clicked Play Next or the Next episode button. Only sent once your page has sent vidrift:nextup-info; otherwise the player moves on by itself. |
vidrift:episode | Player → Parent | The viewer picked an episode in the player, or Up Next moved on by itself. Carries the new season and episode; the iframe swaps right after. |
vidrift:ended | Player → Parent | Playback reached the end. |
vidrift:exit | Player → Parent | The viewer clicked the Exit button (?exit=1). Close or navigate away from the player on your side; the player does nothing else. |
vidrift:mobile-panel | Player → Parent | A phone viewer opened a panel, with mobileSheets=true. Adds panel, options and current. |
vidrift:fullscreen | Player → Parent | With fs=parent: the viewer pressed the fullscreen button, F or double-clicked. on is true to enter, false to leave. Call requestFullscreen() on the element around the iframe right away, inside the message handler, so the viewer's click still counts. |
vidrift:state | Player → Parent | After your page sends vidrift:get-state: right away, then again whenever anything in it changes. See Controlling the player. |
vidrift:command-error | Player → Parent | A command could not be carried out. Adds command, field and reason. |
vidrift:play · vidrift:pause | Parent → Player | Play or pause. |
vidrift:skip | Parent → Player | Move by seconds, negative to go back. |
vidrift:set | Parent → Player | Change volume, speed, server, quality, audio, subtitles, their look or their sync. |
vidrift:cast | Parent → Player | Start casting; needs one click inside the player. |
vidrift:get-state | Parent → Player | Ask for vidrift:state and keep it coming. |
vidrift:resume | Parent → Player | Seek to currentTime in seconds. Safe to send before the video is ready; it is applied once it is. |
vidrift:quality-preference | Parent → Player | Pin a rendition: label is "1080p", "720p", "480p", "360p" or "Auto". |
vidrift:exit-control | Parent → Player | With exit=1 in the URL: show: false hides the player's Back arrow, show: true brings it back, without changing the URL. For a page that draws its own back control on some layouts (a phone header above the player) and leaves it to the player on others; send it on load and whenever the layout changes. |
vidrift:title-control | Parent → Player | show: false takes the title and episode line out of the player's top bar outside fullscreen, show: true puts them back. In fullscreen your page is not on screen, so the player shows them there anyway; hide=title removes them everywhere. For a page that prints the title itself on some layouts; send it on load and whenever the layout changes. |
vidrift:nextup-info | Parent → Player | Take over episodes: the Up Next card and Next button use your next: { season, episode, name?, still? } (an https still shows on the card), and the in-player Episodes list steps aside. Send next: null on the last episode. |
vidrift:mobile-option | Parent → Player | Reply to a mobile-panel message with the chosen option and value. |
vidrift:fullscreen-state | Parent → Player | With fs=parent: tell the player whether your box is fullscreen (on). Send it on every fullscreenchange and once each new iframe has loaded, so the button icon, the title bar and the phone sheets follow. |
window.addEventListener("message", (e) => { if (e.origin !== "https://embed.vidrift.net") return; switch (e.data.type) { case "vidrift:progress": save(e.data.tmdbId, e.data.currentTime); break; case "vidrift:ended": markWatched(e.data.tmdbId); break; } });
Controlling the player
Your page can drive the player and read everything its menus offer. Pause it while a menu of yours is open, or move Quality, Server, Audio and Subtitles into your own sidebar and take them out of the player with hide=settings,subtitles,server. Commands are messages to the iframe. The player answers vidrift:get-state with vidrift:state, and anything it cannot do with vidrift:command-error.
| Send | Fields | Does |
|---|---|---|
vidrift:play | Plays. Sent before the stream is ready, it lets the start go ahead. | |
vidrift:pause | Pauses. Sent before the stream is ready, the first frame waits. | |
vidrift:skip | seconds | 10 forward, -10 back. Stops at the start and the end, and flashes the seek icon on the video, as a double tap does. |
vidrift:set | any below | Changes one field or several in one message. |
vidrift:cast | Starts casting to a TV, with one click from the viewer (below). | |
vidrift:get-state | Answers with vidrift:state, then sends it again every time something in it changes. Send it once, after the iframe loads. |
{
type: "vidrift:state", tmdbId, mediaType, season, episode, anilist,
paused: false, currentTime: 812.4, duration: 2884.1,
volume: 0.8, muted: false, speed: 1,
server: "vela", servers: [{ value: "vela", label: "Vela", state: "ready" }, …],
source: "0", sources: [{ value: "0", label: "Vela · English" }, …],
quality: "2", qualities: [{ value: "auto", label: "Auto" }, …],
audio: "0", audios: [{ value: "0", label: "English" }, …],
subtitle: "en", subtitles: [{ value: "off", label: "Off" }, …],
subtitleStyle: { size: 100, color: "#ffffff", background: "box",
font: "default", position: 0 },
subtitleOffset: 0,
cast: "available" // "unavailable" | "available" | "connecting" | "connected"
}vidrift:settakesvolume(0 to 1),muted,speed(0.25 to 4),subtitleOffset(seconds, −30 to 30; positive shows captions later, the "Sync to audio" control) andsubtitleStylewith any ofsize(percent, 50 to 200),color(#rrggbb),background(boxorshadow),font(default,systemorserif) andposition(0 to 40, how far the captions are raised).- For
server,source,quality,audioandsubtitle, send back avalueexactly as its list gives it.qualityalso takes a label, such as"720p", so you can pin a rendition without reading the list. A server picked this way is remembered for the viewer, as a pick from the player's own menu is. - The lists change while a title plays: subtitles arrive, a failing server hands over to the next, and each server brings its own qualities and audio.
vidrift:statefollows all of it, so draw your menus from the latest one.audiosis empty when there is only one track. A server'sstateis"ready"once it has played here,"failed"if it had nothing, and empty before it is tried.currentTimeis a snapshot;vidrift:progresskeeps time.
When a command is refused
vidrift:command-error names the command, for vidrift:set the field, and a reason. The other fields of the same message still apply.
invalid: a value that is not in its list, or out of range.click-required: the viewer has to click inside the player first. Browsers turn sound on and open the Cast picker only from a click in the frame that plays, so unmuting can come back with this, and the video carries on muted. On some visits the player first checks the page it is embedded in; until the viewer's first click in the player, those take onlypauseandget-state.not-ready:vidrift:skipbefore the stream has loaded.unavailable:vidrift:castwith nothing to cast to.castin the state tells you before you offer it.
Casting
A device picker opens only from a click inside the player, and a message is not one. So vidrift:cast brings the controls up with the player's Cast button marked "Click to cast" for eight seconds (shown even with hide=cast), answers click-required, and the viewer's click on it starts casting.
const EMBED = "https://embed.vidrift.net"; const send = (msg) => iframe.contentWindow.postMessage(msg, EMBED); let state = null, wasPlaying = false; iframe.addEventListener("load", () => send({ type: "vidrift:get-state" })); window.addEventListener("message", (e) => { if (e.origin !== EMBED) return; if (e.data.type === "vidrift:state") { state = e.data; drawQualityMenu(state.qualities, state.quality); } if (e.data.type === "vidrift:command-error") console.warn(e.data.reason, e.data); }); function openMenu() { wasPlaying = state && !state.paused; send({ type: "vidrift:pause" }); } function closeMenu() { if (wasPlaying) send({ type: "vidrift:play" }); } // From your own buttons: send({ type: "vidrift:set", quality: "720p" }); send({ type: "vidrift:skip", seconds: -10 }); send({ type: "vidrift:set", subtitle: "en", subtitleStyle: { size: 120 } });
Mobile sheet
On phones the player opens its own bottom sheet for Settings, Subtitles and Source. If you would rather render that sheet in your site's own UI, add ?mobileSheets=true to the URL and answer the panel message. Leave the flag off and no listener is needed.
window.addEventListener("message", (e) => { if (e.origin !== "https://embed.vidrift.net") return; if (e.data.type !== "vidrift:mobile-panel") return; // e.data.panel: "settings" | "subtitles" | "source" // e.data.options: [{ option, value, label, group, desc? }] // "settings" carries quality, audio, speed and fit (video size) // e.data.current: selected now, e.g. { quality: "auto", fit: "contain" } const chosen = await showYourSheet(e.data.options); iframe.contentWindow.postMessage({ type: "vidrift:mobile-option", option: chosen.option, // use the chosen option's own field, not the panel name value: chosen.value }, "https://embed.vidrift.net"); });
- The
settingspanel always has aqualitygroup. When the stream offers nothing to choose it is a singleAutoentry, and itsdescsays why; showdescunder the label when it is present. fittakescontain(Fit),cover(Fill) orfill(Stretch).- In fullscreen the player opens its own sheet instead of asking for yours: only the player is on screen there, so your sheet would open behind it.
Notes & limits
- hls.js ships inside every embed page. There are no dependencies to add on your side.
- Mobile-responsive, with iOS Safari fullscreen via
webkitEnterFullscreen. That is Safari's own player, so on iPhone the player's menus are available outside fullscreen only. - Embeds are rate-limited per client. Normal traffic never touches the limit; scripted bulk fetching does.
- Public embeds carry a single ad. That is what pays for the bandwidth.
- Metadata and artwork come from TMDB. VidRift is not endorsed or certified by TMDB.
- Issues and takedown requests reach a human on Discord.