语音进度显示 100%,为什么还不能判定播放结束?

一段总长 60 秒的音频,播放到 59.8 秒时,界面把百分比四舍五入成了 100%。如果这时直接触发"播放完成",就把一个显示数字当成了播放器的结束事实。

以米米商聊为背景,已整理的手机端资料包含语音消息。本文借这一功能讨论浏览器音频进度的展示设计,不断言产品采用下文的百分比、时长格式或音频组件,也不推断原生 App 的内部实现。所有输入均为合成快照,没有加载真实语音或用户音频。

1. 播放位置、总时长和结束状态分别表示什么

浏览器媒体元素的 currentTime 表示当前播放位置,duration 表示媒体时长,单位都是秒。位置与时长可以用于计算显示比例,但结束状态还需要依据播放器自身提供的信息。

在真实浏览器接入中,可以参考 ended 属性和 ended 事件的语义。不能把界面经过舍入的百分比重新当成结束判据。

例如,59.8 ÷ 60 × 100 约为 99.67,显示为整数就是 100%。这时剩余时间仍非零。另一方面,位置跳到末尾附近、暂停或更换资源,也不能仅凭一个比例说明发生了哪种播放行为。

信息 本文用来做什么 不应直接推断的事情
currentTime 计算已播放位置的显示值 用户已经听完全部内容
duration 展示总时长及计算比例 一定已经取得有限总时长
舍入后的百分比 显示近似进度 媒体已经触发结束
播放器的 ended 状态 提供结束状态输入 可以替代资源与事件绑定检查

2. 元数据尚未就绪时,不要直接做除法

根据 MDN 的 duration 文档,尚无媒体数据时,duration 可能为 NaN;没有已知时长的媒体可能返回 Infinity。它们不应被格式化成正常的有限总时长。

本例约定:总时长为 NaN 或正无穷时,显示占位文本 --:--,百分比返回 null,表示不能据此画出确定的比例。总时长为 0 时显示 0:00,但也不执行除零运算。

这不是把未知时长当成零。两者的文本结果和含义不同:一个尚无可用的有限总量,一个是输入明确给出的零值。页面需要加载、不可用或错误提示时,还应结合实际媒体事件处理,本文不凭时长一个字段猜测错误原因。

3. 一个只生成显示快照的函数

下面函数接收具名数值与布尔结束状态,输出已播放时间、总时长、整数百分比和原样保留的结束状态。它不读取或控制播放器。

javascript 复制代码
function audioProgressView(input) {
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
    throw new TypeError("media snapshot must be an object");
  }
  const { currentTime, duration, ended } = input;
  const validSeconds = value => typeof value === "number"
    && Number.isFinite(value) && value >= 0
    && value <= Number.MAX_SAFE_INTEGER;
  if (!validSeconds(currentTime)) throw new RangeError("invalid currentTime");
  if (typeof duration !== "number") throw new TypeError("invalid duration type");
  if (typeof ended !== "boolean") throw new TypeError("ended must be boolean");
  const unknown = Number.isNaN(duration) || duration === Infinity;
  if (!unknown && !validSeconds(duration)) throw new RangeError("invalid duration");
  function clock(value) {
    const whole = Math.floor(value);
    const minutes = Math.floor(whole / 60);
    const seconds = String(whole % 60).padStart(2, "0");
    return `${minutes}:${seconds}`;
  }
  if (unknown) {
    return { elapsed: clock(currentTime), total: "--:--", percent: null, ended };
  }
  const shown = Math.min(currentTime, duration);
  return {
    elapsed: clock(shown),
    total: clock(duration),
    percent: duration === 0 ? null : Math.round(shown / duration * 100),
    ended
  };
}

本例以分钟累计显示,超过一小时可以出现 60:01,不会回绕成某个钟表时刻。时间文字向下取整,百分比则四舍五入;它们使用不同的展示精度,不应该反向决定播放器状态。

如果当前位置超过有限总时长,只在显示投影中裁剪到总时长。输入对象不变,也不会把裁剪值写回媒体元素。这样做是示例的显示选择,不是自动修复媒体状态。

数值上限 Number.MAX_SAFE_INTEGER 是本例的输入边界,不是产品语音时长限制。数字字符串、负数、非有限播放位置或非布尔结束状态会被拒绝,避免默默猜测数据含义。

4. 直接验证"100% 但尚未结束"的反例

将下面演示与上一段放在同一个 JavaScript 文件中执行。它只处理合成输入,不发起任何音频请求。

