Skip to content

iPlay Adapter Script Tutorial

The iPlay browser extension can load external adapter scripts that turn media entries on a website into direct playback URLs for iPlay Direct. The adapter runs on the target page. It detects supported pages, adds iPlay buttons, resolves media URLs, and asks iPlay to open them.

Download the adapter template

In Chrome and Edge, if you load an adapter from a local file or an external URL, open the iPlay extension details page in the browser extension manager and enable the "User scripts" permission. Without this permission, the extension may not be allowed to inject external adapter scripts into web pages.

Why Not Userscripts

iPlay initially tried userscripts for website adaptation, but real-world use exposed several limits. That is why iPlay provides a dedicated browser extension and adapter script model.

First, userscripts are not a good fit for some parsing logic that needs sandbox capabilities. Some sites hide the real playback URL behind restricted scripts, dynamic code, or obfuscated logic. An adapter may need to run new Function, execute isolated code, or call extension capabilities to work around page restrictions. Regular userscripts are difficult to make reliable in these cases.

Second, userscript tools can have a paid barrier on macOS, while most iPlay features are free to use. Depending entirely on userscripts would ask some users to buy a third-party tool just to use web adapters, which does not match iPlay's goal of keeping access simple.

Third, iPlay plans to integrate resource sniffing and related capabilities into the main app and its multi-platform ecosystem. The extension adapter model is easier to align with the main app, iOS, Android, and future platforms. A userscript-only design would make cross-platform migration and unified capability management harder.

File Layout

text
iplay-adapter-example/
  adapter.json        Adapter metadata: id, display name, and match rules.
  src/adapter.ts      Adapter entry: exports createAdapters(api).
  src/helper.ts       Example helper bundled into the final JS.
  src/iplay.ts        Minimal iPlay adapter API type definitions.
  vite.config.ts      Builds one standalone JS file with adapter metadata.

Quick Start

bash
npm install
npm run typecheck
npm run build

The build output is:

text
dist/example.js

Load this file from the iPlay Direct popup with Browse JS, or host it and add its HTTP(S) URL.

Development Flow

  1. Edit adapter.json.
    • id must be stable and unique. It is used for button markers, the global module name, and the output filename.
    • name is the display name shown to users.
    • matches controls where the extension injects the adapter, such as *://example.com/*.
  2. Edit src/adapter.ts.
    • createAdapters(api) creates and returns one or more site adapters.
    • canHandleLocation() checks whether the current page belongs to this adapter.
    • scan() scans the page and attaches iPlay buttons to media cards, links, or playback entries.
    • getBestResult() resolves the final media URL from the context collected when the user clicked the button.
  3. Keep dependencies local to this project.
    • Vite bundles local imports into the final JS.
    • Functions passed to api.sandbox.runFunction() are serialized and executed separately, so they cannot directly access module variables, DOM objects, or api.

Entry Function

Every adapter script must export createAdapters(api):

ts
export function createAdapters(api: ExternalAdapterApi) {
  return createExampleAdapter(api);
}

This function injects extension-provided capabilities into your adapter. You do not need to access extension internals directly. Use api to create buttons, send requests, read settings, and open media.

To support multiple sites from one script, return an array:

ts
export function createAdapters(api: ExternalAdapterApi) {
  return [
    createMovieSiteAdapter(api),
    createTvSiteAdapter(api)
  ];
}

SiteAdapter Interface

The core template type is:

ts
export type SiteAdapter<MediaRef = unknown> = {
  id: string;
  name: string;
  refererUrl: string;
  canHandleLocation(href?: string): boolean;
  getBestResult(mediaRef: MediaRef): Promise<MediaResult>;
  getTitle?(mediaRef: MediaRef): string | null;
  scan(root?: ParentNode): void;
  reset?(): void;
};

id is the programmatic adapter identifier. Keep it aligned with adapter.json to avoid conflicts between adapters.

name is the user-facing name. It can be used in logs, button titles, and error messages.

