录制进度与状态:让用户看得见的反馈

做录制功能,最怕什么?

用户点下录制按钮,然后一脸懵——到底在录没录?录了多久?什么时候能停?

我早期做的一个项目就踩过这个坑。用户反馈说「点了录制没反应」,其实后台已经在录了,但界面上啥提示都没有。用户等了一分钟,又点了一次,结果录了两份文件……

所以今天咱们重点聊聊:怎么让录制状态变得「看得见」

录制状态的核心三要素

我个人习惯把录制状态拆成三个维度:

  • 状态指示:当前在干嘛?录制中、暂停、还是空闲?
  • 时间反馈:已经录了多久?
  • 进度可视化:还剩多少空间?或者录到哪了?

你想想看,这三个东西配合好了,用户心里就有底了。

状态指示灯:红点闪烁的艺术

录制指示灯,说白了就是一个红点。但怎么让它看起来「专业」?

我建议用 CSS 动画实现呼吸灯效果,而不是简单的闪烁。

/* 录制指示灯 - 呼吸灯效果 */
.recording-indicator {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background-color: #ff4444;
  display: inline-block;
  margin-right: 8px;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(0.8); }
  100% { opacity: 1; transform: scale(1); }
}

/* 空闲状态 - 灰色 */
.indicator-idle {
  background-color: #999;
  animation: none;
}

/* 暂停状态 - 黄色闪烁 */
.indicator-paused {
  background-color: #ffaa00;
  animation: blink 1s step-end infinite;
}

@keyframes blink {
  50% { opacity: 0.3; }
}

嗯,这里要注意:不要用纯红色常亮。用户会以为那是报错。

我的小技巧: 指示灯旁边一定要配文字。比如「录制中」「已暂停」「空闲」。光靠颜色,色盲用户就惨了。

录制计时器:从零开始的精确计时

计时器怎么实现?很多人直接用 setInterval 每秒加一。但这样有个问题——不准

JavaScript 的定时器会因为事件循环阻塞而延迟。我遇到过录制 10 分钟,计时器显示 9 分 52 秒的情况。

正确的做法是:用开始时间戳来计算差值

class RecordingTimer {
  constructor() {
    this.startTime = null;
    this.elapsed = 0;
    this.timerId = null;
    this.isRunning = false;
  }

  start() {
    this.startTime = Date.now() - this.elapsed;
    this.isRunning = true;
    this._tick();
  }

  pause() {
    if (this.isRunning) {
      clearTimeout(this.timerId);
      this.elapsed = Date.now() - this.startTime;
      this.isRunning = false;
    }
  }

  resume() {
    if (!this.isRunning) {
      this.startTime = Date.now() - this.elapsed;
      this.isRunning = true;
      this._tick();
    }
  }

  stop() {
    clearTimeout(this.timerId);
    this.startTime = null;
    this.elapsed = 0;
    this.isRunning = false;
  }

  _tick() {
    const now = Date.now();
    this.elapsed = now - this.startTime;
    this._updateDisplay(this.elapsed);
    this.timerId = setTimeout(() => this._tick(), 100); // 100ms 刷新一次
  }

  _updateDisplay(ms) {
    const totalSeconds = Math.floor(ms / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    const centiseconds = Math.floor((ms % 1000) / 10);

    // 显示格式:00:00:00.00
    const display = `${this._pad(hours)}:${this._pad(minutes)}:${this._pad(seconds)}.${this._pad(centiseconds)}`;
    // 更新 DOM
    document.getElementById('timer-display').textContent = display;
  }

  _pad(num) {
    return num.toString().padStart(2, '0');
  }
}
关键点: 用 Date.now() 计算差值,而不是累加。这样即使页面卡顿,恢复后时间也是准确的。

录制进度条:不只是好看

进度条有两种场景:

  • 基于时长限制:比如最多录 30 分钟,显示已用比例
  • 基于文件大小:比如最大 500MB,显示已用空间

我建议两种都做,让用户自己选。但默认用时长限制,因为更直观。

function updateProgressBar(elapsedMs, maxDurationMs) {
  const progress = Math.min(elapsedMs / maxDurationMs, 1);
  const percent = (progress * 100).toFixed(1);

  const bar = document.getElementById('progress-bar');
  bar.style.width = `${percent}%`;

  // 颜色渐变:绿 -> 黄 -> 红
  if (progress < 0.6) {
    bar.style.backgroundColor = '#4caf50';
  } else if (progress < 0.85) {
    bar.style.backgroundColor = '#ff9800';
  } else {
    bar.style.backgroundColor = '#f44336';
    // 接近上限时闪烁警告
    bar.classList.add('warning-blink');
  }

  // 显示百分比文字
  document.getElementById('progress-text').textContent = `${percent}%`;
}
我曾经踩过的坑: 进度条到 100% 时自动停止录制,但没给用户缓冲时间。结果用户最后几秒的话被截断了。建议在 95% 时就弹提示,让用户决定是否提前结束。

完整的状态管理

把上面三个组件整合起来,需要一个统一的状态机。

const RecordingState = {
  IDLE: 'idle',
  RECORDING: 'recording',
  PAUSED: 'paused',
  STOPPING: 'stopping', // 正在保存文件
  ERROR: 'error'
};

class RecordingController {
  constructor() {
    this.state = RecordingState.IDLE;
    this.timer = new RecordingTimer();
    this.mediaRecorder = null;
    this.maxDuration = 30 * 60 * 1000; // 30分钟
  }

  startRecording() {
    // ... 初始化 MediaRecorder
    this.state = RecordingState.RECORDING;
    this.timer.start();
    this._updateUI();
  }

  pauseRecording() {
    if (this.state === RecordingState.RECORDING) {
      this.mediaRecorder.pause();
      this.state = RecordingState.PAUSED;
      this.timer.pause();
      this._updateUI();
    }
  }

  resumeRecording() {
    if (this.state === RecordingState.PAUSED) {
      this.mediaRecorder.resume();
      this.state = RecordingState.RECORDING;
      this.timer.resume();
      this._updateUI();
    }
  }

  _updateUI() {
    // 更新指示灯
    const indicator = document.getElementById('recording-indicator');
    indicator.className = `recording-indicator indicator-${this.state}`;

    // 更新状态文字
    const statusText = {
      [RecordingState.IDLE]: '就绪',
      [RecordingState.RECORDING]: '录制中',
      [RecordingState.PAUSED]: '已暂停',
      [RecordingState.STOPPING]: '正在保存...',
      [RecordingState.ERROR]: '发生错误'
    };
    document.getElementById('status-text').textContent = statusText[this.state];
  }
}

知识体系总览

下面这张图,把本章的核心逻辑串起来了:

录制进度与状态核心架构 状态管理 idle / recording / paused 状态指示灯 红点 / 灰点 / 黄点 录制计时器 基于时间戳计算 进度条 时长 / 文件大小 状态机实现 • 状态转换控制 • UI 同步更新 CSS 动画 • 呼吸灯效果 • 颜色状态映射 精确计时 • Date.now() 差值 • 暂停/恢复逻辑 进度计算 • 百分比映射 • 颜色渐变警告 用户体验目标 清晰可见 · 实时反馈 · 避免误操作

实际项目中的避坑指南

最后分享几个我实际项目中遇到的坑:

  1. 计时器在页面隐藏时不准——用 document.visibilitychange 事件校正时间
  2. 进度条不要用 requestAnimationFrame——录制时 CPU 已经吃紧,用 setInterval 100ms 就够了
  3. 状态切换要有防抖——用户快速点击暂停/恢复,可能导致状态错乱
  4. 录制结束时先停计时器,再保存文件——顺序反了会导致时间显示多跳几秒

总结一句话: 录制反馈不是锦上添花,而是必需品。用户看不到状态,就会怀疑你的应用「坏了」。

好,这一章的内容就到这。代码可以直接拿去用,但建议你根据自己的 UI 框架调整一下样式。下一章咱们聊聊更进阶的话题——怎么在录制中实时添加水印和标注。


公众号:蓝海资料掘金营,微信deep3321