Stream(直播)

一条连接进来,从每座城市出去。

直播通过 RTMPS 推送到一座城市,从所有城市被观看。本页讲创建频道、开播、观看地址,以及修改密钥时会发生什么。

01

创建频道

频道是拥有地址的那个东西,直播是它上面的一次会话。频道只需创建一次,可以反复使用 —— 观看地址在两次直播之间不会改变。

创建一个频道
curl -X POST https://dash.yunzheng.space/api/v1/stream/streams \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sunday service"}'

应答中包含推流密钥。它只出现这一次,之后无法再读取 —— API 读不到,控制台看不到,我们也拿不到。如果丢失,只能重新签发。

字段含义
name频道名称,最多 60 个字符。
visibilityunlisted(默认)或 public。平台没有目录页,两者都只能通过地址访问。
city推流落在哪座城市。不填则由还有余量的城市接收。
placement频道如何被访问:city(该城市自己的地址,走公网)或 anycast(我们自己的网络,就近接入)。anycast 自 Pro 套餐起包含。
renditions在推送画质之下再生成几档,0 到 4。默认取套餐允许的上限;超过上限会被拒绝,而不是被悄悄下调。
unlisted 的含义是地址不可被猜到,它不是访问控制:拿到地址的人都能观看。
02

开播

任何支持 RTMP 的推流软件都只需要填两项:服务器与密钥。连接是 RTMPS,每个频道拿到的都是这一个地址:密钥就在推流指令里,不加密的连接意味着软件每次重连都把它送上线路。

服务器与密钥
Server:  rtmps://ingest-<city>.edge.yunzheng.space:1936/live
Key:     osk_…

频道在创建时选定两种地址中的一种,只选一次:

地址含义
某一座城市的
ingest-<city>.edge.yunzheng.space
城市由你选定,每次直播都落在那里;其他任何地方都不会应答这个地址。
全球唯一的
ingest.edge.yunzheng.space
从任何地方都用同一个地址。连接会落到还有余量的最近城市,不同场次可能不同。Pro 套餐起包含。
使用全球地址时,观看地址在两次直播之间依然不变。它会指向当前承载直播的那座城市,因此你发给观众的那个地址不受网络选择的影响。

两者都能在控制台以及创建频道的应答里找到。在命令行下,同样的事情是这样:

用 ffmpeg 推流
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -g 60 -c:a aac -ar 48000 \
  -f flv rtmps://ingest-<city>.edge.yunzheng.space:1936/live/$STREAM_KEY
接受的内容说明
视频H.264。
分辨率最高 2K(2560×1440)。
音频AAC。
关键帧每两秒一个或更密。分片在关键帧处切分,间隔越长,观众开播等待越久。
你推送的那一路永远原样提供给观众 —— 它不会被重新编码后再作为最高画质。renditions 增加的是它之下的档位,由我们这边生成,给连接承载不了原画的观众。
03

明文 RTMP

有些编码器与硬件根本无法建立加密连接。频道可以被切换为在 RTMPS 之外同时接受明文 RTMP —— 逐个频道设置,默认关闭,加密地址在任何情况下都照常可用。

两个服务器地址,同一个密钥
Server (RTMPS):  rtmps://ingest-<city>.edge.yunzheng.space:1936/live
Server (RTMP):   rtmp://ingest-<city>.edge.yunzheng.space:1935/live
Key:             osk_…
为某个频道打开
curl -X PATCH https://dash.yunzheng.space/api/v1/stream/streams/$ID \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"allow_rtmp":true}'

打开后,明文地址会随频道一起返回,字段是 ingest_url_rtmp,关闭时则没有这个地址。若频道没有打开该开关,却用明文推流过来,推流会被拒绝,原因是 rtmps-only,频道事后也会写明这一点。

明文地址上的密钥是不加密传输的。密钥就是推流指令里的流名,因此编码器与城市之间链路上的任何人,都能在每次连接时读到它,并一直使用到你签发新密钥为止。只在编码器确实无法使用加密地址时才打开;每一场直播都会记录它用的是两者中的哪一个。
04

