开发技巧

用 Twitch 官方播放器搭建可切换的频道墙

做一个“频道墙”网页:页面上嵌入官方 Twitch 播放器,上方有一排按钮,点一下就切换到对应的频道。重点讲清楚 parent 参数:嵌入用的 JS 会自动带上当前域名,而直接写 iframe 时必须手动写对 parent,否则播放器会显示 “This embed is misconfigured”。

BINBHO · 图文教程

做一个“频道墙”网页:页面上嵌入官方 Twitch 播放器,上方有一排按钮,点一下就切换到对应的频道。重点讲清楚 parent 参数:嵌入用的 JS 会自动带上当前域名,而直接写 iframe 时必须手动写对 parent,否则播放器会显示 “This embed is misconfigured”。

先看清成品

我在 box 上用 Chrome(playwright)分别通过 localhost 和 127.0.0.1 打开页面,收到了 VIDEO_READY 事件,日志显示“播放器就绪:twitchdev”;点“twitchgaming”按钮后,getPlayer().getChannel() 返回 twitchgaming。播放器里显示 “TwitchDev is offline”,这是测试时频道没有在直播,属于真实状态。直接写 iframe 时,parent=www.example.com 返回 302,并显示 “Whoops! This embed is misconfigured.”;parent=localhost 返回 200,正常显示。没有测试正在直播的频道是否真的能出画面和声音(box 是无头环境)。

注意:只使用 Twitch 官方 API、EventSub 和官方嵌入播放器,并遵守《Twitch 开发者服务协议》。不要抓取或转录直播流,不要遮挡或去掉播放器上的广告,不要共享账号或 Client Secret,也不要用 VPN 或代理绕过地区限制。

做完之后你会得到什么?

成果 说明
index.html Twitch.Embed + VIDEO_READY 事件 + 按钮切换频道
iframe_only.html 不加载 JS 的纯 iframe 写法
e2e.py 自动打开页面、等待就绪、点击切换并检查结果

需要准备哪些环境?

项目 要求 说明
浏览器 现代浏览器 不需要登录 Twitch
网页托管 有自己的域名(正式上线时) parent 要写这个域名
Python + playwright 可选 只有运行 e2e.py 时才需要

官方要点(Embedding Twitch)

项目 内容
脚本 https://embed.twitch.tv/embed/v1.js,然后 new Twitch.Embed(元素id, 选项)
parent 嵌入页面所在的域名;可以有多个,写成数组
layout video-with-chat(传了 channel 时的默认值,只支持直播)/ video
尺寸 视频窗口至少 400×300;width 最小 400,height 最小 300
事件 Twitch.Embed.VIDEO_READY(就绪,可以调用 API)、VIDEO_PLAY
编程控制 embed.getPlayer() 返回播放器对象,可以调用 setChannel、getChannel 等

文件结构

35-twitch-C-src/
├── index.html
├── iframe_only.html
└── e2e.py

第 1 步:页面

index.html:

<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>我的 Twitch 频道墙</title>
<style>body{font-family:sans-serif;margin:16px} button{margin:4px} #log{color:#555;font-size:13px}</style></head>
<body>
  <h1>Twitch 频道墙</h1>
  <div id="buttons"></div>
  <div id="twitch-embed"></div>
  <p id="log">加载中…</p>
  <script src="https://embed.twitch.tv/embed/v1.js"></script>
  <script>
    // 频道清单自己维护(填登录名)。parent 必须是这个页面所在的域名,否则播放器会拒绝加载。
    const CHANNELS = ["twitchdev", "twitchgaming", "twitch"];
    const PARENTS = [location.hostname];   // 上线时改成固定的域名列表,例如 ["www.example.com"]
    const log = (t) => (document.getElementById("log").textContent = t);

    const embed = new Twitch.Embed("twitch-embed", {
      width: 854, height: 480, channel: CHANNELS[0],
      layout: "video", autoplay: false, muted: true, parent: PARENTS
    });
    embed.addEventListener(Twitch.Embed.VIDEO_READY, () => {
      window.playerReady = true;
      log("播放器就绪:" + embed.getPlayer().getChannel());
    });
    embed.addEventListener(Twitch.Embed.VIDEO_PLAY, () => log("开始播放"));

    for (const c of CHANNELS) {
      const b = document.createElement("button");
      b.textContent = c;
      b.onclick = () => { embed.getPlayer().setChannel(c); log("切换到 " + c); };
      document.getElementById("buttons").appendChild(b);
    }
  </script>
</body>
</html>

iframe_only.html:

<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>只用 iframe 的嵌入</title></head>
<body>
  <!-- 不加载 JS,直接用 iframe。parent 要手动写成页面所在的域名;多个域名就写多个 parent 参数 -->
  <iframe id="raw" src="https://player.twitch.tv/?channel=twitchdev&parent=localhost&muted=true&autoplay=false"
          height="480" width="854" allowfullscreen></iframe>
