- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
本篇技术指南以 pure_live 仓库中docs/ROOM_RECORD_ACTION_DIALOG_LAYOUT_AND_TRANSACTION_AUDIT_2026_09_12.md审计为骨架,完整还原直播间标题栏录制按钮、录制动作弹窗、监控/启动/停止/移除四类提交、录制中心路由衔接与窄屏大字号布局的缺陷根因、修订方案与验证结论,并结合lib/domains/recorder/presentation/widgets/record_action_button.dart、recorder_controller.dart等源码逐行拆解实现细节。读完本文,你将掌握:录制弹窗在 320×480 窄屏与 3.0 倍字号下避免溢出的响应式布局手法、动作提交前"权威任务复核"的事务模型、以及按钮级_busy互斥如何消除重复入口。
一、审计范围与基线
本次审计围绕一条明确的功能链路展开:直播间标题栏录制按钮 → 录制动作弹窗 → 监控/启动/停止/移除提交 → 进入录制中心。
- 基线提交:
06603babdf475555f51ab659aa78438f7db5b9da。 - 审计范围:直播间标题栏录制按钮、录制动作弹窗、监控/启动/停止/移除提交、进入录制中心的路由衔接,以及窄屏大字号布局。
- 测试方式:本批使用真实 Widget、受控录制控制器和路由回调完成验证;没有连接手机、没有启动真实录制、没有构建安装包或发布版本。这意味着所有结论都建立在 Widget 测试与代码审查之上,真实录制启停、权限交互、后台收尾等仍按累计验收矩阵执行。
该按钮在直播间播放页标题栏中通过LivePlayHeader挂载(见 live_play_header.dart):LivePlayHeader的actions中依次放置收藏按钮、录制按钮与菜单按钮,录制按钮由RecordActionButton实现,并传入当前房间room、受控的RecorderController以及打开录制中心的回调controller.openRecordCenter。
二、五类稳定复现的缺陷与根因
审计在 320×480 分辨率、3.0 倍文字缩放下稳定复现了以下五类问题,它们共同构成弹窗"布局溢出 + 事务失控"的完整证据链。
2.1 不可滚动的五项 Column 向底部溢出 1819 像素
旧弹窗基于AlertDialog,内部使用不可滚动的五项Column。在 320×480 / 3.0 倍英文环境下,弹窗稳定向底部溢出1819 像素,导致"立即启动录制"等后部动作完全不可达。
根因在于:AlertDialog默认布局不会把内容放进滚动区,字号放大后每项ListTile高度随之增长,固定视口放不下时内容直接溢出而非滚动。旧实现还强制标题单行省略、弹窗没有任何显式取消动作,进一步压缩了操作空间。
2.2 弹窗打开时一次性捕获任务快照,提交瞬间状态可能过期
旧弹窗在打开时一次性捕获任务与运行状态。但用户在选择动作期间,同一房间的任务可能已经由其他入口(如录制中心、其他直播间页)加入。
此时点击"立即启动录制",旧代码仍按打开弹窗时的"空状态"再次走addTask,产生两个关键问题:
- 没有对提交瞬间的权威任务进行复核,可能向调度器重复提交同一个房间的录制意图;
- 原始行为测试根本没有覆盖"现存任务的启动入口",即"任务已存在时通过弹窗启动"这一路径是未验证的。
2.3 录制按钮没有动作所有者,关闭弹窗后可重复触发
旧录制按钮**没有动作所有权(ownership)**概念:弹窗关闭后,添加监控、启动或进入录制中心尚在等待时,按钮已经可以再次被触发,形成并发提交。停止和移除操作同样没有等待完成,用户快速连点时会产生互相竞争的请求。
2.4 "盲提示"与路由时序冲突
- 盲提示:添加监控时,无论控制器返回有效任务还是空结果,旧代码都会提示"已添加",用户无法区分真正添加成功与静默失败。
- 路由时序冲突:进入录制中心时,代码注释明确要求"避开对话框反向动画",但实际实现却在
DialogRoute返回结果后同帧发起新路由,导致弹窗收起动画与录制中心进入动画重叠,出现两个路由过渡重叠的视觉与生命周期问题。
2.5 非紧凑标题按钮固定 38 像素高,放大文字无法自然增高
非紧凑(非compactHeader)标题栏中的录制按钮被固定为 38 像素高度。在系统文字放大时,按钮内文字无法按内容自然增高,出现文字截断或挤压。
三、修订方案:从布局到事务的全链路重构
针对上述根因,审计给出六项修订,全部落到当前源码实现中。
3.1 按钮自有 State 与单一_busy事务
RecordActionButton改为StatefulWidget,持有唯一事务标志_busy。从打开弹窗到动作完成期间禁用重复入口,系统返回或取消后统一释放:
class _RecordActionButtonState extends State<RecordActionButton> { bool _busy = false; // ... onPressed: _busy ? null : () => unawaited(_handlePressed(context, room)),onPressed在_busy == true时置空,按钮从 Flutter 层面被禁用;_handlePressed内再以if (_busy) return;做第二道防线,随后setState(() => _busy = true),并在finally块中if (mounted) setState(() => _busy = false)释放。这样无论动作成功、失败还是用户取消,按钮都会恢复可点状态,不会永久卡死(源码见 record_action_button.dart)。
3.2 提交瞬间的权威任务复核
动作提交前按房间平台和 ID重新读取当前任务及状态,而不是沿用弹窗打开时的旧快照:
final initialTask = _findTask(liveroom); // 弹窗打开时的快照(用于渲染选项) final action = await _showActionDialog(...); // 用户选择 if (!mounted || action == null) return; final task = _findTask(liveroom); // 提交瞬间的权威任务 final exists = task != null; final isRunning = _isTaskRunning(task);_findTask以platform == liveroom.platform && roomId == liveroom.roomId从recorderController.tasks中实时匹配。修订后的语义为:
- 现存任务走
forceStartTask启动入口(复用既有任务,不重复addTask); - 过期的停止/移除动作安全结束(任务已不存在或状态不符时直接返回);
- 完全避免按弹窗旧快照提交过期意图。
3.3 启动、添加、停止、移除全部等待控制器结果
四类动作均await控制器结果,且仅在语义成立时执行:
| 动作 | 调用链 | 等待语义 |
|---|---|---|
| 立即启动录制 | ensureRecordDirUsable()→forceStartTask(task)或addTask(startImmediately: true) | 已运行则直接返回;等待启动请求完成 |
| 进入录制中心 | onOpenRecordCenter() | 等待路由导航完成 |
| 添加监控 | ensureRecordDirUsable()→addTask(startImmediately: false) | 仅在返回有效任务时ToastUtil.show(i18n("record_task_added")) |
| 停止录制 | stopTask(task) | 仅当任务存在且isRunning时执行 |
| 移除监控 | unRecorder(task) | 仅当任务存在且未运行(!isRunning)时执行 |
其中addTask返回的判定在控制器侧是严格的(recorder_controller.dart):只有当!_isClosing && _ownsTask(task) && !_removingTasks.contains(task)时才返回任务,否则返回null——这正好支撑了"仅在添加返回有效任务时显示成功提示"的修订。startTask同样通过_startRequests去重,同一任务的并发启动请求会共享同一个done.future(见 recorder_controller.dart)。
3.4 录制中心路由时序:等待反向动画后再导航
进入录制中心前等待标准反向动画时长,随后再由播放器控制器隐藏原生视频层并导航,避免两个路由过渡重叠。当前实现中LivePlayController.openRecordCenter负责最终导航(见 live_play_controller.dart),且源码注释明确:视频层的隐藏/恢复策略交由路由观察者处理——Windows侧会在录制中心路由的反向过渡结束后再重新挂接纹理,Android侧由原生视频层策略决定是否保留纹理。修订使"弹窗收起"与"录制中心进入"两个过渡不再同帧叠加,避免路由生命周期交错。
3.5 视口有界弹窗布局
弹窗从AlertDialog改为自绘Dialog,核心布局逻辑在_showActionDialog中(record_action_button.dart):
final availableHeight = mediaQuery.size.height - mediaQuery.padding.vertical - mediaQuery.viewInsets.vertical - 32; final stackTitle = mediaQuery.size.width < 360 || mediaQuery.textScaler.scale(22) > 40;- 标题响应式:当宽度小于 360 或 22px 标题字号被缩放到 40px 以上时,标题从"图标 + 文字"单行
Row切换为上下堆叠的Column,避免横向挤压; - 动作区弹性滚动:五项动作放进
Flexible+SingleChildScrollView,位于弹窗中部弹性滚动区,窄屏大字不再溢出,后部动作全部可达; - 取消按钮固定可达:底部通过
Divider分隔出SafeArea(top: false)包裹的取消按钮,固定在弹窗底部,任何滚动状态下都可点击; - 尺寸约束:外层
ConstrainedBox限制maxWidth: 520、maxHeight: availableHeight(不可用则回退到屏幕高度),内部SizedBox固定宽 440,insetPadding上下左右 16px,圆角 24。
五个动作瓦片_ActionTile按任务状态动态启停:start(绿色,!isRunning时可用)、page(主色,恒可用)、monitor(主色,!exists时可用)、stop(橙色,isRunning时可用)、delete(红色,exists && !isRunning时可用)。禁用态通过AnimatedOpacity(0.72)与 32% 透明度的置灰图标表达,并同步置空onTap。
3.6 静态类型收敛:dynamic→LiveRoom?/LiveRecordTask?
房间和任务从dynamic收敛为可空强类型LiveRoom?/LiveRecordTask?,让状态复核与动作合同由静态类型约束而非运行时推断。这使_findTask、_startRecording、_addMonitor、_stopRecording、_removeMonitor等方法在编译期即可保证空安全与字段访问正确性。
四、按钮三态外观与窄屏适配
RecordActionButton通过Obx订阅控制器任务列表,实时渲染三态外观(源码见 record_action_button.dart):
| 状态 | 判定 | 文案 | 图标 | 配色 |
|---|---|---|---|---|
| 录制中 | running/reconnecting/preparing | recording | record_circle_fill | 红色前景 + 12% 红色底 |
| 监控中 | 任务存在且未运行 | monitored | checkbox_circle_fill | 主题主色前景 + 10% 主色底 |
| 未录制 | 无任务 | record | record_circle_line | onSurfaceVariant前景 +surfaceContainerHighest底 |
_isTaskRunning的判定与RecordStatus枚举语义一致(见 record_status.dart):running、reconnecting、preparing视为运行中。
按钮高度:非紧凑标题按钮保留 38 像素最小高度,但不再写死——minimumSize设为Size(kMinInteractiveDimension, kMinInteractiveDimension)(48×48),文字放大时按钮按内容自然增高;紧凑标题(compactHeader)按钮则固定为kMinInteractiveDimension方形,内部图标通过AnimatedSwitcher做 180ms 缩放切换。非紧凑态的文字标签通过FadeTransition切换,并用Flexible+ 单行省略约束住切换动画中的新旧文本(见 record_action_content.dart),外层Tooltip保证完整动作始终可达。
五、验证结果与质量门禁
审计以真实 Widget 测试为主,验证数据如下:
- 新增
test/record_action_button_dialog_test.dart:6/6 PASS,覆盖:- 320×480 / 3.0 倍中英文下全部动作滚动与固定取消按钮可达;
- 任务迟到加入后的提交复核(弹窗打开后任务被其他入口加入,启动仍正确);
- 添加监控 single-flight(等待期间重复入口被
_busy拦截); - 系统返回释放(弹窗取消后
_busy正确复位); - 录制中心路由时序(弹窗收起动画与导航不重叠)。
- 联合测试:标题按钮、直播间普通布局/导航、录制用户意图、录制中心页面六文件联合执行102/102 PASS。
- 静态分析:全库
analyze无问题(No issues found,耗时 77.4 秒)。 - 质量记录:
local-artifacts/build-records/20260912T044123482Z-quality-focused.json;门禁内审计local-artifacts/repository-audits/20260912T043848583Z-focused.json;文档收口后复核local-artifacts/repository-audits/20260912-room-record-action-final.json,错误0、保留既有警告2。
需要说明:上述测试文件与质量记录由审计文档记录,当前仓库工作区检索到的录制相关测试主要位于 test/domains/recorder 目录下(如record_dir_permission_gate_test.dart、record_input_relay_facts_test.dart、recording_resume_test.dart),可按需对照阅读。
六、验收边界与后续矩阵
本批证明的是录制动作 UI、任务复核、动作互斥和路由衔接四件事;真实录制启停、权限交互、后台收尾及 Android/Windows 候选仍按累计矩阵执行:
- A3-04、A3-05、W2-04、W3-01 保持
RUN; - 宏观验收状态:20 PASS / 33 RUN / 9 NR,共 42 组未闭环。
也就是说,弹窗层的事务与布局问题已在本批收敛,但端到端的录制能力验收仍分阶段推进。
七、相关源码阅读地图
围绕本主题,可进一步阅读以下仓库文件:
- record_action_button.dart:按钮三态、
_busy事务、提交复核、弹窗布局(本主题核心); - record_action_content.dart:紧凑/非紧凑按钮内容切换与动画;
- live_play_header.dart:标题栏挂载点与
RecordActionButton装配; - live_play_controller.dart:
openRecordCenter路由导航与视频层路由观察; - recorder_controller.dart:
addTask/forceStartTask/stopTask/unRecorder/ensureRecordDirUsable等动作合同; - record_status.dart 与 live_record_task.dart:任务状态机与任务模型,含
taskId = "${platform}_$roomId"的定位键设计。
实战要点总结:录制动作类 UI 的稳定实现应同时满足三点——布局必须视口有界(滚动区 + SafeArea 固定取消)、状态必须在提交瞬间复核(而非弹窗打开时快照)、每个动作必须由唯一事务串行化(按钮级_busy+ 等待控制器结果)。pure_live 本次审计正是这三条原则在真实仓库中的完整落地样例。
- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
相关推荐
pure_live 关注分区取消关注弹窗的布局与事务重构审计:从 128 px 溢出到有状态单次提交
pure_live 关注分区取消关注弹窗的布局与事务重构审计:从 128 px 溢出到有状态单次提交 本技术指南基于 pure_live 开源仓库中的审计文档
音视频直播移动开发pure_live 录制中心窄屏布局审计与修复:从 RenderFlex 溢出到 90/90 定向门禁
pure_live 录制中心窄屏布局审计与修复:从 RenderFlex 溢出到 90/90 定向门禁 本篇以 pure_live 仓库 RECORDER_PA
音视频直播移动开发pure_live 录制中心“取消监控”事务与布局重构审计:从全局 context 弹窗到有状态按钮全事务
pure_live 录制中心“取消监控”事务与布局重构审计:从全局 context 弹窗到有状态按钮全事务 导读 本文基于 pure_live 仓库中 2026
音视频直播移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考