iPlay 适配器脚本开发教程
iPlay 浏览器扩展可以加载外部适配器脚本,把网站页面里的媒体入口转换成 iPlay Direct 可以播放的直链。适配器脚本运行在目标网页中,负责识别页面、添加 iPlay 按钮、解析媒体地址,然后交给 iPlay 打开。
在 Chrome 和 Edge 中,如果你从本地文件或外部地址加载适配器脚本,请到扩展管理页打开 iPlay 扩展的详情页,并启用“允许用户脚本”或“User scripts”权限。没有这个权限时,扩展可能无法把外部脚本注入到网页。
为什么不用油猴脚本
iPlay 最早也尝试过用油猴脚本来做网页适配,但实际使用后遇到了一些限制,所以才额外提供浏览器扩展和适配器脚本机制。
第一,油猴脚本不适合处理某些需要沙盒能力的解析逻辑。一些站点会把真实播放地址藏在受限脚本、动态代码或混淆逻辑里,适配器有时需要在隔离环境中使用 new Function、独立执行代码或调用扩展能力来绕开页面限制。普通油猴脚本在这些场景下很难稳定实现。
第二,油猴脚本在 macOS 平台上存在付费门槛,而 iPlay 的大部分功能是免费提供给用户使用的。单独依赖油猴会让部分用户为了使用网页适配功能额外购买第三方工具,这和 iPlay 希望降低使用门槛的方向不一致。
第三,iPlay 后续计划把资源嗅探等能力集成到主程序和多端生态中。浏览器扩展提供的适配器模型更容易和主程序、iOS、Android 等平台共享解析思路或能力边界;如果完全基于油猴脚本,后续跨平台迁移和统一能力管理都会更困难。
目录结构
iplay-adapter-example/
adapter.json 适配器元信息:id、显示名称、匹配规则。
src/adapter.ts 适配器入口:导出 createAdapters(api)。
src/helper.ts 示例工具函数,会被 Vite 打包进最终 JS。
src/iplay.ts 最小 iPlay 适配器 API 类型定义。
vite.config.ts 构建一个带元信息的独立 JS 文件。快速开始
npm install
npm run typecheck
npm run build构建产物位于:
dist/example.js你可以在 iPlay Direct 弹窗中使用 Browse JS 选择这个文件,也可以把它部署到 HTTP(S) 地址后添加 URL。
开发流程
- 修改
adapter.json。id必须稳定且唯一。它会用于按钮标记、全局模块名和构建产物文件名。name是给用户看的名称。matches决定扩展在哪些网页注入适配器,例如*://example.com/*。
- 修改
src/adapter.ts。createAdapters(api)创建并返回一个或多个站点适配器。canHandleLocation()判断当前页面是否属于这个适配器。scan()扫描页面,把 iPlay 按钮挂到媒体卡片、链接或播放入口上。getBestResult()根据按钮点击时传入的上下文解析最终媒体地址。
- 保持依赖在项目内部。
- Vite 会把本地 import 的文件打包到最终 JS。
api.sandbox.runFunction()中运行的函数是单独序列化执行的,不能直接访问模块变量、DOM 对象或api。
入口函数
适配器脚本必须导出 createAdapters(api):
export function createAdapters(api: ExternalAdapterApi) {
return createExampleAdapter(api);
}它的作用是把扩展提供的能力注入给你的适配器。你不需要自己访问扩展内部对象,只要通过 api 创建按钮、发请求、读取设置、打开媒体即可。
如果一个脚本支持多个站点,可以返回数组:
export function createAdapters(api: ExternalAdapterApi) {
return [
createMovieSiteAdapter(api),
createTvSiteAdapter(api)
];
}SiteAdapter 接口
模板中的核心类型是:
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 是适配器的程序标识。它应当和 adapter.json 中的 id 保持一致,避免多个适配器之间冲突。
name 是展示给用户看的名称。调试日志、按钮标题或错误提示中可以使用它。
refererUrl 是默认来源页。当最终媒体地址需要 Referer 请求头时,iPlay 可以使用这个值作为兜底。
canHandleLocation(href = location.href) 用来判断当前 URL 是否应该交给这个适配器处理。即使 adapter.json.matches 已经做了第一层过滤,也建议在这里再判断一次,因为同一个脚本可能被加载到多个相近页面。
scan(root = document) 用来扫描页面并绑定按钮。首次加载、页面局部刷新或 SPA 路由切换时都可能调用它,所以实现时要避免重复绑定。模板使用 element.dataset.iplayBound = "true" 做标记。
getBestResult(mediaRef) 是真正解析媒体地址的函数。用户点击按钮后,api.openMedia(adapter, mediaRef) 会调用它。这里可以请求站点 API、解析页面 JSON、选择清晰度或拆分音视频流,最后返回 MediaResult。
getTitle(mediaRef) 是可选函数,用来从上下文中生成播放标题。没有实现时,仍然可以在 getBestResult() 返回的 title 中提供标题。
reset() 是可选函数,适合清理缓存、移除旧按钮或处理 SPA 页面切换后的状态重置。
泛型参数和上下文
SiteAdapter<MediaRef> 中的 MediaRef 是“媒体上下文”的类型。它把 scan() 在页面上发现的信息传给 getBestResult()。
例如模板定义了:
type ExampleMediaRef = {
apiUrl: string;
title?: string | null;
};
function createExampleAdapter(api: ExternalAdapterApi): SiteAdapter<ExampleMediaRef> {
// ...
}在 scan() 中,适配器从链接上取出 API 地址和标题:
api.openMedia(this, {
apiUrl,
title: element.textContent?.trim() || document.title
});随后 getBestResult(ref) 就能安全地使用 ref.apiUrl 和 ref.title:
async getBestResult(ref) {
const response = await api.fetch(ref.apiUrl, { credentials: "include" });
return normalizeMediaResult(await response.json(), this.getTitle?.(ref) || "Example");
}建议把 MediaRef 设计成普通 JSON 对象,只包含字符串、数字、布尔值、数组和简单对象。不要把 DOM 节点、函数、Response、Map 等复杂对象放进去。这样更容易调试,也方便以后把解析逻辑移动到沙盒或后台能力中。
ExternalAdapterApi 常用能力
api.createButton(size?, adapterId?, title?)创建 iPlay 浮动按钮。通常在 scan() 中使用,然后把按钮插入媒体卡片或链接内部。
api.ensureRelativePosition(element)确保目标元素可以承载绝对定位按钮。如果页面元素没有合适的定位上下文,这个函数会帮你补齐。
api.absoluteUrl(urlText, base?)把相对地址转换成绝对地址。抓取 data-*、href、src 等属性时经常会用到。
api.openMedia(adapter, mediaRef, buttonTitle?)让 iPlay 开始解析并打开媒体。它会调用当前适配器的 getBestResult(mediaRef)。
api.fetch(input, init)
api.fetchText(url, init)
api.fetchJson<T>(url, init)
api.fetchRaw(url, init)通过扩展提供的请求能力访问页面或站点 API。需要携带登录态时,通常设置 credentials: "include"。
api.config.getAll()
api.config.get(key, fallback)
api.config.set(key, value)
api.getSettings()读取或保存扩展设置,例如用户偏好的播放器内核、语言、YouTube 音轨和流偏好。getSettings() 是读取全部设置的便捷方法。
api.sandbox.runFunction(fn, ...args)
api.sandbox.runCode(code, input)在沙盒页执行代码。沙盒函数会被序列化后执行,所以不能直接使用外部作用域里的变量、import 的工具函数、DOM 节点或 api。需要的数据必须通过参数传入。
api.requestCapability({ action, payload })请求扩展能力,支持 fetchRaw、fetchText、fetchJson、getCookies、sha1Hex、openUrl、getUrl。当站点解析需要 cookie、哈希或跨上下文能力时可以使用。
api.openUrl(url)打开一个 URL,适合跳转登录页、授权页或调试页面。
api.registerAdapter(adapter)手动注册适配器。大多数模板只需要从 createAdapters() 返回适配器即可。
api.log(site, ...args)
api.warn(site, ...args)输出带站点名的调试日志。适配器开发时建议保留关键解析节点的日志。
MediaResult 返回值
getBestResult() 必须返回:
{
videoUrl: "https://cdn.example.com/video.mp4",
audioUrl: null,
title: "Video title",
refererUrl: "https://example.com"
}常用字段:
videoUrl:必填,视频流或主播放地址。audioUrl:可选,独立音频流地址。音视频分离时填写。title:可选,播放器窗口标题。refererUrl:可选,视频请求需要的Referer。subtitleUrl、subtitleRefererUrl、subtitleTitle:可选,字幕地址、字幕来源和字幕名称。commentUrl、commentRefererUrl、commentTitle:可选,弹幕或评论流地址、来源和名称。prepared、picked:可选,保留扩展字段,适合放调试信息或上游选择结果。
iPlay URL 参数
iPlay Direct 打开媒体 URL 时可以附加参数:
iplay.window.title:播放器窗口标题。iplay.window.width、iplay.window.height:播放器窗口尺寸。iplay.http.*:媒体请求头,例如iplay.http.referer会设置Referer。iplay.kernel.type:覆盖播放器内核,可选值包括mpv、vlc、exo。iplay.audio.url:独立音频流地址。iplay.comment.url:弹幕或评论流地址。iplay.subtitle.url:字幕地址。iplay.video.url:主视频地址。
iPlay Direct 支持 https:// 和 http:// 媒体地址。为了在不破坏原始 URL 的情况下附加 iPlay 参数,适配器也可以使用:
iplays:// -> https://
iplay:// -> http://例如把:
https://cdn.example.com/video.mp4改成:
iplays://cdn.example.com/video.mp4?iplay.window.title=Example&iplay.http.referer=https%3A%2F%2Fexample.comiPlay Direct 会以 https://cdn.example.com/video.mp4 播放,并附带窗口标题和 Referer 请求头。
Agent.md 示例
如果你希望用 AI 帮你为某个网站开发适配器,可以在适配器项目根目录放一个 Agent.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.调试建议
- 先让
canHandleLocation()只匹配一个你正在测试的页面。 - 在
scan()中打印找到的元素数量,确认选择器有效。 - 给已绑定元素加
data-iplay-bound,避免 SPA 重复扫描造成多个按钮。 - 先返回一个固定测试 URL 验证
api.openMedia()流程,再接入真实解析逻辑。 - 如果请求失败,检查站点是否需要 cookie、
Referer、特定请求头或沙盒解析。 - Chrome/Edge 中脚本没有注入时,优先检查扩展详情页的用户脚本权限是否已开启。