</body></html>

本地测试时用 python -m http.server 打开页面,然后访问 http://localhost:端口/。直接双击打开 file:// 页面时没有域名,parent 无法匹配。

第 2 步:自动检查

e2e.py:

"""本地起一个静态服务器(端口 0 = 随机空闲端口),用 Chromium 打开页面,等 VIDEO_READY,再点按钮切换频道。
需要:pip install playwright,并安装 Google Chrome(或改成 playwright install chromium 后去掉 channel 参数)"""
import asyncio, functools, http.server, threading
from pathlib import Path
from playwright.async_api import async_playwright

def serve():
    class Quiet(http.server.SimpleHTTPRequestHandler):
        def log_message(self, *a): pass
    h = functools.partial(Quiet, directory=str(Path(__file__).parent))
    s = http.server.ThreadingHTTPServer(("127.0.0.1", 0), h)
    threading.Thread(target=s.serve_forever, daemon=True).start()
    return s.server_port

async def main(host="localhost", page="index.html"):
    port = serve()
    async with async_playwright() as p:
        b = await p.chromium.launch(channel="chrome"); pg = await b.new_page()
        await pg.goto(f"http://{host}:{port}/{page}")
        frame_src = await pg.locator("#twitch-embed iframe").get_attribute("src", timeout=20000)
        print("iframe:", frame_src)
        await pg.wait_for_timeout(6000)
        print("iframe 内文字:", (await pg.frames[1].inner_text("body"))[:120].replace("\n", " ") if len(pg.frames) > 1 else "无")
        try:
            await pg.wait_for_function("window.playerReady === true", timeout=45000)
            print(await pg.inner_text("#log"))
            await pg.click("text=twitchgaming"); await pg.wait_for_timeout(3000)
            print(await pg.inner_text("#log"), "| getChannel =", await pg.evaluate("embed.getPlayer().getChannel()"))
        except Exception as e:
            print("没有等到 VIDEO_READY:", type(e).__name__)
        await b.close()

if __name__ == "__main__":
    import sys; asyncio.run(main(*sys.argv[1:]))
$ python e2e.py
iframe: https://embed.twitch.tv?autoplay=false&channel=twitchdev&height=480&layout=video&muted=true&parent=localhost&referrer=...
iframe 内文字: OFFLINE TwitchDev is offline. Check out this Software and Game Development stream from Oct 17, 2024. ...
播放器就绪:twitchdev
切换到 twitchgaming | getChannel = twitchgaming
$ python e2e.py 127.0.0.1
iframe: https://embed.twitch.tv?...&parent=127.0.0.1&...
播放器就绪:twitchdev
切换到 twitchgaming | getChannel = twitchgaming

检查点:iframe 地址里的 parent 和你访问时用的主机名一致,并且 #log 显示了“播放器就绪”。

调试示例:parent 写错了,为什么 JS 版本照样能播?

我故意把 index.html 里的 PARENTS 改成 ["www.example.com"],用 localhost 打开,播放器照样就绪。查看 iframe 地址才发现:

...&parent=www.example.com&parent=localhost&...

v1.js 会自动把当前页面的域名加进去,所以用 JS 嵌入时,这个错误不会表现出来。换成直接写 iframe 的版本,问题就出现了:

parent=localhost        -> 200  正常显示播放器
parent=www.example.com  -> 302  Whoops! This embed is misconfigured.

结论:用纯 iframe 时,parent 必须和页面所在的域名完全一致(有多个域名就写多个 parent=)。JS 版本虽然能自动补上当前域名,上线时还是应该把真实域名写清楚。

常见问题

现象 原因 处理
This embed is misconfigured parent 和页面域名对不上 把实际域名写进 parent
播放器显示 offline 频道当时没有在直播 属于正常状态;可以改用 video 参数播放回放
自动播放没有声音 浏览器的自动播放策略 先静音播放,由用户点击开声音
播放器很小,或者显示异常 尺寸小于最小值 宽度至少 400,高度至少 300

模板规则

  • 只用官方嵌入方式,不要遮挡播放器,也不要去掉播放器上的广告。
  • parent 写清楚真实域名,不要依赖 JS 自动补全。
  • 频道列表由自己维护;需要显示直播状态时,配合 A 篇在服务器端查询。

项目资源

用途 地址
Embed Everything https://dev.twitch.tv/docs/embed/everything/
视频和剪辑嵌入 https://dev.twitch.tv/docs/embed/video-and-clips/
Playwright for Python https://playwright.dev/python/
Twitch 开发者服务协议 https://www.twitch.tv/p/legal/developer-agreement/
注册应用(开发者控制台) https://dev.twitch.tv/console/apps

嵌入播放器只需要几行代码,parent 却是最容易出问题的地方。JS 版本会帮你自动补上当前域名,纯 iframe 不会。把两种情况都实测一遍,上线后就不会对着 “misconfigured” 发愣。