本文主要从架构边界角度梳理一类自托管媒体库(以 Octans 播放子系统为例):播放为何从 Catalog / 任务系统里拆出来,服务端与客户端各管什么,以及 PlaybackSession、Planner、DirectPlay / HLS 交付如何构成当前 Web 主链。重点是职责与名词,而不是接口字段大全。

细节决策见 播放能力 Planner,运行时排障见 播放排障指南,转码二进制与能力探测见 FFmpeg 工具链。本文只回答:模块怎么切、主链怎么走、什么算正式范围。

播放子系统为什么要独立

「点播放」看起来像详情页的一个按钮,实际会同时牵涉:

关切为什么不能塞进纯查询或批处理
能否播 / 如何播依赖客户端能力、媒体事实与策略的交叉决策
受控下发短期 token、Range、HLS window,不能暴露裸静态路径
运行时转封装 / 转码进程、Runtime Slot、缓存与清理
状态回收进度、续播、逻辑会话生命周期、异常退出兜底

因此播放应形成独立业务域(概念上即 Modules/Playback 一类模块),读取 Catalog 已沉淀的媒体事实,但把协商与流交付塞进海报墙 / 详情查询;把会话与 HLS 运行时塞进扫描 / 刮削类 Operations;把播放实时心跳直接复用任务 TaskHub。

一条已经能闭环的 Web 主链可以概括为:

MediaFile
  → 权限校验
  → PlaybackPlanner
  → PlaybackSession
  → 受控 HTTP Range 流 / HLS window
  → Web <video> / hls.js
  → Progress 上报
  → ResumePosition

核心原则

服务端决策,客户端执行

负责
服务端解析目标媒体文件、读权限、兼容性判断、选择播放与交付方式、生成受控流地址、进度与续播回收;后续转封装 / 转码 / 缓存 / 调度的最终决策也落在服务端
客户端请求会话、按返回的播放计划初始化播放器、交互控制、展示不可播原因、上报进度;原生端再叠加端侧解码、字幕渲染与播放器切换

客户端不应本地重算应由服务端给出的 HLS window 生命周期、stream token 绑定关系或字幕 delay 等控制面字段;UI 只消费决策结果。

直放优先,最小服务端处理

播放方式(PlayMethod)优先级保持:

DirectPlay → DirectStream → Transcode → Unsupported

能由客户端直接播放时,服务端只做受控 I/O。只有容器、音视频、字幕或网络条件确实要求时,才逐步进入转封装、转码或明确 Unsupported。

播放方式 ≠ 交付方式

这两层必须拆开,否则「走了 HLS」会被误读成「一定转码了」。

概念问的是什么常见取值
PlayMethod服务端是否改变媒体内容DirectPlay / DirectStream / Transcode / Unsupported
Delivery客户端如何拿到流HttpFile / Hls(未来还可扩展 Dash、NativePath 等)

当前 Web 主链上的典型组合:

DirectPlay   + HttpFile  → 原始文件受控 Range,不启 FFmpeg
DirectStream + Hls       → 视频 copy,容器转封装 / 必要音频 AAC
Transcode    + Hls       → 视频或完整主轨重新编码后输出 HLS

HLS 可以承载 DirectStream,也可以承载 Transcode;NativePath 只适合未来原生客户端本地路径场景,不适合 Web。

不把未落地方案写成事实

正式范围应限于代码与契约已接线的能力。ABR / 多 rendition、跨会话 Variant Cache、多端同步、服务端主动 force stop、Android / Apple 完整原生实现等,属于后续方向,进入实现后再写进「当前事实」。

决策模型(四档 PlayMethod)

DirectPlay

服务端不改变媒体内容,只把原始文件以受控方式交付。

适用直觉:用户有读权限、文件可访问,且客户端声明的容器 / 视频 / 音频能力覆盖源事实;色彩安全边界(如明确 HDR / DV / HLG / BT.2020 / 10-bit 风险)也允许直放。字幕路线由后端根据选轨事实决定(关闭、WebVTT 资源、native sidecar、ASS / bitmap overlay 等),不由前端指定「服务端画面合成」作为普通 Web 主线。

DirectStream

