立项日期:2026-07-01 分支:
feature/cast-capability基线:master @v13.1.0(commit5cbab191) 关联文档:CAST_RECEIVER_DESIGN.md · CAST_TEST_PLAYBOOK.md · ARCHITECTURE.md
本次投屏能力的 零号约束:不得破坏 GSYVideoPlayer 已有任何能力。
具体展开为:
- 不改动播放内核:
IJK / EXO(Media3) / System三大内核代码零改动,投屏能力通过 内核 SPI(CastCapability / CastProvider / CastSession) 挂接,UI overlay + 管理层与主播放器解耦。 - 不动
gsyVideoPlayer-java公共 API:GSYBaseVideoPlayer、StandardGSYVideoPlayer、GSYVideoControlView、GSYVideoManager等对外 API 只允许新增方法,不改签名、不改语义、不改默认行为。SPI 接口以新增形式引入。 - 不改 Demo 默认行为:
MainActivity现有 39 个入口按钮的顺序/文本/回调保持不变;投屏 Demo 作为新按钮追加到列表末尾。 - 不新增独立发布 module:投屏能力(SPI + jUPnP DLNA/UPnP 实现)并入
gsyVideoPlayer-java(或作为可选源集),settings.gradle不追加新 include;主发布链com.shuyu:gsyvideoplayer-java不受影响。 - 不改测试基线:J 轮基线的 4 项基础能力(播放/暂停、拖动进度、全屏切换、切换内核)在
DetailPlayer上 100% 通过率必须保持;java_cold_smoke.sh的 38/39 (97.4%) 通过率不允许下滑。 - 不引入必选新依赖到主 AAR:投屏依赖
jUPnP 3.0.3(DLNA/UPnP)以可选方式引入;三方厂商 SDK(乐播等)保持 flavor / 独立 aar 分发。
每一次 PR 合入前,必须回归下列 4 个断言:
./gradlew assembleDebug全绿./gradlew :gsyVideoPlayer-java:test全绿bash doc/test_scripts/java_basic_regression.sh "Detail模式"全绿bash doc/test_scripts/java_cold_smoke.sh≥ 38/39
在 GSYVideoPlayer 上补齐"投屏"能力,覆盖 5 个可断言的能力点:
| 编号 | 能力 | 判据 |
|---|---|---|
| Cast-A | 设备发现 | 5s 内出现至少 1 台设备,onDeviceFound 日志出现 |
| Cast-B | 连接投屏 | onCastConnecting → onCastConnected,耗时 < 5s |
| Cast-C | 远端起播 + 本地暂停 | 远端 PLAYING + 本地 changeUiToPauseShow |
| Cast-D | 远端 seek 回传 | onCastSeek complete,UI 进度差 ≤ 2s |
| Cast-E | 退投恢复本地 | 断开后 onSeekComplete 到远端最后 position,本地恢复 CURRENT_STATE_PLAYING |
对齐 ARCHITECTURE.md 中的原则 "播放内核只负责媒体解码和时间线,跨内核能力尽量放在播放器 UI 或 GSY 管理层"。
┌─────────────────────────────────────────────────────────────┐
│ Demo 层 (app/) │
│ ├─ MainActivity 新增按钮:"投屏DEMO"(追加,不改现有) │
│ ├─ CastDemoActivity │
│ └─ SampleCastControlVideo extends StandardGSYVideoPlayer │
├─────────────────────────────────────────────────────────────┤
│ UI overlay 层 │
│ ├─ CastButton (video_layout_standard.xml 追加,非必现) │
│ └─ CastDeviceListDialog │
├─────────────────────────────────────────────────────────────┤
│ 内核 SPI 层(并入 gsyVideoPlayer-java) │
│ ├─ CastCapability ── 能力声明(是否支持 DLNA/其他协议)│
│ ├─ CastProvider ── 发现 / 建连 / 会话工厂 │
│ ├─ CastSession ── 单次投屏会话:load/play/pause/ │
│ │ stop/seek/status/事件回调 │
│ ├─ 缺省实现:jUPnP 3.0.3 DLNA/UPnP │
│ │ ├─ DlnaCastProvider ── Registry + Discovery │
│ │ └─ DlnaCastSession ── AVTransport1 action 封装 │
│ └─ 三方厂商实现(Chromecast / 乐播 等)走同一 SPI 挂接 │
├─────────────────────────────────────────────────────────────┤
│ 本地 loopback 接收端(Demo 层,用于单机自投自收验收) │
│ DevReceiverService(:dlna) 前台 Service 内 │
│ ├─ jUPnP LocalDevice(MediaRenderer:1) │
│ └─ LoopbackAvTransportService │
│ 通过 jUPnP UDA-SSDP 在本机注册 & 被同 App 内的 sender 发现 │
└─────────────────────────────────────────────────────────────┘
关键设计:SPI(CastCapability/CastProvider/CastSession)与 GSYVideoManager 解耦,通过事件回调对接 Demo;本地播放器不感知远端存在,只按 Demo 层调度进入"暂停 + 覆盖 CastOverlay"状态。本地验收(同机自投自收)走 DevReceiverService(:dlna) 里托管的 jUPnP LocalDevice + LoopbackAvTransportService,与线上真投屏走完全相同的 DLNA/UPnP 协议,仅设备位置不同。
投屏能力(SPI + jUPnP DLNA/UPnP 缺省实现)并入 gsyVideoPlayer-java,不再走"新增独立 module"路线;settings.gradle 无 include 追加。三方厂商 SDK(乐播等闭源)仍走 flavor / 独立 aar 分发。
gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/
├── CastCapability.java (SPI:能力声明)
├── CastProvider.java (SPI:发现 / 建连 / 会话工厂)
├── CastSession.java (SPI:单次会话生命周期与命令面)
├── CastState.java (枚举:IDLE/CONNECTING/PLAYING/PAUSED/...)
├── CastListener.java (事件回调)
├── model/
│ ├── CastDevice.java
│ └── CastMediaInfo.java
└── dlna/ (缺省实现:jUPnP 3.0.3)
├── DlnaCastProvider.java
└── DlnaCastSession.java (封装 AVTransport1 action)
注:jUPnP 依赖由该 flavor / 可选源集引入,不进入基础 AAR classpath,不影响下游只用本地播放能力的 App。
用于同机自投自收验收(同时也是最直白的 DLNA renderer 参考实现):
app/src/main/java/com/example/gsyvideoplayer/cast/
└── DevReceiverService.java (前台 Service, foregroundServiceType=mediaPlayback)
├─ jUPnP UpnpService(Registry + StreamServer)
├─ LocalDevice: urn:schemas-upnp-org:device:MediaRenderer:1
└─ LoopbackAvTransportService(AVTransport1 服务实现,回调驱动本地 IJK/EXO 起播)
| M | 内容 | 交付物 | 回归动作 |
|---|---|---|---|
| M0(本轮) | 分支 + 3 份规划文档 + ARCHITECTURE 追加 | 本 PR | 无源码改动,仅 doc/ 增加 |
| M1 | 在 gsyVideoPlayer-java 内新增 cast 目录 + SPI 空骨架(CastCapability / CastProvider / CastSession),app 不新增依赖 |
空实现可编译 | 跑 basic_regression + cold_smoke,确认无回归 |
| M2 | jUPnP 3.0.3 DLNA/UPnP 缺省实现:设备发现(SSDP + Registry Listener)+ AVTransport1 起播 / 暂停 / Seek / Stop;App 层 DevReceiverService(:dlna) 前台 Service 起 jUPnP LocalDevice + LoopbackAvTransportService,用于单机自投自收 |
Demo 中新增"投屏DEMO"入口,与本机 loopback receiver 起播通链 | Cast-A ~ Cast-E 全绿 + 原有能力零回归 |
| M3 | 位置同步、退投恢复、异常回退(onCastError) |
SampleCastControlVideo + CastOverlay 上线 |
端到端回归 |
| M4 | 扩展:Chromecast provider、字幕投屏、Header 透传、乐播 SDK 挂载点 | 各 provider 各自实现 SPI,独立可选 | 各 provider 独立回归 |
每个 M 单独提交 PR,PR 合入前必跑第 0 节 4 项断言。
| 已有能力 | 影响 | 缓解 |
|---|---|---|
| MainActivity.java 39 个入口 | 追加 1 个按钮 | 追加到 layout 末尾,不改 id/顺序;java_cold_smoke.sh 更新为 40/40 |
| StandardGSYVideoPlayer.java | 零改动 | 投屏按钮/UI 走 Demo 子类 SampleCastControlVideo,不修改父类 |
| video_layout_standard.xml | 零改动 | 投屏按钮放 Demo 层 layout:sample_cast_video.xml |
| PlayerFactory 三内核切换 | 零改动 | 投屏通过 SPI 与内核解耦 |
| ARCHITECTURE.md "近期播放能力补充"表 | 追加 1 行 | 见 §6 |
| gradle/dependencies.gradle | 追加 jUPnP 3.0.3 可选依赖 |
只在启用 cast 源集时生效,不进入基础 AAR classpath |
| publish.gradle 发布链 | 零改动 | 投屏并入 gsyVideoPlayer-java,不新增发布 artifact |
将在 ARCHITECTURE.md "近期播放能力补充"表格末尾追加:
| 能力 | 所在层级 | 设计说明 |
|---|---|---|
| 投屏(DLNA / Chromecast) | UI overlay + 内核 SPI(CastCapability / CastProvider / CastSession) | 内核只负责本地时间线;投屏走 jUPnP 3.0.3 DLNA/UPnP 缺省实现(AVTransport1),第三方厂商实现(Chromecast/乐播)通过同一 SPI 挂接;投屏中本地暂停并保留 UI,退投时按远端最后 position 恢复本地播放。 |
| 风险 | 影响 | 应对 |
|---|---|---|
| jUPnP SSDP 组播权限被路由器屏蔽 | 发现不到设备 | 提供"手动输入 IP"回退入口;WifiManager.MulticastLock 全程保持 |
| 本地文件投屏需 HTTP 化 | 安全 / 端口冲突 | jUPnP 内建 StreamServer 承担 URI 服务,随机端口,退投立即关闭 |
| 电视端 Header/DRM 兼容差 | 部分源无法投 | 白名单降级:会话回调 onCastError → 回退本地播放 |
| jUPnP 依赖体积增量 | AAR 变大 | 只在启用 cast 源集时打入;下游不用投屏时零增量 |
| 三方 SDK license | 主库污染 | Chromecast / 乐播作为 flavor / 独立 aar 分发,实现 SPI 挂接 |
| Compose 端集成 | 生命周期泄漏 | 复用 LifecycleBridge.kt 现有机制 |
- 落地本文件 +
CAST_RECEIVER_DESIGN.md+CAST_TEST_PLAYBOOK.md(M0) - 在 ARCHITECTURE.md 追加投屏一行
- 打 M0 PR,通过第 0 节 4 项回归后进入 M1
| 日期 | 版本 | 内容 |
|---|---|---|
| 2026-07-01 | v0.1 | 立项,M0 规划文档落地 |
| 2026-07-01 ~ 2026-07-02 | v0.2 ~ v0.13 | 历史迭代(已废弃架构):早期分支曾探索"独立发布 module + 自研 HTTP/JSON 私协议 + 桌面 JVM 接收器 + 单机自研 mDNS 广播 + adb loopback 兜底"路线,历时 12 版迭代,其间涉及的独立 module、私协议控制服务器、私自定义 mDNS 广告/解析组件、单机 loopback 通知栏命名等均已在 v0.14 整体下线,仅保留能力目标(Cast-A |
| 2026-07-02 | v0.14 | 架构重定向到 jUPnP 3.0.3 DLNA/UPnP + 内核 SPI:(1)取消独立发布 module,投屏能力(CastCapability / CastProvider / CastSession 三件 SPI + dlna/ 缺省实现)并入 gsyVideoPlayer-java;(2)线上真投屏与本地验收统一走 DLNA/UPnP 标准协议——DlnaCastProvider 通过 jUPnP Registry + RegistryListener 做 SSDP 发现,DlnaCastSession 封装 AVTransport:1 action(SetAVTransportURI / Play / Pause / Stop / Seek / GetPositionInfo);(3)单机自投自收验收工具重构为 DevReceiverService(:dlna):前台 Service (foregroundServiceType=mediaPlayback) 内起 jUPnP UpnpService,注册 LocalDevice: urn:schemas-upnp-org:device:MediaRenderer:1 + LoopbackAvTransportService(AVTransport1 Service 实现,action 回调驱动本机播放器),设备名按 jUPnP UDN 规范生成;(4)废弃全部自研 HTTP/JSON 端点、自研 mDNS 广告/解析组件、adb reverse 兜底路径;(5)第三方厂商(Chromecast / 乐播)实现同一 SPI,作为可选 flavor / 独立 aar 挂接。 |
原文档 §10(GSY-Cast v1 自研 HTTP/JSON 协议规格)、§11(M4 现状盘点)、§12(M5-a JavaFX Backend)、§13(M5-b 竞态修复 + UI 反向桥)、§14(M6 UX 补齐三件套:Disconnect / Polling / Lifecycle Gate)均描述的是 v0.2~v0.13 期间的废弃架构(自研私协议控制端点、桌面 JVM 接收器、单机自研 mDNS 广告与解析组件、adb loopback 兜底、HeadlessPlayerBackend / JavaFxPlayerBackend 等),与当前 v0.14 落地的 jUPnP 3.0.3 DLNA/UPnP + 内核 SPI (CastCapability/CastProvider/CastSession) + DevReceiverService(:dlna) 架构不再吻合,故在本次整理时整体下线以避免误导后续实现者。
如需追溯历史决策(例如为何一度放弃 UPnP 走自研协议、又为何回归 DLNA),请查阅 git 历史中 v0.2v0.13 对应的 commit。当前落地方向以本文件 §0§9 + CAST_TEST_PLAYBOOK.md 为准。