refererUrl is the default source page. iPlay can use it as a fallback Referer when the media request requires one.

canHandleLocation(href = location.href) decides whether this adapter should handle the current URL. Even though adapter.json.matches already filters pages, this function is still useful when one script is injected into several related pages.

scan(root = document) finds media entries and binds buttons. It may run on initial load, partial page updates, or SPA route changes, so avoid duplicate binding. The template marks processed elements with element.dataset.iplayBound = "true".

getBestResult(mediaRef) performs the real media resolution. After the user clicks a button, api.openMedia(adapter, mediaRef) calls this function. Use it to request site APIs, parse embedded JSON, choose quality, or handle separate audio/video streams, then return a MediaResult.

getTitle(mediaRef) is optional and returns a playback title from the context. You can also provide title directly in getBestResult().

reset() is optional. Use it to clear caches, remove stale buttons, or reset state after SPA navigation.

Generics and Context

SiteAdapter<MediaRef> uses MediaRef as the type of the media context. It carries data found by scan() into getBestResult().

The template defines:

ts
type ExampleMediaRef = {
  apiUrl: string;
  title?: string | null;
};

function createExampleAdapter(api: ExternalAdapterApi): SiteAdapter<ExampleMediaRef> {
  // ...
}

In scan(), the adapter collects an API URL and title from the link:

ts
api.openMedia(this, {
  apiUrl,
  title: element.textContent?.trim() || document.title
});

Then getBestResult(ref) can safely use ref.apiUrl and ref.title:

ts
async getBestResult(ref) {
  const response = await api.fetch(ref.apiUrl, { credentials: "include" });
  return normalizeMediaResult(await response.json(), this.getTitle?.(ref) || "Example");
}

Design MediaRef as a plain JSON-like object containing strings, numbers, booleans, arrays, and simple objects. Avoid DOM nodes, functions, Response objects, Map, and class instances. This keeps the adapter easier to debug and leaves room to move parsing work into sandbox or background capabilities later.

ExternalAdapterApi Essentials

ts
api.createButton(size?, adapterId?, title?)

Creates the floating iPlay button. Use it in scan(), then append the button to a media card or link.

ts
api.ensureRelativePosition(element)

Ensures the target element can host an absolutely positioned button.

ts
api.absoluteUrl(urlText, base?)

Converts relative URLs into absolute URLs. Use it for values from data-*, href, and src attributes.

ts
api.openMedia(adapter, mediaRef, buttonTitle?)

Starts media resolution and playback. It calls adapter.getBestResult(mediaRef).

ts
api.fetch(input, init)
api.fetchText(url, init)
api.fetchJson<T>(url, init)
api.fetchRaw(url, init)

Uses extension-provided request helpers to access pages or site APIs. If the request needs the user's login session, usually set credentials: "include".

ts
api.config.getAll()
api.config.get(key, fallback)
api.config.set(key, value)
api.getSettings()

Reads or writes extension settings, including player kernel, language, YouTube audio language, and stream preference. getSettings() is a convenience wrapper for reading all settings.

ts
api.sandbox.runFunction(fn, ...args)
api.sandbox.runCode(code, input)

Runs code in the sandbox page. Sandbox functions are serialized before execution, so they cannot directly use outer-scope variables, imported helpers, DOM nodes, or api. Pass every required value as an argument.

ts
api.requestCapability({ action, payload })

Requests extension capabilities such as fetchRaw, fetchText, fetchJson, getCookies, sha1Hex, openUrl, and getUrl.

ts
api.openUrl(url)

Opens a URL, useful for login, authorization, or debugging flows.

ts
api.registerAdapter(adapter)

Registers adapters manually. Most template-based projects can simply return adapters from createAdapters().

ts
api.log(site, ...args)
api.warn(site, ...args)

Writes site-prefixed debug logs. Keep logs around important parsing decisions while developing.

MediaResult Shape

getBestResult() must return:

