Skip to content

feat(lan): 局域网一起听(BeatSync 同步调度 + 本地文件共享) - #632

Open
Lollipop2233 wants to merge 17 commits into
cwuom:masterfrom
Lollipop2233:codex/lan-beatsync-restart
Open

Lollipop2233 wants to merge 17 commits into
cwuom:masterfrom
Lollipop2233:codex/lan-beatsync-restart

Conversation

@Lollipop2233

@Lollipop2233 Lollipop2233 commented Oct 31, 2026 •

Copy link
Copy Markdown

描述 / Description

在 modules/ltwlan 与 :playback:runtime 的 core.player.lan 包中实现「局域网一起听」(issue #540 的延续与重构),核心是一套 Web Audio 播放架构 + BeatSync 调度语义:

播放架构(与既有 ExoPlayer 云端/本地播放的关系)

  • 播放本体走 WebView Web Audio:局域网会话期间,各设备在 WebView 中用 decodeAudioData 将整曲解码为 PCM,用 AudioContext 硬件时钟外推播放位置,采样级 source.start(when) 定点起播——获得无跳变、无漂移累积的同步基准(对拍 BeatSync 源码语义)。
  • 控制信道:Java-WebSocket 1.6.0 承担房间管理、NTP 式多轮对时采样、播放指令(PLAY/PAUSE/SEEK/SYNC)。
  • 漂移看门狗:偏差 >150ms 才 seek 重排,禁止变速校正;装载语义为 prepare → 暂停等待 → SYNC 确认后才起播。
  • 文件传输:独立 NanoHTTPD HTTP 服务(与控制信道分离),成员凭据鉴权;主控本地歌曲经 LanFileSnapshot 边复制边算 SHA-256 生成不可变快照,副设备完整下载校验后进入解码与同步流程。
  • 封面/歌词共享:随歌曲下发,副设备复用原版歌词解析与展示组件(逐字时序、翻译、音译、偏移均保留),避免副设备重新匹配出不同结果。
  • 限制:音频文件 200 MiB、解码后 PCM 512 MiB,含精确边界测试;超限明确提示原因。

与 PlayerManager 的边界

:playback:runtime 通过 LanPlaybackBinding 门控接管:仅局域网会话激活时命令路由到 LAN 引擎、PlaybackAudioInfo 由 LanPlayerAdapter 写入(保证播放页编码/码率徽章正常显示);非会话时所有钩子直接短路,播放行为与原实现完全一致。播放页显示组件、PlaybackAudioInfo 模型与音频信息提取管线零改动。

入口与 UI

播放页「更多」菜单中与云端「一起听」并列;局域网会话期间播放页可直接打开房间菜单;局域网房间页与云端一起听页同样禁用面板滑动手势(本 PR 已与上游 isMetadataApplicationBusy 条目合并为并集条件)。

上游合并说明

已合入上游 master 51f0100c(14 个提交)。冲突 6 处均已按机械方式解决:

  • NowPlayingSecondaryActions.kt:手势条件取并集(LAN 页面集合 + 上游 isMetadataApplicationBusy);
  • PlayerManagerPlaybackExtensions.kt:LAN 路由门与上游 serverRecoveryPosition = null 均保留;
  • 4 处文档(CONTRIBUTING* / docs/implementation*):采用上游表格结构,补回 :ltwlan 条目、库数 14→15。

因合并叠加使 PlayerManagerPlaybackExtensions.kt(2004 行)与 PlayerManagerPersistenceExtensions.kt(2002 行)超出仓库 2000 行门禁,按门禁意图做纯搬移拆分(无逻辑改动):seek 簇 → PlayerManagerSeekExtensions.kt,歌词获取簇 → PlayerManagerLyricsExtensions.kt。

质量门禁与测试

  • verifyModularization(含模块归属/依赖边界/文件行数门禁)、:ltwlan:check、assembleDebug 通过;
  • LAN 单元测试(快照/传输/鉴权/边界/歌词转换等)与 BeatSync 脚本测试通过;
  • 双机真机验证:本地文件共享播放、SHA-256 一致、封面/歌词一致、seek/暂停/恢复、退出缓存清理均通过。

类型 / Type

  • 新功能 / New Feature

修复或解决的问题 / Issues Fixed or Closed by This PR

Closes #540

清单 / Checklist

  • 我已阅读并遵循贡献指南 / I have read and followed the contribution guidelines
  • 我已在本地测试这些更改 / I have tested these changes locally
  • 我已更新相关文档或注释(如适用) / I have updated relevant documentation or comments (if applicable)
  • 我确认本次更改没有破坏任何原有逻辑 / I confirm this change does not break existing behavior
  • 我确认本次更改保持向下兼容(如适用) / I confirm this change remains backward compatible (if applicable)
  • 我已运行必要的构建、测试或静态检查 / I have run the required build, tests, or static checks
  • 我确认没有提交密钥、Token 或其他敏感信息 / I confirm no secrets, tokens, or other sensitive information were committed
  • 涉及 UI 变更时,我已补充截图或录屏(如适用) / I have added screenshots or recordings for UI changes (if applicable)

其他信息 / Additional Information

  • 双机验收的边缘场景收尾(不同元数据来源连续切换等)仍在进行中,核心链路已验证。
  • tools_pub/beatsync-prototype 为同步语义验证原型,docs/lan-listen-together-beatsync-design.md 为设计文档,供审查参考。

UI 截图 / Screenshots

入口(与云端一起听并列) 局域网面板
入口 面板
主控房间页 设备音量管理
主控房间页 设备音量
听众房间页(已连接 · RTT) 播放页直达房间菜单
听众房间 直达菜单

双机同步播放(同秒截屏,进度一致):

主机 00:32 副机 00:32
主机 副机

局域网一起听

  • 支持创建房间、自动发现和手动加入。房主可设置成员的切歌、播放/暂停和 seek 权限。每台设备可独立调节本机音量。
  • 使用时钟同步和定时调度协调播放。所有成员完成音源加载后,系统再安排播放。
  • 支持共享本地音频,并在接收后校验文件摘要。音频文件上限为 200 MiB,解码 PCM 上限为 512 MiB。封面、歌词、翻译和音译也可共享。
  • 在播放页和设置页提供入口。局域网会话期间,播放控件按权限启用或禁用;局域网曲目不写入播放状态持久化。

测试

  • 新增房间策略、协议、传输、本地媒体、播放调度和权限等测试,也新增共享歌词、播放控件及设置搜索测试。
  • 新增 Android 会话仪器测试。
  • 测试运行结果无法从变更摘要确认。

Preserve host snapshot reads, authenticated local media transfer, shared artwork and lyrics, 200 MiB encoded / 512 MiB PCM limits, LAN queue selection fixes, and compact listening management UI. Validation: 58 LAN, 17 playback and 13 app focused tests; 19 audio script checks; LAN CRAP, lint, dependency and module boundary checks. Original-file direct-read optimization is not included.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-11-03T01:34:31.841925Z
We couldn’t complete this review. Comment @codex review to try again.
25efffc Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 46fbf4b and d2bd302.

Files ignored due to path filters: 17
  • CONTRIBUTING.md is excluded by !**/*.md
  • CONTRIBUTING_EN.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • README_EN.md is excluded by !**/*.md
  • THIRD_PARTY_LICENSES.md is excluded by !**/*.md
  • docs/implementation.md is excluded by !**/*.md
  • docs/implementation_EN.md is excluded by !**/*.md
  • docs/lan-listen-together-component-research.md is excluded by !**/*.md
  • docs/lan-listen-together-implementation-validation.md is excluded by !**/*.md
  • docs/lan-listen-together-prototype-results.md is excluded by !**/*.md
  • docs/lan-local-audio-preparation.md is excluded by !**/*.md
  • docs/lan-local-audio-validation.md is excluded by !**/*.md
  • docs/lan-web-audio-sharing-validation.md is excluded by !**/*.md
  • docs/player-buffer-research.md is excluded by !**/*.md
  • modules/ltwlan/README.md is excluded by !**/*.md
  • tools_pub/beatsync-prototype/README.md is excluded by !**/*.md
  • tools_pub/beatsync-prototype/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
Files selected for processing: 14
  • app/src/main/java/moe/ouom/neriplayer/core/di/AppContainer.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/NeriMiniPlayer.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/navigation/AppNavigationScaffold.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/lyrics/LyricsScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/NowPlayingScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/NowPlayingSecondaryActions.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/actions/MoreOptionsMainContent.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/lyrics/NowPlayingLyricsLoadOwner.kt
  • config/quality/domain-dependencies.json
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/PlayerManager.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerLyricsExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerPersistenceExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlayerManagerPlaybackExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlayerManagerSeekExtensions.kt

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Walkthrough

新增局域网共同聆听功能,包含房间发现、权限控制、同步播放、本地媒体共享和歌词展示。播放器命令及界面会根据 LAN 会话状态和权限调整。新增 LAN 模块、音频引擎、媒体传输、测试和调试入口。另新增 BeatSync 音频与时钟同步原型、跨运行时验证工具、构建脚本及组件研究资料。

Merge Risk: Moderate · previous review d2bd3

LAN sessions may fail to connect, remain active after setup times out, or end unexpectedly. LAN audio may also continue through the speaker after USB output disconnects. Resolve these risks before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 1.65% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed 标题“feat(lan): 局域网一起听(BeatSync 同步调度 + 本地文件共享)”概括了本 PR 的主要变更:新增局域网一起听功能,包括 BeatSync 同步调度与本地文件共享。变更摘要与 PR 目标均与此标题一致。标题长度适中,未包含文件列表或表情符号。
Linked Issues check Passed 针对直接关联的 issue #540,代码已实现局域网 NSD 自动发现、主机与成员会话、WebSocket 控制、时钟同步和统一播放控制。LanRoomEngine、LanPermissionPreferences 与权限校验支持主机授权切歌、播放/暂停和 seek。LanMediaVolume 支持设备独立音量。LanPlaybackScheduler、Web Audio 引擎…
Out of Scope Changes check Passed 变更与 issue #540 的局域网同步播放目标相关。modules/ltwlan、播放器接入、播放页入口、权限和音量 UI、共享音频与歌词、自动化测试及 BeatSync 原型均直接支持该目标。调试接收器和研究资料用于验证与诊断,未确认存在无关的产品行为变更。
Full details: Linked Issues check

Explanation

针对直接关联的 issue #540,代码已实现局域网 NSD 自动发现、主机与成员会话、WebSocket 控制、时钟同步和统一播放控制。LanRoomEngine、LanPermissionPreferences 与权限校验支持主机授权切歌、播放/暂停和 seek。LanMediaVolume 支持设备独立音量。LanPlaybackScheduler、Web Audio 引擎及相关测试覆盖同步、加载屏障、权限拒绝、文件校验和资源清理。

Full details: Docstring Coverage

Explanation

Docstring coverage is 1.65% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 121 functions across 55 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 1
docs/research/fetch-beatsync-component-sources.ps1: 1

31-37: Minor · Data Integrity & Integration · Quick win

不要将重定向提示页记录为文档。

ktor-supported-platforms.txt 只有“Redirecting…”提示,但 official-doc-sources.json 将其记录为成功抓取。查阅该快照的人无法获得 Ktor 支持平台的信息。请在写入文件和成功记录前检查正文;遇到提示页时,抓取实际目标或记录失败。


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 51f0100 and 46fbf4b.

Files ignored due to path filters: 17
  • CONTRIBUTING.md is excluded by !**/*.md
  • CONTRIBUTING_EN.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • README_EN.md is excluded by !**/*.md
  • THIRD_PARTY_LICENSES.md is excluded by !**/*.md
  • docs/implementation.md is excluded by !**/*.md
  • docs/implementation_EN.md is excluded by !**/*.md
  • docs/lan-listen-together-component-research.md is excluded by !**/*.md
  • docs/lan-listen-together-implementation-validation.md is excluded by !**/*.md
  • docs/lan-listen-together-prototype-results.md is excluded by !**/*.md
  • docs/lan-local-audio-preparation.md is excluded by !**/*.md
  • docs/lan-local-audio-validation.md is excluded by !**/*.md
  • docs/lan-web-audio-sharing-validation.md is excluded by !**/*.md
  • docs/player-buffer-research.md is excluded by !**/*.md
  • modules/ltwlan/README.md is excluded by !**/*.md
  • tools_pub/beatsync-prototype/README.md is excluded by !**/*.md
  • tools_pub/beatsync-prototype/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
Files selected for processing: 210
  • app/src/debug/AndroidManifest.xml
  • app/src/debug/java/moe/ouom/neriplayer/testing/DebugLanReceiver.kt
  • app/src/main/java/moe/ouom/neriplayer/core/di/AppContainer.kt
  • app/src/main/java/moe/ouom/neriplayer/core/di/lan/AppLanModule.kt
  • app/src/main/java/moe/ouom/neriplayer/core/di/lan/LanPermissionPreferences.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/NeriMiniPlayer.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/NeriTabletMiniPlayer.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/WaveformSlider.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/lan/LanPlaybackControls.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/lan/LanRoomPanel.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/feedback/LanFeedback.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/navigation/AppMiniPlayerTabletActions.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/navigation/AppNavigationScaffold.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/lyrics/LyricsScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/NowPlayingScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/NowPlayingSecondaryActions.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/actions/MoreOptionsMainContent.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/cover/NowPlayingCoverActionToolbar.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/lyrics/NowPlayingLyricsLoadOwner.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/tab/settings/lan/LanListenTogetherSection.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/tab/settings/listentogether/SettingsListenTogetherSection.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/tab/settings/page/SettingsSearchIndex.kt
  • app/src/test/java/moe/ouom/neriplayer/architecture/OwnedMainSourceLineBudgetTest.kt
  • app/src/test/java/moe/ouom/neriplayer/core/di/lan/LanPermissionPreferencesTest.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/component/playback/NeriMiniPlayerComposeTest.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/component/playback/WaveformSliderGestureTest.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/component/playback/lan/LanPlaybackControlsTest.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/component/playback/lan/LanRoomEntryTest.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/screen/NowPlayingSecondaryActionsTest.kt
  • app/src/test/java/moe/ouom/neriplayer/ui/screen/tab/settings/page/LanSettingsSearchTest.kt
  • config/quality/crap-scope.json
  • config/quality/domain-dependencies.json
  • docs/research/beatsync-components-2026-10-27/Kotlin--kotlinx.coroutines-README_md.txt
  • docs/research/beatsync-components-2026-10-27/Kotlin--kotlinx.serialization-README_md.txt
  • docs/research/beatsync-components-2026-10-27/NanoHttpd--nanohttpd-README_md.txt
  • docs/research/beatsync-components-2026-10-27/ableton-link-metadata.json
  • docs/research/beatsync-components-2026-10-27/ableton-link-readme.txt
  • docs/research/beatsync-components-2026-10-27/androidx--media-README_md.txt
  • docs/research/beatsync-components-2026-10-27/apache--commons-net-README_md.txt
  • docs/research/beatsync-components-2026-10-27/badaix--snapcast-README_md.txt
  • docs/research/beatsync-components-2026-10-27/bun-metadata.json
  • docs/research/beatsync-components-2026-10-27/cashapp--zipline-README_md.txt
  • docs/research/beatsync-components-2026-10-27/dokar3--quickjs-kt-README_md.txt
  • docs/research/beatsync-components-2026-10-27/freeman-jiang--beatsync-README_md.txt
  • docs/research/beatsync-components-2026-10-27/github-repositories.json
  • docs/research/beatsync-components-2026-10-27/google--gson-README_md.txt
  • docs/research/beatsync-components-2026-10-27/google--oboe-README_md.txt
  • docs/research/beatsync-components-2026-10-27/instacart--truetime-android-README_md.txt
  • docs/research/beatsync-components-2026-10-27/java-websocket-pom.txt
  • docs/research/beatsync-components-2026-10-27/java-websocket-readme.txt
  • docs/research/beatsync-components-2026-10-27/java-websocket-release.json
  • docs/research/beatsync-components-2026-10-27/jmdns--jmdns-README_md.txt
  • docs/research/beatsync-components-2026-10-27/koush--AndroidAsync-README_md.txt
  • docs/research/beatsync-components-2026-10-27/ktor-release.json
  • docs/research/beatsync-components-2026-10-27/ktor-server-engines.txt
  • docs/research/beatsync-components-2026-10-27/ktor-server-websockets.txt
  • docs/research/beatsync-components-2026-10-27/ktor-supported-platforms.txt
  • docs/research/beatsync-components-2026-10-27/ktorio--ktor-README_md.txt
  • docs/research/beatsync-components-2026-10-27/media3-release.json
  • docs/research/beatsync-components-2026-10-27/netty--netty-README_md.txt
  • docs/research/beatsync-components-2026-10-27/oboe-release.json
  • docs/research/beatsync-components-2026-10-27/official-doc-sources.json
  • docs/research/beatsync-components-2026-10-27/protocolbuffers--protobuf-README_md.txt
  • docs/research/beatsync-components-2026-10-27/quickjs-kt-release.json
  • docs/research/beatsync-components-2026-10-27/quickjs-readme.txt
  • docs/research/beatsync-components-2026-10-27/readme-sources.json
  • docs/research/beatsync-components-2026-10-27/sendspin--sendspin-README_md.txt
  • docs/research/beatsync-components-2026-10-27/square--moshi-README_md.txt
  • docs/research/beatsync-components-2026-10-27/square--okhttp-README_md.txt
  • docs/research/beatsync-components-2026-10-27/supplement-sources.json
  • docs/research/beatsync-components-2026-10-27/zipline-release.json
  • docs/research/fetch-beatsync-component-metadata.ps1
  • docs/research/fetch-beatsync-component-sources.ps1
  • docs/research/fetch-beatsync-component-supplements.ps1
  • gradle/libs.versions.toml
  • gradle/owned-modules.txt
  • modules/common/src/main/res/values-en/strings_lan.xml
  • modules/common/src/main/res/values-zh/strings_lan.xml
  • modules/common/src/main/res/values/strings_lan.xml
  • modules/listentogether/src/main/java/moe/ouom/neriplayer/data/ltw/mapping/ListenTogetherTrackIdentity.kt
  • modules/local/src/main/java/moe/ouom/neriplayer/data/settings/AutoSettingsSchema.kt
  • modules/local/src/test/java/moe/ouom/neriplayer/data/settings/AutoSettingsGeneratedTest.kt
  • modules/ltwlan/build.gradle.kts
  • modules/ltwlan/src/androidTest/java/moe/ouom/neriplayer/lanltw/LanSessionDeviceTest.kt
  • modules/ltwlan/src/main/AndroidManifest.xml
  • modules/ltwlan/src/main/assets/lan-audio/LICENSE
  • modules/ltwlan/src/main/assets/lan-audio/UPSTREAM.json
  • modules/ltwlan/src/main/assets/lan-audio/core.js
  • modules/ltwlan/src/main/assets/lan-audio/index.html
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/audio/LanAudioState.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/audio/LanWebAudioEngine.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/BeatSyncRuntime.java
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanAudioDuration.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanClock.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanNtpEstimator.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanPeerTiming.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanRoomEngine.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanRoomPolicy.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/core/LanSchedule.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/discovery/LanDiscovery.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/media/LanFileSnapshot.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/media/LanMediaDownload.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/media/LanMediaServer.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/media/LanPresentationCodec.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/playback/LanPlaybackPort.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/playback/LanPlaybackScheduler.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/playback/LanScheduledAudioPort.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/protocol/LanCodec.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanKeepAlive.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/transport/LanWebSocketClient.kt
  • modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/transport/LanWebSocketServer.kt
  • modules/ltwlan/src/main/resources/beatsync/LICENSE
  • modules/ltwlan/src/main/resources/beatsync/UPSTREAM.json
  • modules/ltwlan/src/main/resources/beatsync/core.js
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/core/LanAudioDurationTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/core/LanLocalRoomTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/core/LanNtpEstimatorTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/core/LanRoomEngineTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/core/LanRoomPolicyTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/core/LanScheduleTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/media/LanFileSnapshotTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/media/LanMediaServerTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/media/LanPcmBudgetTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/media/LanPresentationCodecTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/playback/LanPlaybackSchedulerTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/protocol/LanCodecTest.kt
  • modules/ltwlan/src/test/java/moe/ouom/neriplayer/lanltw/transport/LanWebSocketTransportTest.kt
  • modules/model/src/main/java/moe/ouom/neriplayer/data/model/lan/LanLocalMedia.kt
  • modules/model/src/main/java/moe/ouom/neriplayer/data/model/lan/LanModels.kt
  • modules/model/src/main/java/moe/ouom/neriplayer/data/model/lan/LanSharedPresentation.kt
  • modules/model/src/main/java/moe/ouom/neriplayer/data/model/ltw/track/ListenTogetherTrackKey.kt
  • modules/playback/runtime/build.gradle.kts
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/PlayerManager.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanAudioFileLoader.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanAudioMetadata.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanAudioPresentation.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanHostedMedia.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanLocalIdentity.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanLocalPlayback.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanMediaVolume.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanPlaybackBinding.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanPlayerAdapter.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lan/LanSharedLyrics.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/lifecycle/PlayerManagerLifecycleExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerPersistenceExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerStatePersistenceExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlayerManagerPlaybackExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlayerManagerQueueNavigationExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/service/AudioPlayerService.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/lan/LanAudioMetadataTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/lan/LanHostedMediaTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/lan/LanPlaybackBindingTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/lan/LanQueueSelectionTest.kt
  • modules/playback/runtime/src/test/java/moe/ouom/neriplayer/core/player/lan/LanSharedLyricsTest.kt
  • tools_pub/beatsync-prototype/.gitignore
  • tools_pub/beatsync-prototype/android-smoke/build.gradle.kts
  • tools_pub/beatsync-prototype/android-smoke/candidate-rules.pro
  • tools_pub/beatsync-prototype/android-smoke/gradle.properties
  • tools_pub/beatsync-prototype/android-smoke/proguard-rules.pro
  • tools_pub/beatsync-prototype/android-smoke/settings.gradle.kts
  • tools_pub/beatsync-prototype/android-smoke/src/candidate/kotlin/prototype/smoke/CandidateActivity.kt
  • tools_pub/beatsync-prototype/android-smoke/src/main/AndroidManifest.xml
  • tools_pub/beatsync-prototype/android-smoke/src/main/java/prototype/smoke/MainActivity.kt
  • tools_pub/beatsync-prototype/js/analyze.mjs
  • tools_pub/beatsync-prototype/js/audio-test.mjs
  • tools_pub/beatsync-prototype/js/audio/entry.ts
  • tools_pub/beatsync-prototype/js/audio/shared.ts
  • tools_pub/beatsync-prototype/js/build-audio.mjs
  • tools_pub/beatsync-prototype/js/build-production.mjs
  • tools_pub/beatsync-prototype/js/build.mjs
  • tools_pub/beatsync-prototype/js/entry.ts
  • tools_pub/beatsync-prototype/js/oracle.mjs
  • tools_pub/beatsync-prototype/js/production.ts
  • tools_pub/beatsync-prototype/js/shared.ts
  • tools_pub/beatsync-prototype/js/tsconfig.json
  • tools_pub/beatsync-prototype/js/upstream/LICENSE
  • tools_pub/beatsync-prototype/js/upstream/UPSTREAM.json
  • tools_pub/beatsync-prototype/js/upstream/audioContextManager.ts
  • tools_pub/beatsync-prototype/js/upstream/config.ts
  • tools_pub/beatsync-prototype/js/upstream/constants.ts
  • tools_pub/beatsync-prototype/js/upstream/global.tsx
  • tools_pub/beatsync-prototype/js/upstream/ntp.test.ts
  • tools_pub/beatsync-prototype/js/upstream/ntp.ts
  • tools_pub/beatsync-prototype/js/upstream/ws.ts
  • tools_pub/beatsync-prototype/jvm/build.gradle.kts
  • tools_pub/beatsync-prototype/jvm/gradle.properties
  • tools_pub/beatsync-prototype/jvm/settings.gradle.kts
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/JsExperiment.kt
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/KtorProbeServer.kt
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/Main.kt
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/NetworkExperiment.kt
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/ProbeServers.kt
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/QuickJsExperiment.kt
  • tools_pub/beatsync-prototype/jvm/src/main/kotlin/prototype/Validation.kt
  • tools_pub/beatsync-prototype/package.json
  • tools_pub/beatsync-prototype/results/android-build.json
  • tools_pub/beatsync-prototype/results/android-full-r8-missing-rules.txt
  • tools_pub/beatsync-prototype/results/apk-manifest.json
  • tools_pub/beatsync-prototype/results/bun-original-tests.txt
  • tools_pub/beatsync-prototype/results/node-oracle.json
  • tools_pub/beatsync-prototype/results/rhino.json
  • tools_pub/beatsync-prototype/results/web-audio-original-code.json
  • tools_pub/beatsync-prototype/results/websocket-loopback.json
  • tools_pub/beatsync-prototype/results/websocket-summary.json
  • tools_pub/beatsync-prototype/results/zipline-quickjs.json
  • tools_pub/beatsync-prototype/run.ps1
  • tools_pub/quality/domain_dependencies.py
  • tools_pub/quality/module_boundaries.py
  • tools_pub/quality/test_domain_dependencies.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment on lines +146 to +147
} catch (cancelled: CancellationException) { throw cancelled }
catch (error: Exception) { stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = error.message?.takeIf { it in ERRORS } ?: "NETWORK") }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major · Stability & Availability · Quick win