javascript 复制代码
const nearEnd = audioProgressView({ currentTime: 59.8, duration: 60, ended: false });
console.assert(nearEnd.percent === 100);
console.assert(nearEnd.elapsed === "0:59");
console.assert(nearEnd.total === "1:00");
console.assert(nearEnd.ended === false);

const loading = audioProgressView({ currentTime: 0, duration: NaN, ended: false });
console.assert(loading.total === "--:--" && loading.percent === null);

const zero = audioProgressView({ currentTime: 0, duration: 0, ended: false });
console.assert(zero.total === "0:00" && zero.percent === null);

const finished = audioProgressView({ currentTime: 60, duration: 60, ended: true });
console.assert(finished.percent === 100 && finished.ended === true);
console.log(nearEnd);

第一项已经显示 100%,但函数仍保留输入的 ended: false。最后一项保留 ended: true,依据来自调用方提供的结束状态,不是比例本身。

配套验证覆盖未知时长、零时长、正常进度、舍入边界、超出时长、累计分钟、非法输入和对象不变性,并检查 500 个连续合成快照的比例范围及单调显示。它只验证这个纯函数,不是编解码、音频播放或真实客户端验收。

5. 结束状态与进度更新需要分别接入

timeupdate 可以用于刷新播放位置,但它的触发频率会受到系统负载等因素影响,不应被假设为固定频率的精确计时器,见 MDN 的 timeupdate 说明。更频繁地刷新一个经过舍入的百分比,也不会使它变成结束事实。

接入时,应从当前媒体实例取得相应状态,再把同一资源的快照交给显示函数。资源切换后,不能把旧音频的结束回调应用到新消息;播放、暂停、等待缓冲与错误,也不能都用 ended 一个字段表达。

循环播放还需要单独约定。MDN 对 ended 事件说明:当 loop 为真且播放速率非负时,该事件不会按普通单次播放的方式触发。不能把"每段音频最终总会收到一次 ended 事件"写成通用保证。

需要处理的场景 本函数的边界
媒体时长还未知 只提供占位文本与空比例
播放暂停或缓冲 没有实现这些状态的判定
资源切换或组件卸载 没有管理监听器及资源身份
循环播放与自动续播 没有实现播放控制策略
输入状态彼此矛盾 不验证完整媒体状态历史

如果调用方传入了错误的结束状态,函数也无法替它证明播放行为。测试展示投影与验收事件绑定应分别进行,不能把其中一项通过当成另一项已经完成。

6. 验收时同时看数字和实际行为

先核对单位和输入来源:当前位置与总时长是否都是秒,是否属于同一个媒体资源。再分别检查未知时长、零时长和有限时长下的文本与比例,最后通过实际播放验收结束回调、切换资源和循环策略。

对于"用户是否已经听完"这样的业务判断,还需另行定义规则。播放位置可能被拖动,媒体元素到达末尾也不等于用户完整听到了每一段声音,不能直接把展示层的 100% 当成收听行为的证明。

时间文字、百分比和结束状态各自表达一层信息。让显示函数只负责显示,让播放器状态与事件负责提供事实,才能避免一个看似方便的 percent === 100 判断提前改变语音消息的交互。

本文由 AI 辅助整理,独立示例经过本地合成验证。产品背景只引用已整理的手机端语音消息资料;浏览器媒体示例不代表产品时长格式、原生音频组件或内部实现。

相关推荐
高晶2 小时前
C 语言实现 BMS 系统中 SPI 通信协议实战指南
设计模式·程序员
用户355090174862 小时前
工业 RAG 实战:忠实度从 0.85 调到 0.95,我们做对了这几件事
程序员
高晶3 小时前
Android 短信收发模块从零到实战:权限配置、界面设计、发送接收与兼容性适配全攻略
android·程序员
诸神缄默不语1 天前
Zotero安装和简单使用教程
程序员
TF男孩1 天前
老AI人面对MCP、Skills等新名词
人工智能·程序员
Solara1 天前
一个标志位挂了 18 天不落地:它等的重启,2 天 14 小时后就完成了
人工智能·程序员·ai编程
海带紫菜菠萝汤1 天前
本周 AI 观察:算力、路由、账本——护城河正搬出模型层
人工智能·ai·程序员·架构·大模型
高晶2 天前
ARM 嵌入式 Linux 系统启动流程实战指南
设计模式·程序员
楠风测开2 天前
MySQL 慢查询 + AI 智能分析
程序员