Skip to content

Commit e5d74d3

Browse files
committed
feat(cast): DLNA/UPnP casting as first-class GSY capability (jUPnP 3.0.3)
Refactor the cast feature from a private HTTP loopback demo into a kernel-native DLNA/UPnP capability shipped with gsyVideoPlayer-java. Kernel side (gsyVideoPlayer-java/.../cast): - CastCapability facade + CastProvider/CastSession/CastListener SPI so callers depend on a stable API instead of protocol details. - CastMediaInfo carries an immutable startPositionMs so casting from a mid-playback position no longer restarts from 0 on the remote end. - DLNA provider stack (dlna/JupnpDlnaProvider, JupnpDlnaSession, DidlLite) implemented on top of jUPnP 3.0.3 support library: SetAVTransportURI -> Play -> Seek chain, 1Hz GetPositionInfo / GetTransportInfo polling, DIDL-Lite metadata, seek/pause/stop. - Registered as an optional module inside GSYVideoBaseManager so it can be enabled/disabled without touching video_layout_standard. App demo side: - SampleCastControlVideo collapses to a remote-control overlay while casting, releases the local surface, and resumes at the last remote position on disconnect. Reads local position on the main thread before releaseAllVideos and forwards it via CastMediaInfo. - CastDemoActivity + activity_cast_demo layout expose the DLNA device picker, connect/disconnect flow, and the loopback receiver toggle. - Loopback dev receiver (DevReceiverService, LoopbackDeviceFactory, LoopbackAvTransportService, LoopbackRenderingControlService) runs in a dedicated :dlna process and advertises a fake TV so the whole SOAP path can be smoke-tested on one phone. - CastReceiverFloatingWindow + CastReceiverPlayer + CastReceiverManager render the incoming stream in a system-alert floating window and expose read-only position/duration/state snapshots that the loopback AVTransport service feeds back to the sender. - DevReceiverService broadcasts READY/STOPPED/ERROR across the :dlna boundary via setPackage-scoped intents (with RECEIVER_NOT_EXPORTED on API 33+), because static listener fields are invisible across processes; CastDemoActivity registers a BroadcastReceiver in onStart/onStop to keep the toggle label in sync. Cleanup: - Delete the legacy gsyVideoPlayer-cast module, the standalone gsy-cast-receiver JavaFX project, and the private GSY-Cast v1 HTTP protocol together with their unit tests; the DLNA capability supersedes all of them. - Update settings.gradle, gradle/dependencies.gradle, and gsyVideoPlayer-java/build.gradle to wire jUPnP 3.0.3 and drop the removed modules. - Refresh doc/CAST_FEATURE_PLAN.md and doc/CAST_TEST_PLAYBOOK.md to describe the DLNA-first architecture and the loopback smoke test. All strings live in res/values{,-zh-rCN}/strings.xml, no hardcoded copy. AVTransport heavy lifting runs on a dedicated executor so the UI thread only touches surface/state changes.
1 parent 5cbab19 commit e5d74d3

45 files changed

Lines changed: 5653 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ keyid
326326
.idea/
327327
.junie/
328328

329+
# Local cast E2E debugging scratch (M4)
330+
m4-*.json
331+
m4-shots/
332+
m4-receiver.log
333+
334+
# Cast E2E scratch (uiautomator dumps, screenshots)
335+
.tmp/
336+
329337
# Secrets for local act testing
330338
.secrets
331339
*.asc

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
**openssl** | **Currently ex_so's arm64/x86_64 uses openssl 1.1.1w**
3131
**FFmpeg** | **Currently ex_so's arm64/x86_64 uses FFmpeg 4.3**
3232
**FFmpeg** | **Currently ex_so's arm64/x86_64 supports G711a(pcm_alaw)**
33+
**Cast** | **First-class DLNA/UPnP casting built on jUPnP 3.0.3; `CastCapability` / `CastProvider` / `CastSession` SPI, `SetAVTransportURI → Play → Seek` chain preserves the local position when casting mid-playback; ships with a single-device Loopback Receiver for on-device smoke tests. [Details](doc/CAST_FEATURE_PLAN.md).**
3334
**More** | **No black screen when pausing front and back switching; multi-URL quality switching; Exo HLS/DASH adaptive quality; seamless switching support; keep-last-frame demo; WebVTT progress bar preview.**
3435
**Customization** | **Customizable rendering layer, custom management layer, custom playback layer (control layer), custom cache layer.**
3536

@@ -488,6 +489,14 @@ Library APIs include `GSYVideoPreviewVttParser`, `GSYVideoPreviewProvider`, and
488489

489490
## V. Recent Versions
490491