Supported by static analysis

Script executed:

sed -n 105,160p modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt
sed -n 270,300p modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt
rg -n 'withTimeout|TimeoutCancellation|stopResources|finally' modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt

Repository: cwuom/NeriPlayer

Length of output: 6397


Script executed:

sed -n 80,104p modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt
echo '---ACTIVE/PREPARING---'
rg -n -F -e 'val active' -e 'active:' -e 'preparing' -e 'phase' -- modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/ || test "$?" -eq 1
echo '---ENGINE TIMEOUTS---'
rg -n -F -e 'withTimeout' -e 'TimeoutCancellation' -e 'LanWebAudioEngine' -- modules/ltwlan/src/main/java/ || test "$?" -eq 1
echo '---SESSION REPLACE CALLERS---'
rg -n -F -e 'replaceSession' -- modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt || test "$?" -eq 1

Repository: cwuom/NeriPlayer

Length of output: 5008


超时异常被当作取消重新抛出,会话资源和 CONNECTING 状态无法清理。

host() 的 replaceSession 块中,第 96 行 withTimeout(6000) { transport.ready.await() } 超时后会抛出 TimeoutCancellationException。该异常是 CancellationException 的子类。第 146 行会将其重新抛出,因此第 147 行的 stopResources() 不会执行。该协程随后以取消状态结束,外层没有其他清理逻辑。