服务端只做容器转封装或必要音频转码,主视频保持 copy。

典型场景:视频编码浏览器可解但容器不兼容;视频可 copy 但选中音轨需转 AAC;需要 HLS 交付但不需要重编画面。

Transcode

服务端对视频或完整主轨重新编码

典型场景:客户端不支持源编码;需要降码率;字幕必须烧录;设备能力不足需服务端降级。实现上通常还牵涉 FFmpeg capability graph、软件 / VAAPI / OpenCL 路径与 tone mapping——细节见工具链与硬转码专题,不在此展开命令构造。

Unsupported

当前无法为该客户端生成可执行计划,且必须带明确原因(容器 / 轨组合无路、工具链或硬件能力不足、能力上报与媒体事实不匹配、物理文件不可访问等)。

注意:用户无权限、资源不可见或媒体记录对当前身份不可达,一般按资源授权口径隐藏为不可见(如 404),应伪装成「兼容性 Unsupported」。正式错误码以目标环境 API 契约为准。

PlaybackSession:控制面中枢

可以把一次用户点击播放理解为创建逻辑会话,而不是「直接塞一个文件 URL」。

会话层通常承接:

能力说明
协商带上客户端类型 / 能力与选轨意图,换回 method、delivery、流地址或 HLS 入口
生命周期pause / resume / seek / rebase / stop,以及异常退出后的 TTL 清理
进度session-scoped 进度上报与 ResumePosition
与 HLS 解耦逻辑会话 ID ≠ HLS window ID;rebase 或 grace miss 后会话可不变、window 可换新

实时侧若有 PlaybackHub 一类通道,当前更适合做 logical session heartbeat(页面隐藏、前台恢复、缓冲、reconnect 刷新 activity),而不是把多端同步或远程遥控一次做满。REST 进度仍负责持久观看位置;logical cleanup 仍是异常退出后的最终释放兜底。

排查时务必先分清「逻辑会话」与「HLS window」——见排障文的两个易混 ID。

Planner:如何从事实走到计划

Planner 不是「codec 白名单查表」,而是多维约束求解。产品视角可先收成三轴:

ClientCapabilities  ×  MediaFacts  ×  Policy
        →  PlaybackPlan
           (method + delivery + 代价与否决原因)
问的是什么
客户端能力端上声明能承接什么(平台 / 引擎、容器与 codec、MSE / native HLS、字幕路线、是否支持 HttpFile 等)
媒体事实源片实际是什么(容器、profile/level、动态范围、音轨、字幕形态等)
策略允许与偏好什么,以及服务端现在能不能做(质量 / 省电、LAN/WAN、FFmpeg / Slot / GPU / 磁盘、保守兜底)

输出除 method / delivery 外,还应让控制面能解释代价:是否 tone mapping、是否 downmix、字幕路线、是否降档、是否等 Slot 等,避免用户只看到「转码中」而无归因。

更细的输入块拆分、DirectStream 边界与否决原则,见 播放能力 Planner;runtime 已接线的输入×输出真相表读法,见转码支持矩阵相关文。

交付:HttpFile 与 HLS

DirectPlay + HttpFile

  • 受控文件流 + HTTP Range。
  • 数据面常用短期签名 token(浏览器原生 <video> 难以稳定带自定义 Authorization 头)。
  • Token 应绑定用户、会话、认证版本、媒体文件与文件版本,并在 stream 端点复核会话 / 账号 / 文件存在性与读权限。
  • 典型错误语义(公开口径):token 非法或会话失效 → 拒绝;过期 → 需刷新;资源不可见或文件不可访问 → 不可见。具体状态码以部署环境 API 为准。

DirectStream / Transcode + Hls

  • 交付 playlist、segment,以及可选字幕分段。
  • 运行时牵涉 HLS window、session cache、runtime pause、Runtime Slot 并发治理。
  • 前端需处理 fatal、token refresh、window stale、segment missing 等恢复路径(rebase / 重建 window)。
  • DirectPlay 不启 FFmpeg 是正常现象;HLS 路径则依赖专用工具链能力图是否完整——见 FFmpeg 工具链

