☰
pure_live 直播间录制动作弹窗:布局溢出修复与事务化提交审计指南
2026/10/15 11:50:23 网站建设 项目流程
  • 音视频
  • 直播
  • 移动开发

【免费下载链接】pure_live

纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。

项目地址:https://gitcode.com/gh_mirrors/pur/pure_live
点击查看免费下载

本篇技术指南以 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,产生两个关键问题:

  1. 没有对提交瞬间的权威任务进行复核,可能向调度器重复提交同一个房间的录制意图;
  2. 原始行为测试根本没有覆盖"现存任务的启动入口",即"任务已存在时通过弹窗启动"这一路径是未验证的。

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/preparingrecordingrecord_circle_fill红色前景 + 12% 红色底
监控中任务存在且未运行monitoredcheckbox_circle_fill主题主色前景 + 10% 主色底
未录制无任务recordrecord_circle_lineonSurfaceVariant前景 +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自定义源应有尽有。

项目地址:https://gitcode.com/gh_mirrors/pur/pure_live
点击查看免费下载

相关推荐

上一篇:Plotly.py 生物信息学可视化指南:用 Alignment Viewer 与 px.bar 绘制多序列比对(MSA)与保守性分析
下一篇:React Admin Buttons 完全指南:导航、批量操作与记录级按钮组件实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询