结果如下:

  • replaceSession 中获取的 keepAlive 不会释放。
  • 已启动的传输服务和媒体服务不会关闭。
  • state.phase 保持 "CONNECTING",界面不会回到 IDLE。只有手动调用 leave() 或下一次会话替换时,才会执行 stopResources()。

第 277-278 行的 sourceJob 使用相同的 catch 顺序。如果 prepareSharedSource 内部抛出超时,reportError 不会执行,preparing 可能保持为 true。

请在重新抛出 CancellationException 之前单独处理 TimeoutCancellationException。

修复建议
--- "a/modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt"
+++ "b/modules/ltwlan/src/main/java/moe/ouom/neriplayer/lanltw/session/LanSessionManager.kt"
@@ -143,8 +143,10 @@
                     keepAlive.acquire()
                     scheduler = LanPlaybackScheduler(checkNotNull(sessionScope), playback, clock, { offset }, { state.value.nudgeMs }, { state.value.synced }, ::send, ::reportError, ::clearError)
                     block()
-                } catch (cancelled: CancellationException) { throw cancelled }
-                catch (error: Exception) { stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = error.message?.takeIf { it in ERRORS } ?: "NETWORK") }
+                } catch (timeout: TimeoutCancellationException) {
+                    stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = "NETWORK")
+                } catch (cancelled: CancellationException) { throw cancelled }
+                catch (error: Exception) { stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = error.message?.takeIf { it in ERRORS } ?: "NETWORK") }
             }
         }
     }
Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (cancelled: CancellationException) { throw cancelled }
catch (error: Exception) { stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = error.message?.takeIf { it in ERRORS } ?: "NETWORK") }
} catch (timeout: TimeoutCancellationException) {
stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = "NETWORK")
} catch (cancelled: CancellationException) { throw cancelled }
catch (error: Exception) { stopResources(); mutable.value = state.value.copy(phase = "IDLE", error = error.message?.takeIf { it in ERRORS } ?: "NETWORK") }