延迟,以及你自己加的延迟

每个频道默认都以 HLS 能做到的最接近直播的方式分发,新建频道就是这一档。画面被切成 1 秒一片,观众大约落后你 3 到 5 秒。

设置观众看到的
low
默认
落后约 3–5 秒,分片 1 秒。前提是编码器每 1 到 2 秒一个关键帧。
standard落后约 15 秒,分片 4 秒。对关键帧间隔没有要求,网络差时也更稳。
关键帧间隔在你手里,不在我们手里。你推送的那一路原样提供给观众,因此只能在你的编码器放了关键帧的地方切分:每 8 秒一个关键帧的编码器只能切出 8 秒的分片,无论频道设成哪一档,都会落后约 15 秒。在 OBS 里这个字段是「输出 → 串流」下的关键帧间隔,设为 1 或 2 秒。

在此之上,你还可以手动把观众往后压。delay_s 是 0 到 600 的整秒数,默认 0,也就是不加任何延迟。

先拉到最接近直播,再整体延后三十秒
curl -X PATCH https://dash.yunzheng.space/api/v1/stream/streams/$ID \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"latency_mode":"low","delay_s":30}'

延迟是在分发端生效的,不在播放器里:分片其实已经写好,但播放器读到的列表在分片够「老」之前不会提到它。因此观众换一个播放器也跳不过去,也不会有人用别的方式看而抢在你前面。

录制不会跟着一起延迟。录下来的那一场从你推送的那一刻起,一秒不少。

两项都在下一场直播生效,并且都能在控制台里、服务器地址旁边改。

05

准实时播放

低延迟已经是 HLS 能做到的极限,而这个下限大约是 5 秒:播放器开播前要先攒下整片,而清单里公布最后一片时,下一片还在写。频道可以在它旁边再提供一条完全没有分片的通道 —— WebRTC,和视频通话用的是同一套 —— 在这条通道上,观众大约只落后你一秒。

为某个频道打开
curl -X PATCH https://dash.yunzheng.space/api/v1/stream/streams/$ID \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"realtime_play":true}'

频道返回时,会在 playback_url 旁边多出一个 whep_url。我们的播放器两个都要:给了前者它就先试 WebRTC,一旦出问题 —— 被拒绝、网络走不通、八秒内没有画面、连上之后又断 —— 它会自己切到 HLS 地址,不会向观众提示任何东西。HLS 那条路径从不关闭,所以并不存在「无路可退」。

你得到什么代价是什么
大约落后一秒,而且就是你自己的画面 —— 你的分辨率、你的帧率,原样转发,不经过解码。编码器必须不发 B 帧。有 B 帧时,我们只为这条通道重新编码:推流城市多占一个编码器,这些观众收到的画面也不再与你推上来的逐字节相同。开了水印的频道总会为这条通道重新编码,这样这些观众看到的画面里同样有水印。无论哪种情况,HLS 都不受影响。
B 帧,只是编码器里的一行。在 OBS 的「输出 → 串流 → 编码器设置」里,把 x264 选项填成 bf=0;或者把 tune 设为 zerolatency,效果相同,还顺便去掉了编码器自身的前瞻延迟。硬件编码器里这一项叫 B 帧 或 连续 B 帧 —— 设为 0。其余设置都不用动,同一路推流照旧服务 HLS 通道。

在两个关键帧之间打开页面的观众,要等到下一个关键帧才会出现画面 —— 最多一个关键帧间隔,因为这条通道上的关键帧是你的,观众端的任何请求都无法让它提前出现。按低延迟本来就要求的关键帧间隔(1 到 2 秒),这个等待就是一两秒;如果编码器每 8 秒才一个关键帧,就可能等满八秒,这里没有任何办法把它缩短。