ts
{
  videoUrl: "https://cdn.example.com/video.mp4",
  audioUrl: null,
  title: "Video title",
  refererUrl: "https://example.com"
}

Common fields:

  • videoUrl: required video stream or main playback URL.
  • audioUrl: optional separate audio stream URL.
  • title: optional player window title.
  • refererUrl: optional Referer for video requests.
  • subtitleUrl, subtitleRefererUrl, subtitleTitle: optional subtitle URL, referer, and label.
  • commentUrl, commentRefererUrl, commentTitle: optional comment or danmaku stream URL, referer, and label.
  • prepared, picked: optional extension fields for debug data or upstream selection results.

iPlay URL Parameters

iPlay Direct can open media URLs with additional query parameters:

  • iplay.window.title: player window title.
  • iplay.window.width, iplay.window.height: preferred player window size.
  • iplay.http.*: media request headers. For example, iplay.http.referer sets Referer.
  • iplay.kernel.type: overrides the player kernel. Common values include mpv, vlc, and exo.
  • iplay.audio.url: separate audio stream URL.
  • iplay.comment.url: comment or danmaku stream URL.
  • iplay.subtitle.url: subtitle URL.
  • iplay.video.url: main video URL.

iPlay Direct recognizes https:// and http:// media URLs. To add iPlay parameters without changing the original URL scheme, adapters can also use:

js
iplays:// -> https://
iplay:// -> http://

For example:

text
iplays://cdn.example.com/video.mp4?iplay.window.title=Example&iplay.http.referer=https%3A%2F%2Fexample.com

iPlay Direct plays it as https://cdn.example.com/video.mp4 and applies the title plus Referer header.

Agent.md Example

If you want AI assistance while building an adapter for a website, place an Agent.md file at the adapter project root. You can start with this:

md
# iPlay Adapter Agent Guide

You are helping build an iPlay Direct browser adapter script.

## Goal

Create or update a TypeScript adapter that detects playable media entries on the target website, adds iPlay buttons, and returns direct media URLs through the iPlay adapter API.

## Project Rules

- Use the existing template structure.
- Keep the adapter id stable and aligned between `adapter.json` and `SiteAdapter.id`.
- Implement `createAdapters(api)` as the public entry.
- Use `SiteAdapter<MediaRef>` with a typed plain-object `MediaRef`.
- Collect page context in `scan()` and pass it to `api.openMedia(adapter, mediaRef)`.
- Resolve final playback data in `getBestResult(mediaRef)`.
- Avoid duplicate buttons by marking processed elements with `data-iplay-bound="true"`.
- Use `api.absoluteUrl()` for relative URLs.
- Use `api.fetch()`, `api.fetchJson()`, or `api.requestCapability()` instead of raw global assumptions when extension capabilities are needed.
- Do not put DOM nodes, functions, Response objects, Map, or class instances inside `MediaRef`.
- Keep sandbox functions self-contained. Pass every value they need as an argument.
- Run `npm run typecheck` and `npm run build` before finishing.

## Target Site Notes

- Domain:
- Example media page URL:
- List/card selector:
- Title selector:
- API endpoint or embedded JSON source:
- Required cookies or headers:
- Stream selection rule:
- Subtitle/comment source:

## Expected Result

`getBestResult()` should return a `MediaResult` with at least `videoUrl`, and include `title`, `refererUrl`, `audioUrl`, `subtitleUrl`, or `commentUrl` when available.

Debugging Tips

  • Start with canHandleLocation() matching only the page you are testing.
  • Log the number of elements found in scan() to confirm your selectors.
  • Mark processed elements with data-iplay-bound to avoid duplicate buttons on SPA pages.
  • Return a fixed test URL first to verify the api.openMedia() flow, then add real parsing.
  • If requests fail, check whether the site requires cookies, Referer, special headers, or sandbox parsing.
  • If scripts are not injected in Chrome or Edge, first check that the extension details page has the User scripts permission enabled.