@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 46fbf4b and d2bd302.

Files ignored due to path filters: 17
  • CONTRIBUTING.md is excluded by !**/*.md
  • CONTRIBUTING_EN.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • README_EN.md is excluded by !**/*.md
  • THIRD_PARTY_LICENSES.md is excluded by !**/*.md
  • docs/implementation.md is excluded by !**/*.md
  • docs/implementation_EN.md is excluded by !**/*.md
  • docs/lan-listen-together-component-research.md is excluded by !**/*.md
  • docs/lan-listen-together-implementation-validation.md is excluded by !**/*.md
  • docs/lan-listen-together-prototype-results.md is excluded by !**/*.md
  • docs/lan-local-audio-preparation.md is excluded by !**/*.md
  • docs/lan-local-audio-validation.md is excluded by !**/*.md
  • docs/lan-web-audio-sharing-validation.md is excluded by !**/*.md
  • docs/player-buffer-research.md is excluded by !**/*.md
  • modules/ltwlan/README.md is excluded by !**/*.md
  • tools_pub/beatsync-prototype/README.md is excluded by !**/*.md
  • tools_pub/beatsync-prototype/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
Files selected for processing: 14
  • app/src/main/java/moe/ouom/neriplayer/core/di/AppContainer.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/component/playback/NeriMiniPlayer.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/navigation/AppNavigationScaffold.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/lyrics/LyricsScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/NowPlayingScreen.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/NowPlayingSecondaryActions.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/actions/MoreOptionsMainContent.kt
  • app/src/main/java/moe/ouom/neriplayer/ui/screen/nowplaying/lyrics/NowPlayingLyricsLoadOwner.kt
  • config/quality/domain-dependencies.json
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/PlayerManager.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerLyricsExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/persistence/PlayerManagerPersistenceExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlayerManagerPlaybackExtensions.kt
  • modules/playback/runtime/src/main/java/moe/ouom/neriplayer/core/player/playback/PlayerManagerSeekExtensions.kt

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@Lollipop2233
Lollipop2233 force-pushed the codex/lan-beatsync-restart branch from 8d8e652 to d7f9efc Compare October 31, 2026 20:52
@Lollipop2233
Lollipop2233 force-pushed the codex/lan-beatsync-restart branch from d7f9efc to 2d6658a Compare October 31, 2026 21:47
@eltavine

