Skip to content

feat(player): 编辑弹幕时暂停播放 - #2711

Merged
Predidit merged 3 commits into
Predidit:mainfrom
JesseLee-CN:feat/pause-on-danmaku-edit
Oct 17, 2026
Merged

Predidit merged 3 commits into
Predidit:mainfrom
JesseLee-CN:feat/pause-on-danmaku-edit

Conversation

@JesseLee-CN

@JesseLee-CN JesseLee-CN commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

描述

在播放器底部的弹幕输入框里打字时,番剧仍在继续播放:等弹幕打完,想和小伙伴们吐槽的那一幕往往已经过去,只好先手动暂停、发完再手动继续。这个 PR 让聚焦输入框时自动暂停播放、输入框失焦后自动恢复,和 B 站发弹幕逻辑类似,同时也参考了 animeko 的 pauseVideoOnEditDanmaku 的行为。

默认值与 B 站客户端一致:桌面端不暂停,移动端暂停(移动端行为已在 Android / 鸿蒙 / iOS 上确认,见 #2708)。设置项位于 设置 → 弹幕 → 弹幕显示 → 编辑弹幕时暂停播放,可随时开关。

开启后的行为:

常见操作 结果
播放中聚焦输入框 暂停
输入框失焦(回车发送、点画面、点面板其它区域) 恢复播放
先手动暂停,再聚焦输入框 保持暂停;失焦也不会自动播放
发送弹幕 等「一起看」还是整体弹幕墙的选择弹窗关闭后再恢复
编辑中弹出提示框(例如定时关闭到点) 保持暂停
一起看房间内(含正在连接) 不介入任何播放环节,相当于没开启这个功能。理由是房间内应该不存在发弹幕还要大家一起暂停的荒谬情况
关闭设置开关 与本 PR 改动前一致

实现要点

桌面端和移动端不同默认值的实现:在 settings_keys 里面我做了三处修改,一共是 15 行。第一处是用来判断是否是桌面端供默认值使用的,第二处是加了个设置项,也就是我们这个 PR 的本体,即编辑弹幕时暂停,同时做了一个刚刚提到的第一处的逆运算:如果是桌面端那就为关闭,不是就开启,这两处合起来就实现了桌面端默认关闭,移动端默认开启的功能。第三处就是把该设置项加进了 SettingsKeys.all,作用是调整回默认的时候能把这个也一并重置回到平台默认,这个和其他的设置项是保持一致的。

定时关闭服务的小改动:在 timed_shutdown_service.dart 里加了 5 行 —— 定时真正到期时把 expiryTicks 序号 +1;面板用“暂停时记下的序号”与当前序号比较来判断恢复资格是否仍然有效,这样即使中途面板被重建(切集、画中画)也不会用旧状态把视频播起来。

用户在图形交互层界面设置的实现:在 danmaku_settings 里面我也做了三处修改,其中前两个是为了和其他设置项保持一致,即存放设置的本地状态、打开设置页读一次存储,其他的弹幕设置都有这样的字段,第三处就是加了图形界面上的开关本体,可以供用户自己选择开闭。

功能层的实现(即点击输入框聚焦暂停、发送或点击输入框外取消机制的实现):这个功能主要是对 player_item_panel.dart 做出了修改(面板 +63/-3),机制如下:当我们点击输入框时,函数会监听一个输入框焦点变化信号 textFieldFocus,如果监听到了输入框得到了聚焦,此时做两个条件判断:如果在一起看房间内,直接 return 不参加功能(和操作一览中提到的一样);接下来看如果用户开启了输入暂停且视频正在播放,如果符合该条件,则记录“这次暂停属于本次编辑”(_danmakuPauseTick 快照),然后通过原有的 pause 接口函数暂停弹幕画布和播放,同时播放键图标也随之切换。到这里聚焦暂停工作就实现了。

而失焦有 A 和 B 两条路:A 路径调用了原有 main 里面的 onTapOutside(即点击输入框以外任何地方),此时会传出一个焦点丢失的信号,这个信号会通知播放器和弹幕画布等恢复播放;同时为了防止用户点击播放键造成「失焦恢复 → 点了播放键又暂停了」的负负得正的情况出现,我加入了一个 _danmakuEditBarrier 屏障,在这个屏障作用的过程中,用户即使点到了播放键也不会触发,从而避免了负负得正的情况。B 路径则是回车后点击了发送,但是我们还不能恢复播放,因为此时 Kazumi 会弹出一个弹窗要求选择发送到聊天室还是弹幕墙,故而就做了一个 _holdResumeForDestinationSheet 的 true 手动标记继续暂停,等到弹窗消失后变为 false,手动补一次失焦,播放恢复。

关联的 Issues

PR 类型

  • 添加新功能

AI 辅助开发

  • 基本上都由 AI 实现,但是我非常了解 AI 的代码与写完的产物

AI 辅助模型: deepseek-v4.1-flash

提交前检查

  • 我已经填写了 PR 模板中的所有内容
  • 我已经阅读了 贡献指引 ,并遵守指引进行开发
  • 这个 PR 的功能已经经过我的测试,并且我完全理解我(或 AI)做出的改动

附注

  • 原 PR feat(player): 编辑弹幕时暂停播放 #2706 由我本人关闭,原因是流程不合贡献指引,后续功能整体方向已在 [feat]: 编辑弹幕时暂停播放(桌面端默认关闭,移动端默认开启) #2708 得到维护者确认。
  • 参考实现:animeko 的 pauseVideoOnEditDanmaku 提供了同样的能力,见 VideoScaffoldConfig.kt。
  • 未新增测试文件,沿用仓库既有测试(flutter test 213 项通过);flutter analyze --no-fatal-infos --fatal-warnings 退出码 0,未引入新的提示。
  • 另外按原仓库截图流程(player_item.dart:808-850)补齐了三处守卫:mounted(await 返回后先判断)、应用前台状态(AppLifecycleState.resumed)、睡眠定时到期;并保留发送目标弹窗期间“保持暂停”的等待逻辑。
  • 验证:
    • 手动测试覆盖上表行为,以及编辑期间点视频画面 / 播放键 / 进度条、回车发送、反复进出输入框(Linux 桌面和安卓);
    • 移动端实机验证:Android 构建在鸿蒙设备的 Android 兼容层中安装运行,播放番剧后聚焦弹幕输入框确实暂停、失焦恢复、设置开关默认开启且可关闭,编辑期间点画面/播放键不会出现「恢复又暂停」。

required bool playing,
}) {
if (hasFocus) {
if (playing && _pausedByEdit) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: Re-claim branch pauses even when the setting is disabled or a SyncPlay session exists

This branch returns pause before the shouldPause(enabled:, hasSession:) check below, so stale ownership overrides both gates. Example: the composer pauses, the sleep timer expires mid-edit, and the blur-time resume is vetoed. _pausedByEdit stays true because resumeHandled() never runs. The user then presses play and either joins a SyncPlay room or turns the setting off. Focusing the composer now reaches this branch, and the panel calls pause(enableSync: false). That pauses playback locally with the setting off, or fights the room's playback, which the class doc says must never happen.

When shouldPause is false, drop the stale ownership (_pausedByEdit = false; _resumeAfterSubmit = false;) and return none. Only re-claim when shouldPause(...) is true.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread lib/pages/player/player_item_panel.dart Outdated
// feature allows.
return ListenableBuilder(
listenable: textFieldFocus,
builder: (context, child) => TapRegion(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: consumeOutsideTaps also eats taps on the text field's own selection toolbar and handles

This TapRegion has no groupId, so it forms a group of its own. Flutter draws the TextField's context menu (Paste/Copy/Cut/Select all) and its selection handles in the Overlay, wrapped in TextFieldTapRegion (groupId: EditableText). A tap on any of them is therefore "outside" this region. While hasFocus && pausedByEdit is true, the surface adds an accepting arena member and the toolbar button never receives the tap. On mobile, where the setting is on by default, long-press → Paste stops working while editing.

Put the region in the EditableText group so the field's own overlays count as inside, the same way MenuAnchor scopes its consumer to its own group:

Suggested change
builder: (context, child) => TapRegion(
builder: (context, child) => TapRegion(
groupId: EditableText,

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

This incremental review covers 36f8d24..c441869. The new commit (Revert "update") undoes 36f8d24. The tree at HEAD now matches 1270922 exactly (git diff 1270922..HEAD is empty).

  • Empty-danmaku regression is fixed: the else branch that cleared _danmakuPauseTick on a focus gain that didn't pause is gone. The synchronous _onDanmakuEditFocusChanged() call in the empty-message path no longer drops ownership, so the real blur that follows resumes playback again.
  • mounted after await: _submitDanmakuText still returns on !mounted (line 148) before it calls _onDanmakuEditFocusChanged().
  • Tap barrier: consumeOutsideTaps is still textFieldFocus.hasFocus && _pausedByDanmakuEdit.

The revert also brings back the uncommon edge case noted in the 1270922 review. If a resume is vetoed because a route is on top when the composer loses focus, _danmakuPauseTick stays set. If the user then plays and pauses the video by hand, the next focus and blur of the composer resumes it. This is not posted inline because it takes an unusual sequence of events. A narrow fix would clear _danmakuPauseTick only on a real focus gain while playback is already paused, not in the synchronous call from _submitDanmakuText.

Files Reviewed (1 file)
  • lib/pages/player/player_item_panel.dart
Previous Review Summaries (5 snapshots, latest commit 36f8d24)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 36f8d24)

Status: 1 Issue Found | Recommendation: Address before merge

This incremental review covers 1270922..36f8d24. The new commit clears _danmakuPauseTick on every non-pausing focus gain, on every blur that isn't held, and whenever a SyncPlay session is active. That fixes the stale-ownership edge case from the previous review. It also adds one regression.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
lib/pages/player/player_item_panel.dart 205 Sending an empty danmaku now leaves the video paused. unfocus() is applied in a microtask, so the synchronous _onDanmakuEditFocusChanged() call at line 142 still sees hasFocus == true. It then runs the new else branch, which clears _danmakuPauseTick, so the real blur that follows never resumes playback.
Files Reviewed (1 file)
  • lib/pages/player/player_item_panel.dart - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit 1270922)

Status: No Issues Found | Recommendation: Merge

The branch was force-pushed, so the previous review commit 73fe7e0 is no longer in history. This review covers the full diff at 1270922.

Both earlier findings are fixed:

  • Tap barrier: consumeOutsideTaps is now textFieldFocus.hasFocus && _pausedByDanmakuEdit. The barrier turns off once the composer loses focus, so taps on the destination sheet, dialogs, and player controls go through.
  • mounted after await: _submitDanmakuText now returns on !mounted before it calls _onDanmakuEditFocusChanged().

One edge case, not posted inline because it needs an unusual sequence of events: if a resume is vetoed because a route is on top when the composer loses focus, _danmakuPauseTick stays set. Suppose the user then plays and pauses the video by hand. The next focus and blur of the composer would still resume that video.

Files Reviewed (4 files)
  • lib/pages/player/player_item_panel.dart
  • lib/pages/settings/danmaku/danmaku_settings.dart
  • lib/services/player/timed_shutdown_service.dart
  • lib/services/storage/settings_keys.dart

Previous review (commit 73fe7e0)

Status: 2 Issues Found | Recommendation: Address before merge

The branch was force-pushed and now has a 3-file implementation, so the previous review commit is no longer in history. This review covers the full diff at 73fe7e0. The two earlier inline comments refer to files or code that no longer exist and are outdated.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
lib/pages/player/player_item_panel.dart 209 consumeOutsideTaps only checks _pausedByDanmakuEdit and ignores focus. While the destination sheet or a vetoing dialog is open, taps on its buttons get consumed. After the sheet closes, the flag is reset without a rebuild, so taps on the video and player controls keep being consumed. Arm the barrier only while textFieldFocus.hasFocus.
lib/pages/player/player_item_panel.dart 142 _onDanmakuEditFocusChanged() runs after await showDanmakuDestinationSheet with no mounted check. If the panel is disposed while the sheet is open, ModalRoute.of(context) throws.
Files Reviewed (3 files)
  • lib/pages/player/player_item_panel.dart - 2 issues
  • lib/pages/settings/danmaku/danmaku_settings.dart
  • lib/services/storage/settings_keys.dart

Fix these issues in Kilo Cloud

Previous review (commit 3e5c680)

Status: No Issues Found | Recommendation: Merge

The branch was force-pushed, so the previous review commit is no longer in history. This review covers the full diff at 3e5c680.

Both earlier findings are fixed:

  • danmaku_edit_pause.dart: the shouldPause gate now runs before the re-claim branch.
  • Composer tap barrier: it now uses TextFieldTapRegion (groupId: EditableText), so taps on the selection toolbar and handles are no longer consumed.
Files Reviewed (7 files)
  • lib/pages/player/controller/danmaku_edit_pause.dart
  • lib/pages/player/danmaku_edit_tap_barrier.dart
  • lib/pages/player/player_item_panel.dart
  • lib/pages/settings/danmaku/danmaku_settings.dart
  • lib/services/storage/settings_keys.dart
  • test/danmaku_edit_barrier_test.dart
  • test/danmaku_edit_pause_test.dart

Note (not posted inline because it takes an uncommon sequence of events): if a resume is vetoed, _pausedByEdit stays set. A resume can be vetoed by a SyncPlay session or by a route that does not give focus back. If the user then plays and pauses the video by hand, retryResume() on app foreground, the next blur, or onDisposed() can still resume that paused video. Possible fix: drop the ownership when playback state changes outside the composer.

Previous review (commit 3afd740)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
lib/pages/player/controller/danmaku_edit_pause.dart 44 The re-claim branch returns pause before the shouldPause check. If a stale ownership flag is left from a vetoed resume, focusing the composer still pauses locally while the setting is off or a SyncPlay session is active.
lib/pages/player/player_item_panel.dart 315 This TapRegion has no groupId, so consumeOutsideTaps also eats taps on the TextField's own selection toolbar and handles, which are drawn in the Overlay inside a TextFieldTapRegion. On mobile, where the setting is on by default, Paste/Copy/Select all stop working while editing.
Files Reviewed (5 files)
  • lib/pages/player/controller/danmaku_edit_pause.dart - 1 issue
  • lib/pages/player/player_item_panel.dart - 1 issue
  • lib/pages/settings/danmaku/danmaku_settings.dart
  • lib/services/storage/settings_keys.dart
  • test/danmaku_edit_pause_test.dart

Note: a vetoed resume (for example, a dialog on top when the composer loses focus) leaves _pausedByEdit set. Later, retryResume() on app foreground or onDisposed() on PiP or fullscreen can then resume a video the user has since paused by hand. This takes an uncommon sequence of events, so it is noted here and not posted inline.

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5.5 · Input: 264 · Output: 3.8K · Cached: 302.8K

@JesseLee-CN
JesseLee-CN force-pushed the feat/pause-on-danmaku-edit branch from 3afd740 to 3e5c680 Compare October 15, 2026 16:56
@JesseLee-CN

Copy link
Copy Markdown
Contributor Author

@Predidit 想请教一下:现在这个提交看起来有点臃肿(7 个文件 +1046 行),需要瘦身吗?

需要的话希望你能给点建议,我按你的意见改;要是觉得没必要,我就不动了。谢谢!

@Predidit

Copy link
Copy Markdown
Owner

我还没来得及看这个pr,现在有些太晚了,我会在明天给出review

大概是需要进行重构的,为这个小功能增加1000行代码是无法接受的,deepseek应该犯了一些架构上的错误,小参数模型总是这样

此外如果可以的话,尽可能不要把ai的回复原封不动贴到pr里,毕竟已经勾选了 我对生成的代码充分理解。稍微用几句话解释一下,而不是上千字的ai输出比较好

@JesseLee-CN

JesseLee-CN commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

ok我这边明天应该也会整体重新看一下架构的问题,最初版架构基本上是我提出并且自己审查,完全没有臃肿到这么严重的程度。后面的确由于我本人一直在让ds跑导致出现的问题。

我最初整体的思路其实是复用类似于倍速弹窗那种给整体加一个遮罩+暂停然后就能实现了。也麻烦您帮忙review一下了

这边我指的是之前关闭的PR #2706

@JesseLee-CN
JesseLee-CN force-pushed the feat/pause-on-danmaku-edit branch from 3e5c680 to 73fe7e0 Compare October 16, 2026 06:46
@JesseLee-CN

Copy link
Copy Markdown
Contributor Author

作者大大您好,我个人稍微重构了一下:现在的diff情况是 3 个文件 +75 行,我把ds自己迭代中产生的独立 controller、独立 widget 和一大堆新增测试全部删掉了,但是“一起看”的机制可能会有点复杂,我做了一个比较简单的决策,也想问一下您的意见。麻烦您有空时可以直接review这版。

Comment thread lib/pages/player/player_item_panel.dart Outdated
return ListenableBuilder(
listenable: textFieldFocus,
builder: (context, _) => TextFieldTapRegion(
consumeOutsideTaps: _pausedByDanmakuEdit,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: The barrier stays active after the composer loses focus

consumeOutsideTaps only checks _pausedByDanmakuEdit and ignores focus. That flag stays true while the field is unfocused in two cases:

  • The destination sheet is open (_holdResumeForDestinationSheet).
  • A resume was vetoed because another route is on top, such as the sleep timer dialog.

Both the sheet and the dialog are outside this region, so taps on their buttons get consumed.

The flag also goes stale. When the sheet closes, line 142 sets _pausedByDanmakuEdit = false without a focus change, so this ListenableBuilder does not rebuild and the region keeps consumeOutsideTaps: true. Until the composer gains focus again, taps on the video and the player controls keep being consumed.

Arm the barrier only while the field has focus, as the previous revision did. Focus is still true at pointer-down for the tap that dismisses the composer, so that tap is still consumed:

Suggested change
consumeOutsideTaps: _pausedByDanmakuEdit,
consumeOutsideTaps: textFieldFocus.hasFocus && _pausedByDanmakuEdit,

Reply with @kilocode-bot fix it to have Kilo Code address this issue.


final destination = await showDanmakuDestinationSheet(context);
_holdResumeForDestinationSheet = false;
_onDanmakuEditFocusChanged();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: _onDanmakuEditFocusChanged() runs after await with no mounted check

If the panel is disposed while the destination sheet is open, this call still runs. With _pausedByDanmakuEdit set and the field unfocused, it reaches ModalRoute.of(context). On an unmounted State, context throws, so the result is an unhandled async error. The !mounted check on the next line comes too late to prevent it.

Suggested change
_onDanmakuEditFocusChanged();
if (mounted) _onDanmakuEditFocusChanged();

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@Predidit

Copy link
Copy Markdown
Owner

Hi,

这个PR现在还在你的视野范围内吗,在review之前,需要先解决 kilo bot 自动审查问题,或是为其找到的问题给出明确的异议

@Predidit

Copy link
Copy Markdown
Owner

如果你陷入了某种令人讨厌的修改循环,可以尝试一下不同的模型,例如 Gemini 会是低成本的绝佳选择

@JesseLee-CN

Copy link
Copy Markdown
Contributor Author

作者大大您好,感谢您的建议,这个PR目前还在我的视野范围内,我这边借用了朋友的gpt 6.1 sol xhigh进行了review和建议,同时也进行了人工复核,有几件事情需要告知您:

  1. kilo自动审查对await后没有mounted的判断成立,且这个只需要加一个取反判断即可,我将会在后续的PR更新补上;其对于弹窗关闭后无法触发重建的判断也成立,这个我也会修复;
  2. kilo对于弹窗打开期间点击“一起看”和“视频弹幕”会被消费的判断是不成立的,从代码角度讲Flutter 3.47.7的TapRegion会使得弹窗覆盖时自动停用我的拦截,因此能够正常点击。而从实际测试来看,这两个按钮可以正常点击。因此,我对这个判断提出异议;
  3. 此外我复核了原仓库的截图流程中一些有用的守卫功能,包括mounted、应用前台状态(AppLifecycleState.resumed)、睡眠定时到期。这三个功能上的补齐我也会在后续的PR中补上。

- 聚焦弹幕输入框时暂停、失焦后只恢复自己造成的暂停;一起看房间内不介入
- 复用的 TextFieldTapRegion 屏障只在编辑中且持有暂停时消费那一次点击
- 发送目标弹窗期间保持暂停,关闭后才恢复
- 补齐 mounted、应用前台、睡眠定时到期三处守卫;定时到期由真实事件回收恢复资格
@JesseLee-CN
JesseLee-CN force-pushed the feat/pause-on-danmaku-edit branch from 73fe7e0 to 1270922 Compare October 17, 2026 09:39
@JesseLee-CN

Copy link
Copy Markdown
Contributor Author

Kilo 这轮已经是 No Issues Found / Merge,上面提到的两条也都修正到最新提交了,方便的话麻烦您 review 这一版。

Comment thread lib/pages/player/player_item_panel.dart Outdated
_danmakuPauseTick = TimedShutdownService().expiryTicks.value;
unawaited(playerController.pause(enableSync: false));
} else {
_danmakuPauseTick = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: Sending an empty danmaku now leaves the video paused

FocusNode.unfocus() doesn't change focus right away. Flutter applies the change in a microtask, so hasFocus is still true when the empty-message branch of _submitDanmakuText calls _onDanmakuEditFocusChanged() synchronously (line 142). Playback is already paused because the editor paused it, so this call lands in the new else and clears _danmakuPauseTick. When the real blur arrives a moment later, _canResumeAfterDanmakuEdit is false and playback never resumes. Before this change, the focused branch did nothing in that state, so the later blur still resumed.

Remove the synchronous call at line 142 and let the focus listener handle the blur once _holdResumeForDestinationSheet is false. Another fix is to skip this else when the call doesn't come from a real focus gain.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

This reverts commit 36f8d24.
@Predidit
Predidit merged commit 9a20a01 into Predidit:main Oct 17, 2026
9 checks passed
@JesseLee-CN
JesseLee-CN deleted the feat/pause-on-danmaku-edit branch October 17, 2026 10:49
ErBWs pushed a commit to ErBWs/Kazumi that referenced this pull request Oct 23, 2026
…redidit#2711)

* feat(player): 编辑弹幕时暂停播放

- 聚焦弹幕输入框时暂停、失焦后只恢复自己造成的暂停;一起看房间内不介入
- 复用的 TextFieldTapRegion 屏障只在编辑中且持有暂停时消费那一次点击
- 发送目标弹窗期间保持暂停,关闭后才恢复
- 补齐 mounted、应用前台、睡眠定时到期三处守卫;定时到期由真实事件回收恢复资格

* update

* Revert "update"

This reverts commit 36f8d24.

---------

Co-authored-by: Predidit <34627277+Predidit@users.noreply.github.com>
(cherry picked from commit 9a20a01)
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.

2 participants