人数多的场次请留在默认路径上。HLS 在每座边缘城市都有缓存,一千个观众对推流城市来说仍然是每秒一次请求。WebRTC 没有任何东西可以被边缘缓存:每一个准实时观众都是一条直连推流城市的连接,每一条都要从那台机器再送出一份。它适合「必须只落后一秒」的少数人,不适合有规模观众的直播。

这条通道上没有回看:WebRTC 传的是数据包,不是分片清单,因此进度条会隐藏,画质菜单只有一项 —— 你推上来的那一路画面,也是唯一的一路。播放器其余部分完全一样;页面若想知道最终走的是哪条通道,监听 mode 事件即可。

06

画质档位

频道提供推送时的画质,以及可选的若干更低档位。观众的播放器会挑连接能承载的那一档,并随网络变化在档位间切换 —— 对连接较差的观众来说,没有低档位的后果不是画面变小,而是卡住不动。

额外档位生成的内容
0只有推送的那一路。我们这边完全不解码。
1 … 41080p、720p、480p、360p,从高到低,且永不超过推送画质 —— 720p 的直播即使要四档,也只会得到 480p 与 360p 两档,因为放大不会凭空产生细节。

清单里会列出全部档位,由播放器自行选择;没有需要手动切换的地方,也不需要另外发一个地址。

每一个额外档位都是一路与直播并行运行的编码器,因此它被计价也被计数:带三个额外档位的频道占用城市容量的 4 份而不是 1 份。套餐给出的是每频道的上限。
07

水印

频道可以在画面的一角带上版权水印:灰色半透明的 “Orbit Stream”,下面用更小的字写着 “Powered by YunZheng.Space” 和你的 UID。默认在右下角,画面右下有内容时可以改到左上角;不打开就不会有。

打开,放在右下角
curl -X PATCH https://dash.yunzheng.space/api/v1/stream/streams/$ID \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"watermark":"br"}'

水印是在转码时烧进画面的,不是播放器画上去的。因此无论观众用什么播放,收到的画面里都有它,每一个画质档位里都有它,录制下来的那一场里也有它 —— 录像保留的就是播出时的样子。没有任何播放器开关能把它关掉。

打开水印会让最高画质那一路被重新编码。不带水印时,你推上来的那一路原样转发,完全不耗 CPU;带水印就必须解码、绘制、再编码一次。因此频道在接入城市多占用一路编码器 —— 带三个额外档位的频道会从占用 4 份容量变成 5 份 —— 观众收到的画面也不再与你推送的逐字节相同。设置在下一场直播生效。

文案与 UID 由我们生成,不可自定义:水印的作用就是标明这一场直播属于哪个账号。两个控制台都把这个选项放在服务器地址旁边。

08

节目单:没人推流时播什么

频道可以在没有人推流时播放自己的内容:循环播放若干文件、循环播放自己过往的直播,或者转播你自己的另一个频道。它不是发给每位观众各自播放的视频 —— 它与直播走同一条分发路径,因此所有打开地址的人都处在同一个位置,一小时后进来的人就是从一小时的位置开始看。

循环播放两场历史直播和一个文件
curl -X PUT https://dash.yunzheng.space/api/v1/stream/streams/$ID/programme \
  -H "Authorization: Bearer $ORBIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "enabled": true,
        "mode": "loop",
        "items": [
          { "kind": "recording", "ref": "orc_…", "title": "Last Sunday" },
          { "kind": "recording", "ref": "orc_…" },
          { "kind": "url", "ref": "https://files.example/interval.mp4" }
        ]
      }'
条目指向什么
recording本账户的某一次录制,用它的 orc_… 标识。必须已完成且保存在我们的存储中 —— 存在你自己桶里的录制通过带签名、会过期的地址回放,因此不能用在这里。
url一个 https 地址,指向 MP4 或 HLS 播放列表。明文 http 会被拒绝。
channel你自己的另一个频道,用它的 ops_… 播放标识,以本频道的地址转播出去。转播别人的频道需要对方授权,目前还没有授权的入口。
模式作用
loop按给定顺序播放,播完从头再来。最多 50 条。
relay把一路直播源以本频道的地址重新分发。只接受一条。

