Skip to content
00Integration guide

A whole streaming player, in one iframe.

Point an iframe at a TMDB or IMDb id and it plays. Everything on this page is optional except the first snippet.

01Quick start

Quick start

Paste this. That is the whole integration.

  1. 1Drop in the iframe and point src at a title.
  2. 2Keep the allow and allowfullscreen attributes — fullscreen and picture-in-picture only work if the parent grants them.
  3. 3Give it a 16:9 box. The player fills whatever you give it and never scrolls.
index.html
<iframe
  src="https://player.example.com/movie/550"
  style="width:100%;aspect-ratio:16/9;border:0"
  allow="autoplay; fullscreen; encrypted-media; picture-in-picture"
  allowfullscreen
  referrerpolicy="origin"></iframe>

Nothing loads until someone presses play. Until then the frame is a poster and about 80 KB of JavaScript — no video, no requests to our sources. Embed it on a listing page with ten titles and it stays cheap.

A TV episode

episode.html
<iframe src="https://player.example.com/tv/1399/1/1"
  style="width:100%;aspect-ratio:16/9;border:0"
  allow="autoplay; fullscreen; encrypted-media; picture-in-picture"
  allowfullscreen></iframe>
<!-- 1399 = Game of Thrones (TMDB), season 1, episode 1 -->
02URL formats

URL formats

Both id spaces, both shapes. If you already have code that builds URLs for another embed provider, it almost certainly works here unchanged.

FormExample
/movie/{id}/movie/550 · /movie/tt0137523
/tv/{id}/{season}/{episode}/tv/1399/1/1 · /tv/tt0944947/1/1
/embed/movie/{id}Alias of the above, for drop-in replacement
/embed/tv/{id}/{season}/{episode}Same
/?type=movie&tmdb={id}Query form · id and imdb also accepted
/?type=tv&imdb={id}&season=1&episode=1Query form for series

Ids: a TMDB id (digits) or an IMDb id (tt + digits). A series URL without a season and episode plays S01E01. Unknown query parameters are ignored rather than fatal, so copying a competitor's URL still gets you a player.

03Options

Options

Every one is a query parameter, and every one is optional. Booleans accept 1, true or a bare flag.

ParameterValueWhat it does
tsecondsStart position. ?t=930 opens at 15:30.
autoplaybooleanTry to start without the poster press. Browsers block unmuted autoplay in a cross-origin frame, so treat this as a hint, not a guarantee.
posterhttps URLYour own artwork on the click-to-play screen.
subhttps URLAn extra subtitle track, listed first in the picker. WebVTT or SRT.
sub_langlabelWhat to call that track — e.g. ?sub_lang=Español.
colorhex, no #Accent for the whole frame — the play badge, the seek bar, focus rings and every control that takes the brand colour. ?color=e5a50a. Three or six digits. Your colour is kept; the labels on it flip between white and near-black by measured contrast, so an amber accent stays amber and reads at WCAG AA either way.
nextbuttonbooleanOn by default for series. ?nextbutton=0 hides it.
autonextbooleanRoll into the next episode when one ends.
servernumberPin one source instead of letting the player choose. Read it off SOURCE_CHANGE first.
originyour originLocks messaging to your site: events are posted only to this origin, and commands are accepted only from it. Recommended.
everything at once
// Episode 3, start at 2 minutes, auto-advance, your accent, locked to your origin
https://player.example.com/tv/1399/1/3?t=120&autonext=1&color=e5a50a&origin=https://yoursite.com
04Iframe setup

Iframe setup

Five things decide whether the player works properly in your page.

allow
autoplay; fullscreen; encrypted-media; picture-in-picture. These are parent-granted permissions — without them the buttons exist but do nothing.
allowfullscreen
Still required by older engines alongside allow="fullscreen". Cheap to keep.
referrerpolicy
origin. It sends us https://yoursite.com and never the page path — which is how the player knows whose site it is playing on, and so which titles to keep working for you. no-referrer hides that in Firefox and costs you the reporting; anything looser hands us a URL we do not want.
Your CSP
If your site sends a Content-Security-Policy, add frame-src https://player.example.com or the frame never loads.
no sandbox
Leave the attribute off. The player is ad-supported and a sandboxed frame cannot open the advertising window, so it earns nothing. If your platform forces one, it must include allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox — the last token is the one that matters, and without it the ad opens into your sandbox, does not count, and the player refuses to start.

Nothing else is required. The frame needs no extra attributes and no script on your page — drop it in, give it a box, and it plays. Add loading="lazy" if it sits below the fold.

Responsive without a wrapper

player.css
.embed-player {
  width: 100%;
  aspect-ratio: 16 / 9;  /* no padding-top hack needed */
  border: 0;
  border-radius: 12px;
  background: #000;
}
05Events & control