进度与续播

  • Progress 上报绑定逻辑会话,写入持久 ResumePosition。
  • 详情页 / 继续观看只消费位置,不负责重新决策 PlayMethod。

模块边界(与周边域的关系)

周边域关系
Catalog提供媒体事实与详情入口;播放协商与流地址不走详情查询接口
Operations扫描 / 刮削 / 删除等后台动作;播放运行时即使内部有队列,也属于播放域内部,不塞进 actions
System / 任务系统任务与 TaskHub 不做播放控制面;播放实时域独立演进
Auth / 资源授权会话与流地址都不能绕过库读权限;受控 token ≠ 裸静态路径
Metadata可读技术轨信息;播放决策不写回 Metadata 模块

概念上的播放模块职责清单(公开层):播放协商与 options 预检、计划生成、HttpFile / HLS 输出、FFmpeg 能力图与转码调度、fallback replan、进度与续播、Runtime Slot 与 heartbeat,以及后续缓存与多端同步的扩展位。

客户端边界

当前 Web 正式范围(概念)

后端侧:会话协商、权限、Web 基线兼容、DirectPlay HttpFile、DirectStream / Transcode HLS、受控流与 HLS 输出、Range、pause/resume/seek/rebase/stop、进度与续播、heartbeat 与 TTL cleanup、明确失败原因。

前端侧:播放 API 调用、选轨 / options UI、独立播放页、原生 <video> + hls.js、HLS 恢复路径、错误提示、session 进度、heartbeat、续播提示。

播放入口可以从详情页「自然生长」,但应落到独立播放壳,而不是把会话生命周期散落在组件库里。

客户端类型与扩展

契约侧通常先承诺有限 clientKind(如 Web / Windows),能力模型再向多端协商演进。原生 Android / Apple / Desktop / TV 的双核播放器、端侧字幕、SMB/NFS、AFR、HDR passthrough 等,应在对应工程启动后再写入正式范围,而不是提前当作已交付事实。

跨端 UI 与 native 播放的分工,可另见系列中的跨端与 Windows / Android 专题。

当前正式范围 vs 后续

已在主链(概念已落地)后续方向(勿写成已交付)
DirectPlay + HttpFileABR / 多 rendition
DirectStream + Hls跨会话 Variant Cache
Transcode + Hls(含软硬解与 tone mapping 主线)多端同步 / 远程控制 / 服务端主动 stale 推送
options、fallback、逻辑会话、进度与续播Android / Apple 完整原生实现
Runtime Slot、PlaybackHub heartbeat外部播放器深度集成

推进 Variant Cache 或 ABR 前,需先想清:跨播放复用边界、cache key 与权限复核、源失效与失败 session 不入 cache、对 Slot / 磁盘的压力、用户可见错误与运维排查方式。

怎么读这一系列

建议阅读顺序(从总览到落地):

1. 本文:架构边界与主链名词
2. 播放能力 Planner:决策输入与四档边界
3. 转码支持矩阵读法 / 硬转码与 Tone Mapping:Transcode 内如何选型
4. FFmpeg 工具链:二进制与能力探测
5. 播放排障:会话 / window / token / 字幕 / 残留如何定位

一句话分工:

文档回答
本文模块怎么切、主链怎么串、正式范围在哪
Planner为什么选这条 method / delivery
工具链服务端有没有能力把计划跑出来
排障计划对了但播不了时落在哪一层

小结

  • 播放是独立业务域:权限 + 决策 + 受控交付 + 状态回收,不是详情查询附件,也不是扫描任务变体。
  • 服务端决策,客户端执行;直放优先;PlayMethod 与 Delivery 正交
  • 主链骨架:MediaFile → Planner → Session → HttpFile / HLS → 客户端 → Progress / Resume
  • 逻辑会话控制面与 HLS window 数据面必须分开理解。
  • 公开文只写已接线主链;ABR、跨会话缓存、多端同步与未启动的原生客户端保持后续位。

相关阅读

同系列还可对照:

来源

本文改写自 Octans 项目文档中的播放架构说明(octans-docs arch 层)。正文收敛为公开架构总览与职责边界,不展开完整 API 字段、控制器方法、数据库表或前端组件实现;接口契约与运行时细节以目标环境当前 API / 实现文档为准。