一旦有人向频道推流,频道立刻回到直播:节目停止,直播开始,中断不会超过一个分片的长度。推流结束后,频道会像对待任何一次掉线那样先等 15 秒 —— 以防对方马上回来 —— 然后节目继续。

频道用 on 字段报告自己处在三种状态中的哪一种:live、programme 或 off。托管页把中间这种称作「重播」而不是直播;播放器按直播处理,因为它就是一条时间线,地址始终指向它的最前沿。

节目会被重新编码为本频道的各个画质档位,因此它对城市容量的占用与该频道的一场直播相同 —— 带三个额外档位的频道无论是在被推流还是在播节目,都占用 4 份容量。从我们这里发出的流量按传输量计量,与直播一致;它不计推流分钟数,因为并没有人推流。
09

观看地址

两种形式,都来自播放标识 —— 它与推流密钥是两个不同的字符串。密钥不会出现在浏览器发起的任何请求里。

页面与清单
https://live.yunzheng.space/p/<playback id>            a page that plays it
https://live.yunzheng.space/s/<playback id>/index.m3u8  HLS, for your own player

两个地址都随频道一起返回,不需要自己拼;它们在两次直播之间都不会改变。

观众实际连到哪里,由创建频道时的 placement 决定 —— 推流与观看两端是同一个选择:

访问方式对观众意味着什么
你指定的一座城市无论观众在哪里,都连到那一座城市,走公网。一座城市、一个地址,每次都一样。
我们自己的网络观众连到离自己最近的城市,剩下的路程由网络内部承担。自 Pro 套餐起包含。
两者的缓存方式完全相同:无论观众多少,一座城市每秒只回源一次,因此 1 个观众与 1000 个观众对回源的消耗一样。两者的差别在于路径,不在于回源次数。
内容边缘保留时长
画质清单10 秒 —— 它标识本场直播,每场只变一次。
分片列表low 档 1 秒,standard 档 2 秒 —— 半个分片,因为它随直播推进而变化。
单个分片2 分钟。分片一旦写出就不再改变。
除非频道开启了录制,这里不会留存任何内容。直播结束后分片自然过期,地址随即显示为未在播;开启录制的直播另行保存,并从它自己的地址回放(见下文“录制”)。
保存在我们这里的录制,直播结束后 7 天内保持原画质,之后保留为 720p 版本。控制台里每条录制都会标明它当前是哪一种。
一路推流进来,各地分发出去一路推流进入一台 Orbit Compute 节点,除原样外再提供若干更低的清晰度,随后被各个位置各拉取一次,由它们服务附近的所有观众。推流软件Orbit ComputeRTMPS 推流,一路连接向播放器提供的清晰度原样720p480p360p最近的位置最近的位置最近的位置观众观众观众
直播只进来一次,对外提供推流时的原始画质,外加若干更低的档位。无论观众多少,每个位置每秒只拉取一次 —— 一个观众和一千个观众代价相同。
10

播放器

托管页用的播放器你也可以直接用。它是一个无依赖的脚本:绘制控件、提供画质选择、显示观众落后直播多少秒,浏览器能原生播放时走原生、不能时加载组件。

直接使用
<div id="player"></div>
<script src="https://live.yunzheng.space/v1.js"></script>
<script>
  OrbitPlayer.mount(document.getElementById("player"), {
    src: "<the channel's playback_url>",
    hlsjs: "https://live.yunzheng.space/static/player.js",
    lang: "en",
    latency: "low"   // match the channel: "low" (default) or "standard"
  });
</script>
传入 latency,与频道自身的设置保持一致。不填就是 "low",频道不改也是 "low";在低延迟频道上传 "standard" 会把短窗口白白丢掉,在标准频道上传 "low" 则会多缓冲。Safari 自己播放 HLS,没有可配置的项,这个选项在那里不起作用。

