Skip to content

媒体事件

媒体事件(Media Events)在音频、视频、流媒体等媒体资源播放过程中触发,包括加载、播放、暂停、结束、缓冲等状态变化。媒体事件是构建自定义播放器、处理流媒体加载状态和优化媒体播放体验的核心事件。

前置知识

阅读本节前,建议先了解:剪贴板事件

基础概念

媒体事件类型

事件触发时机说明
loadstart开始加载媒体开始加载时
loadedmetadata元数据加载完成获取到时长、尺寸等信息
loadeddata数据加载完成首帧数据可用
canplay可以播放已缓冲足够数据开始播放
canplaythrough可流畅播放预计可以播放到结束
play开始播放play() 调用后
playing正在播放实际播放中
pause暂停pause() 调用后
ended播放结束播放到末尾
waiting等待缓冲因缓冲而暂停
timeupdate播放位置更新currentTime 变化时
volumechange音量变化volumemuted 变化
error加载错误资源加载失败
html
<!-- HTML 事件属性 -->
<video src="video.mp4"
       onloadstart="showLoading()"
       oncanplay="enablePlayBtn()"
       ontimeupdate="updateProgress()"
       onended="showReplay()">
</video>

<audio src="audio.mp3"
       onplay="startVisualizer()"
       onpause="stopVisualizer()">
</audio>

语法

HTML 事件属性

html
<video controls
       onloadedmetadata="onMetaLoaded()"
       ontimeupdate="onTimeUpdate()"
       onvolumechange="onVolumeChange()"
       onerror="onMediaError(event)">
  <source src="video.mp4" type="video/mp4">
  <source src="video.webm" type="video/webm">
  您的浏览器不支持 video 标签。
</video>

JavaScript 绑定

javascript
const video = document.getElementById('my-video');

video.addEventListener('loadedmetadata', () => {
  console.log('时长:', video.duration);
  console.log('尺寸:', video.videoWidth, 'x', video.videoHeight);
});

video.addEventListener('timeupdate', () => {
  const progress = (video.currentTime / video.duration) * 100;
  progressBar.style.width = progress + '%';
});

video.addEventListener('ended', () => {
  showReplayButton();
});

详细说明

加载状态事件流程

loadstart → loadedmetadata → loadeddata → canplay → canplaythrough
   ↓              ↓              ↓            ↓            ↓
 开始加载     获取元数据    首帧可用    可开始播放    可流畅播放

常用媒体属性

属性类型说明
currentTimenumber当前播放位置(秒)
durationnumber总时长(秒)
pausedboolean是否暂停
endedboolean是否播放结束
volumenumber音量(0-1)
mutedboolean是否静音
bufferedTimeRanges已缓冲的时间范围
playedTimeRanges已播放的时间范围
readyStatenumber就绪状态(0-4)
videoWidth / videoHeightnumber视频原始尺寸

实战示例