492+
### Unreleased / feature/cast-capability
493+
494+
- Add first-class DLNA/UPnP cast capability inside `gsyVideoPlayer-java`: `CastCapability` / `CastProvider` / `CastSession` / `CastListener` SPI are the stable public contract, and the default `JupnpDlnaProvider` / `JupnpDlnaSession` implementation speaks DLNA `AVTransport:1` on top of jUPnP 3.0.3.
495+
- `CastMediaInfo` now carries an immutable `startPositionMs` field. The `SetAVTransportURI → Play → Seek` chain guarantees "casting mid-playback resumes at the same position remotely" and disconnect returns the local player to the last known remote position.
496+
- `SampleCastControlVideo` collapses into a remote-control overlay while casting — the local surface and audio are released, and a clean resume path restores local playback on disconnect. `CastDemoActivity` provides the DLNA device picker plus a Loopback Receiver toggle.
497+
- Ships an on-device Loopback Receiver (`DevReceiverService` in `:dlna` process + `LoopbackAvTransportService` + `CastReceiverFloatingWindow`) for end-to-end smoke tests without a real TV. Cross-process state is bridged with `sendBroadcast` + `setPackage` private intents.
498+
- All strings live in `res/values{,-zh-rCN}/strings.xml`. Android 13+ receivers use `RECEIVER_NOT_EXPORTED` and the notification/foreground-service permission model is honoured.
499+
491500
### v13.1.0 (2026-06-30)
492501

493502
- Publish the `gsyvideoplayer-compose` artifact and document Maven Central / GitHub Packages coordinates.

README_CN.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
**openssl** | **目前 ex_so 的 arm64/x86_64 使用 openssl 1.1.1w**
3232
**FFmpeg** | **目前 ex_so 的 arm64/x86_64 使用 FFmpeg 4.3**
3333
**FFmpeg** | **目前 ex_so 的 arm64/x86_64 支持 G711a(pcm_alaw)**
34+
**投屏** | **内核一等公民 DLNA/UPnP 投屏能力,基于 jUPnP 3.0.3;`CastCapability` / `CastProvider` / `CastSession` SPI,SetAVTransportURI → Play → Seek 支持中途投屏保留本地进度;带单机 Loopback Receiver 便于自测。[说明](doc/CAST_FEATURE_PLAN.md)**
3435
**更多** | **暂停前后台切换不黑屏;多 URL 清晰度切换;Exo HLS/DASH 自适应清晰度;无缝切换支持;完成后保留最后一帧 Demo;进度条 WebVTT 小窗口预览。**
3536
**自定义** | **可自定义渲染层、自定义管理层、自定义播放层(控制层)、自定义缓存层。**
3637

@@ -490,6 +491,14 @@ WEBVTT
490491

491492
## 五、近期版本
492493

494+
### 未发布 / feature/cast-capability
495+
496+
- 新增 DLNA/UPnP 投屏能力,作为 `gsyVideoPlayer-java` 内核一等公民:`CastCapability` / `CastProvider` / `CastSession` / `CastListener` SPI 接口稳定对外,默认实现 `JupnpDlnaProvider` / `JupnpDlnaSession` 基于 jUPnP 3.0.3 走 DLNA `AVTransport:1` 标准协议。
497+
- 投屏 `CastMediaInfo` 支持不可变 `startPositionMs` 字段,`SetAVTransportURI → Play → Seek` 链路保证"本地播到 40% → 投屏后从 40% 继续",断开后本地在最近远端位置继续。
498+
- `SampleCastControlVideo` 演示塌陷成远端遥控 overlay,投屏时释放本地 surface / audio,断开后无缝恢复本地播放;`CastDemoActivity` 提供 DLNA 设备选择 + Loopback 接收器开关。
499+
- 内置 `DevReceiverService` (`:dlna` 独立进程) + `LoopbackAvTransportService` + `CastReceiverFloatingWindow` 组成单机 Loopback Receiver,便于在没有真电视时端到端自测;跨进程状态用 `sendBroadcast` + `setPackage` 私有广播回填。
500+
- 文案全部走 `res/values{,-zh-rCN}/strings.xml`,Android 13+ 已适配 `RECEIVER_NOT_EXPORTED` / `POST_NOTIFICATIONS` 权限模型。
501+
493502
### v13.1.0 (2026-06-30)
494503

495504
- 发布 `gsyvideoplayer-compose` artifact,并补齐 Maven Central / GitHub Packages 依赖坐标说明。