eltavine commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

我靠怎么这么大

@Lollipop2233

Copy link
Copy Markdown
Author

我靠怎么这么大

直接让AI抄了 BeatSync 这个项目

- LAN control WebSocket never uses a proxy (NO_PROXY) so RFC1918 and
  loopback addresses are not handed to the system proxy selector
- join timeouts are teardown errors, not cancellations: stop resources
  and fall back to IDLE instead of leaving the room stuck in CONNECTING
- ACTION_SYNC from the LAN bootstrap bypasses the empty-queue stopSelf
  (the sync arrives before the queue is populated)
- service idle/sticky checks count an active LAN session, not just the
  cloud one
@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9447d26747

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

synchronizeClock();if(!audioContextManager.isReady())throw Error('AUDIO_SUSPENDED');
revision=d.revision;state.offsetEstimate=d.offsetMs;state.nudgeOffsetMs=d.nudgeMs;
if(d.type==='PLAY'){
const position=Math.max(0,d.positionSeconds);if(position>=buffer.duration)throw Error('POSITION');stopAt=null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp endpoint seeks before scheduling Web Audio

When a user taps or drags progress to 100%, the UI can submit exactly durationMs, and the room policy accepts that endpoint, but this branch throws POSITION for position >= buffer.duration. The room has already committed the PLAY revision, so scheduler recovery and subsequent SYNC messages keep retrying an unplayable position and leave every member paused/loading until another control changes room state; clamp to the last playable frame or handle the endpoint as track completion.

Useful? React with 👍 / 👎.

Comment on lines +206 to +207
PlayerManager.applyRemoteQueueUpdate(originalQueue, originalIndex)
PlayerManager.setCurrentSongForPlayback(originalQueue.getOrNull(originalIndex))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the pre-session position with the queue

When a listener leaves after a received local lan-session: track, this branch restores the pre-session queue and song but leaves _playbackPositionMs at the shared track's position. applyRemoteQueueUpdate also immediately snapshots that value for the restored queue, and with keep-last-progress enabled the next play uses it as a nonzero requested position, so an unrelated original song can resume and later persist at the LAN track's offset; save and restore the original position alongside the queue/index.

Useful? React with 👍 / 👎.

port.sharedLocalMedia(track, m.localMedia)
port.sharedAudioSources(track, m.sourceUrls)
action?.cancel(); loading?.cancel(); pending = null
action = scope.launch { if (port.prepare(track, m.trackTimeSeconds ?: 0.0)) port.pause() else error("SOURCE") }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the authoritative position for paused syncs

When a client reconnects after missing a pause, or explicitly syncs while paused, and it already has this track decoded, LanPlayerAdapter.prepare() returns immediately from ready(track) without applying trackTimeSeconds; this branch then only pauses the engine. The client therefore keeps its pre-disconnect position instead of the room's authoritative paused position, so paused progress/manual sync remains wrong until a later PLAY reschedules it; seek to the supplied position after preparation (or make the ready path apply it).

Useful? React with 👍 / 👎.

- clamp endpoint seeks in the Web Audio scheduler instead of throwing
  POSITION after the room already committed the PLAY revision, which
  left every member retrying an unplayable position
- save and restore the pre-session playback position alongside the
  restored queue so an unrelated original song does not resume at the
  LAN track's offset
- apply the room's authoritative position when prepare() short-circuits
  on an already-decoded track during paused syncs
@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@Lollipop2233

Lollipop2233 commented Nov 2, 2026 •

Copy link
Copy Markdown
Author

这轮 CI 的 Gradle tests 在 :playback:runtime:testDebugUnitTest 上挂起约 50 分钟后被取消,是偶发的 Robolectric 挂起,不是代码问题——同一个提交 467f1c09 在 fork 的 CI 已整套跑绿(run 41328825110):Gradle tests、4 个仪器分片、2 个特殊宿主、Release build、Verify APK 全部通过,Native CI 也绿。

本 PR 之前的一轮也遇到过一次同样的挂起,重跑即过。麻烦 Re-run failed jobs 重跑这轮的失败任务即可(其余 job 本轮已全绿:4 个仪器分片、2 个特殊宿主、Release build、Native CI)。

@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

…e test

The bundle now clamps out-of-range positions to the last playable frame
per the Codex review fix, so the assertion expecting a POSITION rejection
no longer matches the corrected behavior.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5a4e95e5d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
val at = executeAt(1500.0)
send(id, LanMessage("SCHEDULED_ACTION", scheduledAction = LanAction("PLAY", track.stableKey,
LanSchedule.position(playback.positionSeconds, playback.executeAtMs, at), track), serverTimeToExecute = at, revision = playback.revision, sourceUrls = currentSources, localMedia = currentLocalMedia))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh expiring source URLs before late syncs

When a listener joins or reconnects after the current online candidate has expired, this SYNC reuses currentSources captured by the original PLAY instead of asking the host for fresh candidates. These URLs are explicitly time-limited—the YouTube path parses the expire parameter and bounds its normal cache to eight minutes (modules/platform/src/main/java/moe/ouom/neriplayer/platform/youtube/cache/YouTubePlayableAudioCache.kt:12-30)—and LanPlayerAdapter.resolveSharedAudioSources() also returns cached values whenever the host is ready. The late client therefore fails preparation, and every subsequent SYNC receives the same dead URL until the track changes.

Useful? React with 👍 / 👎.


/** Endpoint host is supplied by the connected control session, never by track metadata. */
class LanMediaDownload(http: OkHttpClient) {
private val client = http.newBuilder().followRedirects(false).followSslRedirects(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass proxies for the LAN media transfer

On a device whose Wi-Fi/system proxy is configured, this client still inherits the default ProxySelector, so the http://<room-host>:<mediaPort> request in fetchResource() can be sent to the proxy instead of directly to the host. LanSessionManager explicitly uses Proxy.NO_PROXY for the control WebSocket, but LanLocalPlayback constructs a default client for this downloader; local-file joins on such networks can therefore fail with LOCAL_TRANSFER (and unnecessarily disclose the bearer request) even though the control connection succeeds.

Useful? React with 👍 / 👎.

ensureInitialized()
if (!initialized) return
if (LanPlaybackBinding.safetyPause(commandSource)) return
if (LanPlaybackBinding.route("PAUSE", commandSource)) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep sleep-timer expiry from becoming a room command

When a sleep countdown expires during a LAN session, createSleepTimerManager() calls pause() with the default LOCAL source (PlayerManager.kt:789-791), and this new early return converts it into a room PAUSE. For a listener without play/pause permission the server rejects it and local Web Audio continues past the requested timer; with permission it pauses every member instead of only satisfying the local timer. Timer expiry needs a local safety/leave path rather than the interactive room-control route.

Useful? React with 👍 / 👎.

Comment on lines +313 to +314
private fun localAddress(): String = NetworkInterface.getNetworkInterfaces().toList().flatMap { it.inetAddresses.toList() }
.firstOrNull { it is Inet4Address && it.isSiteLocalAddress && !it.isLoopbackAddress }?.hostAddress ?: "127.0.0.1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report an address from the active LAN interface

On hosts with Wi-Fi plus mobile data or a VPN, multiple interfaces can have site-local IPv4 addresses and NetworkInterface enumeration is not ordered by the route used for the room. If a cellular/VPN address appears first, state.address and the manual-join address displayed by LanRoomPanel.kt:94 are unreachable from Wi-Fi peers even though the server is listening correctly. Resolve the address from the active LAN/Wi-Fi network rather than taking the first site-local interface.

Useful? React with 👍 / 👎.

- LAN media transfer bypasses proxies like the control channel, so
  local-file joins still work on devices with a system proxy
- sleep-timer expiry stops only the local output: during a LAN session
  it interrupts the local Web Audio instead of broadcasting a room pause
  that could be rejected or pause every member
- the host reports an address from the active internet-capable interface
  (Wi-Fi preferred, VPN excluded) instead of the first site-local one
- when every host-shared online URL has expired, the member retries
  once with freshly resolved URLs from its own account before failing
@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@Lollipop2233

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@Lollipop2233

Copy link
Copy Markdown
Author

@cwuom 这轮的三个仪器测试 job 失败都是环境抖动,证据如下:

  • shard 1:XML 里 0 个真实断言失败(仅 13 个设计内 opt-in 跳过),job 本身却失败——步骤级基础设施问题。
  • shard 3:唯一真失败是 DownloadCorePublicationInstrumentedTest 的一个 ComparisonFailure(下载目录 UUID 断言,本 PR 未触碰 core/download)。同一提交在 fork 的 CI 里该测试类 21/21 全过(run 41490063969 shard 3)。
  • External Lyrics host:已知模拟器抖动,同提交在 fork 首轮失败、rerun 即绿。

Gradle tests(全部门禁 + 单测)本轮已在你的 runner 上通过,四条 Codex P2 修复与七项截图覆盖的 UI 变更都被这轮验证。麻烦 Re-run failed jobs 重跑 shard 1/3 和 External Lyrics host 即可。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Suggestion] 一起听功能建议:基于局域网实现高精度多设备同步播放

2 participants