完整的自定义视频播放器

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>媒体事件示例</title>
  <style>
    .player {
      max-width: 640px;
      margin: 0 auto;
      background: #000;
      border-radius: 12px;
      overflow: hidden;
    }
    .player video {
      width: 100%;
      display: block;
    }
    .controls {
      padding: 12px 16px;
      background: #1e293b;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .progress-bar {
      width: 100%;
      height: 6px;
      background: #334155;
      border-radius: 3px;
      cursor: pointer;
      position: relative;
    }
    .progress-fill {
      height: 100%;
      background: #3b82f6;
      border-radius: 3px;
      width: 0%;
      transition: width 0.1s linear;
    }
    .buffer-bar {
      position: absolute;
      top: 0;
      left: 0;
      height: 100%;
      background: #475569;
      border-radius: 3px;
      width: 0%;
    }
    .btn-row {
      display: flex;
      align-items: center;
      gap: 12px;
    }
    .btn {
      background: none;
      border: none;
      color: white;
      cursor: pointer;
      font-size: 18px;
      padding: 4px 8px;
      border-radius: 4px;
    }
    .btn:hover { background: #334155; }
    .time {
      color: #94a3b8;
      font-family: monospace;
      font-size: 13px;
      flex: 1;
      text-align: center;
    }
    .volume-slider {
      width: 80px;
      accent-color: #3b82f6;
    }
    .status {
      color: #94a3b8;
      font-size: 12px;
      text-align: center;
    }
  </style>
</head>
<body>
  <div class="player">
    <video id="video" preload="metadata">
      <source src="sample.mp4" type="video/mp4">
    </video>

    <div class="controls">
      <div class="progress-bar" id="progress-bar">
        <div class="buffer-bar" id="buffer-bar"></div>
        <div class="progress-fill" id="progress-fill"></div>
      </div>

      <div class="btn-row">
        <button class="btn" id="play-btn">&#9654;</button>
        <button class="btn" id="mute-btn">&#128266;</button>
        <input type="range" class="volume-slider" id="volume"
               min="0" max="1" step="0.1" value="1">
        <span class="time" id="time">0:00 / 0:00</span>
        <button class="btn" id="fullscreen-btn">&#9974;</button>
      </div>

      <div class="status" id="status">加载中...</div>
    </div>
  </div>

  <script>
    const video = document.getElementById('video');
    const playBtn = document.getElementById('play-btn');
    const muteBtn = document.getElementById('mute-btn');
    const volumeSlider = document.getElementById('volume');
    const progressFill = document.getElementById('progress-fill');
    const bufferBar = document.getElementById('buffer-bar');
    const timeDisplay = document.getElementById('time');
    const statusBar = document.getElementById('status');
    const progressBar = document.getElementById('progress-bar');

    // 格式化时间
    function formatTime(seconds) {
      const m = Math.floor(seconds / 60);
      const s = Math.floor(seconds % 60);
      return m + ':' + String(s).padStart(2, '0');
    }

    // 加载元数据
    video.addEventListener('loadedmetadata', () => {
      timeDisplay.textContent = '0:00 / ' + formatTime(video.duration);
      statusBar.textContent = '就绪';
    });

    // 播放/暂停
    playBtn.addEventListener('click', () => {
      if (video.paused) {
        video.play();
      } else {
        video.pause();
      }
    });

    video.addEventListener('play', () => {
      playBtn.innerHTML = '&#9646;&#9646;';
      statusBar.textContent = '播放中';
    });

    video.addEventListener('pause', () => {
      playBtn.innerHTML = '&#9654;';
      statusBar.textContent = '已暂停';
    });

    video.addEventListener('ended', () => {
      playBtn.innerHTML = '&#9654;';
      statusBar.textContent = '播放结束';
    });

    // 更新进度
    video.addEventListener('timeupdate', () => {
      const progress = (video.currentTime / video.duration) * 100;
      progressFill.style.width = progress + '%';
      timeDisplay.textContent = formatTime(video.currentTime) + ' / ' + formatTime(video.duration);
    });

    // 缓冲进度
    video.addEventListener('progress', () => {
      if (video.buffered.length > 0) {
        const buffered = (video.buffered.end(video.buffered.length - 1) / video.duration) * 100;
        bufferBar.style.width = buffered + '%';
      }
    });

    // 等待缓冲
    video.addEventListener('waiting', () => {
      statusBar.textContent = '缓冲中...';
    });

    video.addEventListener('playing', () => {
      statusBar.textContent = '播放中';
    });

    // 点击进度条跳转
    progressBar.addEventListener('click', (e) => {
      const rect = progressBar.getBoundingClientRect();
      const pos = (e.clientX - rect.left) / rect.width;
      video.currentTime = pos * video.duration;
    });

    // 音量控制
    volumeSlider.addEventListener('input', () => {
      video.volume = volumeSlider.value;
    });

    video.addEventListener('volumechange', () => {
      muteBtn.innerHTML = video.muted ? '&#128263;' : '&#128266;';
      if (!video.muted) volumeSlider.value = video.volume;
    });

    muteBtn.addEventListener('click', () => {
      video.muted = !video.muted;
    });

    // 错误处理
    video.addEventListener('error', () => {
      statusBar.textContent = '视频加载失败';
    });

    // 快捷键
    document.addEventListener('keydown', (e) => {
      if (e.target.tagName === 'INPUT') return;
      switch (e.key) {
        case ' ':
          e.preventDefault();
          video.paused ? video.play() : video.pause();
          break;
        case 'ArrowLeft':
          video.currentTime = Math.max(0, video.currentTime - 5);
          break;
        case 'ArrowRight':
          video.currentTime = Math.min(video.duration, video.currentTime + 5);
          break;
        case 'ArrowUp':
          e.preventDefault();
          video.volume = Math.min(1, video.volume + 0.1);
          volumeSlider.value = video.volume;
          break;
        case 'ArrowDown':
          e.preventDefault();
          video.volume = Math.max(0, video.volume - 0.1);
          volumeSlider.value = video.volume;
          break;
        case 'm':
        case 'M':
          video.muted = !video.muted;
          break;
      }
    });
  </script>
</body>
</html>

注意事项

autoplay 限制

现代浏览器限制自动播放带音频的视频。需要满足以下条件之一:

  • 用户已与页面交互
  • 媒体已静音或 volume 为 0
  • 用户之前对该网站设置过允许自动播放
html
<!-- 推荐:静音自动播放 -->
<video autoplay muted src="video.mp4"></video>

<!-- 需要:先交互再播放 -->
<video id="video" src="video.mp4"></video>
<button onclick="document.getElementById('video').play()">播放</button>

最佳实践

1. 监听多种加载状态

javascript
const events = ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough'];
events.forEach(event => {
  video.addEventListener(event, () => {
    console.log(event, '→ readyState:', video.readyState);
  });
});

下一节

继续学习:draggable 拖拽

参考链接