app/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ android {
1111
versionCode 21
1212
versionName PROJ_VERSION
1313

14+
// M7-a-5:jUPnP 3.x 依赖 Jetty 9.4,Jetty 9.4 底层用了 MethodHandle.invoke,
15+
// 这是 VM level 能力,D8 core-library-desugaring 无法降级,官方 dex 起步就是 API 26。
16+
// 因此 :app(也只有 :app 需要 DLNA 能力)单独把 minSdk 提到 26。内核 AAR 依旧 23。
17+
minSdk 26
18+
1419
multiDexEnabled true
1520

1621
ndk {
@@ -118,10 +123,16 @@ dependencies {
118123
implementation dataDependencies.okhttpUtil
119124
implementation dataDependencies.okhttp
120125

126+
// M7-a-4: NanoHTTPD for the in-app Loopback DevReceiver's HTTP control server.
127+
implementation dataDependencies.nanohttpd
128+
121129
implementation dataDependencies.permissionsDispatcher
122130
annotationProcessor dataDependencies.permissionsDispatcherProcessor
123131

124132
debugImplementation dataDependencies.leakcanary
125133

126134
implementation 'androidx.activity:activity:1.10.1'
135+
136+
//M2d 投屏 demo 纯 JVM 单元测试(SampleCastControlVideo 的静态工具方法)
137+
testImplementation testDependencies.junit
127138
}

app/src/main/AndroidManifest.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
1111
<uses-permission android:name="android.permission.WRITE_SETTINGS"
1212
tools:ignore="ProtectedPermissions" />
13+
<!-- M7-a-3 mDNS discovery: WifiManager.MulticastLock lets us receive 224.0.0.251:5353
14+
packets on vendor ROMs (Xiaomi/Huawei/OPPO) that filter multicast in power save. -->
15+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
16+
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
17+
<!-- M7-a-5 jUPnP:MulticastSocket 长时间收 SSDP,需要 WAKE_LOCK 避免 doze 掉线;
18+
Android 12+ (API 33+) SSDP 属于 nearby wifi 用途,需要 NEARBY_WIFI_DEVICES 权限。 -->
19+
<uses-permission android:name="android.permission.WAKE_LOCK" />
20+
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
21+
android:usesPermissionFlags="neverForLocation"
22+
tools:targetApi="33" />
23+
24+
<!-- M7-a-4 in-app Loopback DevReceiver: foreground service (media playback) hosts the
25+
mDNS advertiser + NanoHTTPD control server; notification requires POST_NOTIFICATIONS
26+
on Android 13+. FOREGROUND_SERVICE_MEDIA_PLAYBACK is only enforced on targetSdk 34+
27+
but we declare it now for forward compatibility. -->
28+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
29+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
30+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1331

1432
<application
1533
android:name=".GSYApplication"
@@ -253,6 +271,33 @@
253271
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
254272
android:screenOrientation="portrait"
255273
android:theme="@style/Theme.AppCompat.Transitions" />
274+
<activity
275+
android:name=".CastDemoActivity"
276+
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
277+
android:exported="true"
278+
android:screenOrientation="portrait"
279+
android:theme="@style/Theme.AppCompat.Transitions" />
280+
281+
<!-- Loopback DevReceiver 现在渲染在应用内悬浮小窗(CastReceiverFloatingWindow)
282+
+ 独立的 CastReceiverManager IJK 实例,不再拉起独立 Activity。原先的
283+
.cast.DevReceiverPlayerActivity 已随架构重构一并删除。 -->
284+
285+
<!-- M7-a-5 主进程 DLNA 发现栈:JupnpDlnaProvider 通过 bindService 挂到这里,
286+
用 AndroidUpnpServiceConfiguration + AndroidRouter 承担 SSDP 客户端职责。
287+
与 :dlna 进程内的 UpnpServiceImpl(receiver)走不同进程/不同 socket。 -->
288+
<service
289+
android:name="org.jupnp.android.AndroidUpnpServiceImpl"
290+
android:exported="false" />
291+
292+
<!-- M7-a-5 Loopback DevReceiver:跑在独立进程 :dlna。这样主进程 sender 侧的
293+
jUPnP 发现栈 + 本地 receiver 端 jUPnP 栈各占各的 UDP socket / Registry,
294+
天然避免 M-SEARCH 自己看不到自己的坑。
295+
foregroundServiceType=mediaPlayback 匹配 Android 14+ allowlist 要求。 -->
296+
<service
297+
android:name=".cast.DevReceiverService"
298+
android:exported="false"
299+
android:foregroundServiceType="mediaPlayback"
300+
android:process=":dlna" />
256301
<activity
257302
android:name=".compose.ComposeDemoListActivity"
258303
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

0 commit comments

Comments
 (0)