再传入 whep,播放器就会先尝试准实时播放,失败时自行退回 src。你的页面其余部分不用改:控件、事件、句柄都一样。

先走准实时,播放列表兜底
OrbitPlayer.mount(document.getElementById("player"), {
  src:  "<the channel's playback_url>",
  whep: "<the channel's whep_url>",   // near-real-time; omit it and nothing changes
  hlsjs: "https://live.yunzheng.space/static/player.js",
  latency: "low"
}).on("mode", function (m) {
  // "realtime" or "hls" - which engine ended up playing
});
准实时通道上没有回看。WebRTC 传的是数据包而不是分片清单,直播边缘之后没有窗口:进度条会隐藏,seekLive() 与后退按键不起作用,latency() 返回 null,levels() 只有你推上来的那一路画面。handle.mode() 随时给出当前使用的引擎,mode 事件在切换时触发 —— 包括准实时尝试失败、由 HLS 接手的那一刻;这是页面唯一能拿到的信号,因为对观众我们刻意什么都不说。

三种改造方式,由轻到重:

程度做法
改样式在挂载元素上设置自定义属性:--op-fg、--op-frame、--op-panel、--op-hover、--op-live、--op-warn、--op-line、--op-muted。任何文案可用 text: { live: "直播中" } 覆盖。
扩展用 controls: { stats: false, pip: false } 关掉单个控件;用 handle.addButton({ icon, aria, onClick }) 加自己的按钮,它和我们的按钮同样构造、落在同一条控件栏里;用 handle.on("state" | "levels" | "level" | "latency" | "mode", fn) 监听。
完全自建controls: false 只挂载引擎:视频元素、档位(levels()、setLevel()、currentLevel())、latency()、seekLive() 和事件,什么都不画。所有控件由你自己绘制。
画质菜单显示的是画面,不是带宽:1080p、720p 等。每一档的开销在统计面板里,给想看的人看,不会摆在正在选画质的观众面前。
按键作用
空格、K播放 / 暂停
J、左方向键往回退十(五)秒,进入直播边缘之后的窗口
L、右方向键回到直播
M静音
F全屏
上、下方向键音量

直播就是直播:只要没人往回拉,进度条一直是满的、徽标一直显示直播中,不管实测落后多少;因浏览器节流等原因自己掉队的画面会被悄悄追回 —— 低延迟频道超过 8 秒时,标准频道超过 30 秒时。传入 title 可以让锁屏显示这场直播的名字,播放、暂停和媒体键在那里都能用。

11

更换密钥

任何时候都可以签发新密钥。默认情况下,正在播出的直播会继续使用旧密钥直到结束,新密钥从下一次开播起生效 —— 因此可以在活动进行中轮换密钥而不中断播出。

签发新密钥
curl -X POST https://dash.yunzheng.space/api/v1/stream/streams/<id>/key \
  -H "Authorization: Bearer $ORBIT_TOKEN"

传入 {"revoke_now": true} 可以同时结束正在播出的直播。密钥外泄时应当使用这一种。

12

了解发生了什么

频道会显示它是否在播、从何时开始、在哪座城市。历史直播会留存记录:每一次的时间、时长、推送量与分发量,以及结束的方式。

历史直播
curl https://dash.yunzheng.space/api/v1/stream/streams/<id>/sessions \
  -H "Authorization: Bearer $ORBIT_TOKEN"

无法开播的频道会在自己身上写明原因,是一句话:套餐额度用尽,或密钥已被吊销。推流软件只会告诉你连接失败,原因在这里。

继续

相关文档

开始使用

Orbit 账户的组织方式、控制台各处的功能,以及从登录到让我们的网络开始应答一个域名的最短路径。

DNS 托管

建区、把注册商的 NS 指向我们、管理记录。含加权应答、带健康检查的记录、顶点 ALIAS 与区文件导入。

接入主机名

整域委派、单主机名委派,或加一条 CNAME 而完全不动现有 DNS。三者各自的代价与适用场景。

← 全部文档