Events & control

The player talks to your page over postMessage, in both directions. Every message carries source: "embed-player"and version: 1 — check it, because any frame can post to your window.

What the player sends you

EventdataWhen
READY—The frame is listening. Safe to send commands.
PLAY—The viewer pressed play on the poster.
SOURCE_CHANGE{ server, name }A source answered and is now playing.
TIME_UPDATE{ currentTime, duration }Once a second while playing — enough for your own resume.
NEXT_EPISODE{ season, episode }The player moved to another episode (auto-next or its own drawer). Sync your list to it.
ENDED—Playback finished.
MEDIA_UNAVAILABLE—No source could serve this title. Show your own message or hide the frame.

What you can send it

CommandShape
PLAY / PAUSE{ source, command: "PLAY" }
SEEK{ source, command: "SEEK", time: 300 } — seconds
SET_VOLUME{ source, command: "SET_VOLUME", volume: 0.5 } — 0–1
SET_SERVER{ source, command: "SET_SERVER", server: 31 }
LOAD{ source, command: "LOAD", type, id, season?, episode? }

Use LOAD for binge watching. Swapping episodes with a command keeps the frame alive, so the warm cache and the already-resolved source survive and the next episode starts noticeably faster than reloading src would.

integration.js
const PLAYER = "https://player.example.com";
const frame = document.querySelector("#player");

// 1. Listen. Check the origin AND the source field.
window.addEventListener("message", (event) => {
  if (event.origin !== PLAYER) return;
  const msg = event.data;
  if (!msg || msg.source !== "embed-player") return;

  switch (msg.event) {
    case "TIME_UPDATE":
      saveProgress(msg.data.currentTime, msg.data.duration);
      break;
    case "NEXT_EPISODE":
      highlightEpisode(msg.data.season, msg.data.episode);
      break;
    case "MEDIA_UNAVAILABLE":
      showYourOwnFallback();
      break;
  }
});

// 2. Drive it. No reload — the frame keeps its warm cache.
function playEpisode(season, episode) {
  frame.contentWindow.postMessage(
    { source: "embed-player", command: "LOAD",
      type: "tv", id: "1399", season, episode },
    PLAYER,
  );
}
06Recipes

Recipes

The same embed, in whatever your site is built with.

index.html
<div class="player-box">
  <iframe id="player"
    src="https://player.example.com/movie/550?origin=https://yoursite.com"
    allow="autoplay; fullscreen; encrypted-media; picture-in-picture"
    allowfullscreen loading="lazy"></iframe>
</div>

<style>
  .player-box { width: 100%; max-width: 1100px; margin: 0 auto }
  .player-box iframe { width: 100%; aspect-ratio: 16/9; border: 0; border-radius: 12px }
</style>
07Troubleshooting

Troubleshooting

Nine times out of ten it is one of these five.

The frame never appears

Your own Content-Security-Policy is blocking it. Add frame-src https://player.example.com.

Fullscreen does nothing

Permissions are granted by the parent, not by us. Keep allow="fullscreen" and allowfullscreen.

It won't autoplay

No cross-origin frame may start unmuted audio without a gesture. The poster press is that gesture, which is why it is the default.

Resume position is forgotten

Browsers partition storage per embedding site, so preferences are per-partner and are dropped in private mode. Track progress from TIME_UPDATE if you need your own.

"This player was sandboxed"

Your iframe carries a sandbox attribute that blocks the advertising window funding the player, so playback is refused and the viewer is offered a link to watch in a tab of their own instead. Remove the attribute, or add allow-popups allow-popups-to-escape-sandbox to it.

You get this page instead of a player

Something fetched the URL as a top-level document — a server-side proxy, a preview bot, or a plain tab. Embed the URL in a real iframe; don't re-host or proxy it.

Good to know

  • Availability varies by title. The player walks several sources before giving up; when they all miss you get MEDIA_UNAVAILABLE rather than a spinner forever. Listen for it.
  • The player is ad-supported, and the ads stay inside the frame. Nothing is injected into your page and your own layout is never touched. Inside the frame we keep playback preferences and one analytics cookie, both partitioned to your site by the browser.
  • Do not add sandbox to the iframe. It stops the frame from opening the advertising window the player is funded by, so a sandboxed embed earns nothing and will refuse to play. See Iframe setup.
  • We record your hostname, never your URL. Usage analytics count views, titles and which sites embed the player — the domain only, which is why the snippet carries referrerpolicy="origin" rather than something that would send us a path. Going the other way, the frame is served Referrer-Policy: no-referrer, so the hosts we resolve learn nothing about you at all.
  • Fair-use rate limits apply per viewer IP. Normal embedding never approaches them; scripted mass requests do.

Want to see it run before you paste anything?

Open the test player