{T}

嵌入多媒体元素

HTML5 提供了多种元素用于在网页中嵌入多媒体内容,包括音频、视频、PDF 文档和其他外部资源。这些元素使网页能够展示丰富的媒体内容,无需依赖第三方插件。

概述

HTML5 多媒体元素主要包括:

元素用途说明
<audio>嵌入音频播放音频文件
<video>嵌入视频播放视频文件
<track>媒体轨道为视频添加字幕、说明等
<source>媒体源为 audio/video 提供多个源
<object>嵌入外部资源嵌入 PDF、Flash、SVG 等
<embed>嵌入外部资源简单的嵌入方式
<iframe>嵌入其他网页嵌入其他 HTML 页面

技术架构

HTML5 多媒体技术栈采用分层架构设计:

code
┌─────────────────────────────────────────────────────┐
│                  应用层 (Application)                 │
│     JavaScript API、事件处理、状态管理                 │
├─────────────────────────────────────────────────────┤
│                 DOM 层 (DOM Layer)                   │
│   HTMLMediaElement、HTMLVideoElement、HTMLAudioElement │
├─────────────────────────────────────────────────────┤
│              媒体引擎层 (Media Engine)                 │
│     解码器、缓冲管理、播放控制、同步机制                │
├─────────────────────────────────────────────────────┤
│             平台层 (Platform Layer)                   │
│     操作系统多媒体框架、硬件加速、音频输出              │
└─────────────────────────────────────────────────────┘

核心特性

1. 原生支持

  • 无插件依赖:无需安装 Flash、QuickTime 等第三方插件
  • 跨平台兼容:支持桌面端和移动端浏览器
  • 硬件加速:利用 GPU 进行视频解码和渲染

2. 丰富的 API

  • 播放控制play()pause()load() 等方法
  • 状态管理currentTimevolumeplaybackRate 等属性
  • 事件监听playpauseendedtimeupdate 等事件
  • 高级功能:画中画、全屏、媒体流等

3. 多格式支持

  • 音频格式:MP3、WAV、OGG、AAC、WebM Audio
  • 视频格式:MP4(H.264)、WebM(VP8/VP9)、Ogg(Theora)
  • 自适应码率:HLS、DASH 等流媒体协议

4. 可访问性

  • 字幕支持:通过 <track> 元素添加字幕、说明
  • 键盘控制:支持键盘导航和操作
  • 屏幕阅读器:提供语义化的描述信息

音频 audio

<audio> 是 HTML5 引入的多媒体元素,用于在网页中嵌入音频内容。提供简单而强大的方式来播放音频文件,无需依赖第三方插件

html
<!-- 最简单的音频播放器 -->
<audio src="audio.mp3" controls></audio>

<!-- 多源音频(提供多种格式) -->
<audio controls>
  <source src="audio.mp3" type="audio/mpeg">
  <source src="audio.ogg" type="audio/ogg">
  您的浏览器不支持 audio 元素。
</audio>

<!-- 自动播放和循环 -->
<audio src="background.mp3" autoplay loop muted></audio>
DANGER

注意:大多数浏览器会阻止自动播放带有声音的音频,除非用户与页面进行了交互

<h4>050-audio-player-custom.html</h4>
html
<!-- 来源:9-嵌入多媒体元素.md - 自定义音频播放器 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>自定义音频播放器</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      line-height: 1.6; color: #333;
      background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
      min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px;
    }

    .player-container {
      background: rgba(255,255,255,0.06); backdrop-filter: blur(12px);
      border-radius: 20px; padding: 32px; width: 100%; max-width: 480px;
      box-shadow: 0 16px 48px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.08);
    }

    /* 封面区域 */
    .album-art {
      width: 200px; height: 200px; margin: 0 auto 24px;
      border-radius: 16px; overflow: hidden;
      box-shadow: 0 10px 30px rgba(0,0,0,0.4);
      position: relative;
      animation: rotateAlbum 15s linear infinite paused;
    }
    .album-art.playing { animation-play-state: running; }

    @keyframes rotateAlbum {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }

    .album-art img { width: 100%; height: 100%; object-fit: cover; }

    .play-overlay {
      position: absolute; inset: 0; background: rgba(0,0,0,0.4);
      display: flex; align-items: center; justify-content: center;
      cursor: pointer; transition: opacity 0.3s; border-radius: 16px;
    }
    .play-overlay:hover { background: rgba(0,0,0,0.5); }
    .play-icon {
      width: 60px; height: 60px; background: #667eea; border-radius: 50%;
      display: flex; align-items: center; justify-content: center;
      font-size: 26px; color: white; transition: all 0.3s;
    }
    .play-overlay:hover .play-icon { transform: scale(1.1); }

    /* 歌曲信息 */
    .track-info { text-align: center; margin-bottom: 24px; }
    .track-title { color: white; font-size: 20px; font-weight: 700; margin-bottom: 4px; }
    .track-artist { color: #888; font-size: 14px; }

    /* 进度条 */
    .progress-section { margin-bottom: 18px; }
    .progress-bar {
      height: 6px; background: rgba(255,255,255,0.12); border-radius: 3px;
      cursor: pointer; position: relative; overflow: hidden;
    }
    .progress-fill {
      height: 100%; background: linear-gradient(90deg, #667eea, #764ba2);
      border-radius: 3px; width: 0%; position: relative; transition: width 0.08s linear;
    }
    .time-display {
      display: flex; justify-content: space-between;
      font-family: 'Monaco', monospace; font-size: 11px; color: #888;
      margin-top: 6px;
    }

    /* 控制按钮 */
    .controls {
      display: flex; align-items: center; justify-content: center; gap: 16px;
      margin-bottom: 24px;
    }
    .ctrl-btn {
      width: 44px; height: 44px; border: none; border-radius: 50%;
      background: rgba(255,255,255,0.08); color: white;
      font-size: 18px; cursor: pointer; transition: all 0.2s;
      display: flex; align-items: center; justify-content: center;
    }
    .ctrl-btn:hover { background: rgba(102,126,234,0.6); transform: scale(1.08); }
    .ctrl-btn.main-btn { width: 56px; height: 56px; font-size: 22px; background: #667eea; }
    .ctrl-btn.main-btn:hover { background: #5568d3; }

    /* 音量 */
    .volume-row { display: flex; align-items: center; gap: 10px; justify-content: center; }
    .volume-row input[type="range"] { width: 120px; accent-color: #667eea; cursor: pointer; }

    /* 可视化 */
    .visualizer {
      height: 80px; display: flex; align-items: flex-end; justify-content: center;
      gap: 3px; margin-top: 20px; padding: 0 20px;
    }
    .viz-bar {
      width: 6px; background: linear-gradient(to top, #667eea, #764ba2);
      border-radius: 3px 3px 0 0; height: 4px; transition: height 0.05s ease-out;
    }

    /* 音频元素(隐藏) */
    audio { display: none; }

    /* 歌曲列表 */
    .playlist { margin-top: 20px; max-height: 180px; overflow-y: auto; }
    .playlist h4 { color: #aaa; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
    .playlist-item {
      display: flex; align-items: center; gap: 12px; padding: 10px 14px;
      border-radius: 8px; cursor: pointer; transition: all 0.2s;
      color: #ccc; font-size: 13px;
    }
    .playlist-item:hover { background: rgba(255,255,255,0.06); color: white; }
    .playlist-item.active { background: rgba(102,126,234,0.2); color: white; }
    .playlist-item .item-num { color: #555; min-width: 20px; font-size: 11px; }
    .playlist-item.active .item-num { color: #667eea; }
    .playlist-item .item-duration { margin-left: auto; font-family: 'Monaco', monospace; font-size: 11px; color: #666; }
  </style>
</head>
<body>

<div class="player-container">

  <!-- 隐藏的音频元素 -->
  <audio id="audioPlayer" preload="metadata">
    <source src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" type="audio/mpeg">
    <source src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3" type="audio/mpeg">
    您的浏览器不支持音频播放。
  </audio>

  <!-- 专辑封面 + 播放按钮 -->
  <div class="album-art" id="albumArt">
    <img src="https://picsum.photos/400/400?random=album" alt="专辑封面">
    <div class="play-overlay" onclick="togglePlay()">
      <div class="play-icon" id="mainPlayIcon">▶️</div>
    </div>
  </div>

  <!-- 曲目信息 -->
  <div class="track-info">
    <div class="track-title" id="trackTitle">SoundHelix Song 1</div>
    <div class="track-artist" id="trackArtist">Tobias Agustín</div>
  </div>

  <!-- 进度条 -->
  <div class="progress-section">
    <div class="progress-bar" onclick="seekAudio(event)">
      <div class="progress-fill" id="audioProgress"></div>
    </div>
    <div class="time-display">
      <span id="currentTime">0:00</span>
      <span id="totalTime">0:00</span>
    </div>
  </div>

  <!-- 控制按钮 -->
  <div class="controls">
    <button class="ctrl-btn" onclick="prevTrack()" title="上一曲">⏮️</button>
    <button class="ctrl-btn main-btn" onclick="togglePlay()" title="播放/暂停" id="playBtn">▶️</button>
    <button class="ctrl-btn" onclick="nextTrack()" title="下一曲">⏭️</button>
  </div>

  <!-- 音量控制 -->
  <div class="volume-row">
    <span style="color:#888;font-size:13px;">🔈</span>
    <input type="range" id="volumeSlider" min="0" max="1" step="0.05" value="0.7"
           oninput="setVolume(this.value)">
    <span style="color:#888;font-size:12px;min-width:28px;" id="volumeLabel">70%</span>
  </div>

  <!-- 可视化效果 -->
  <div class="visualizer" id="visualizer">
    <!-- 由 JS 动态生成 -->
  </div>

  <!-- 播放列表 -->
  <div class="playlist">
    <h4>🎵 播放列表</h4>
    <div class="playlist-item active" data-src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
         data-title="SoundHelix Song 1" data-artist="Tobias Agustín" onclick="selectTrack(this)">
      <span class="item-num">01</span> SoundHelix Song 1
      <span class="item-duration">--:--</span>
    </div>
    <div class="playlist-item" data-src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3"
         data-title="SoundHelix Song 2" data-artist="Tobias Agustín" onclick="selectTrack(this)">
      <span class="item-num">02</span> SoundHelix Song 2
      <span class="item-duration">--:--</span>
    </div>
    <div class="playlist-item" data-src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3"
         data-title="SoundHelix Song 3" data-artist="Tobias Agustín" onclick="selectTrack(this)">
      <span class="item-num">03</span> SoundHelix Song 3
      <span class="item-duration">--:--</span>
    </div>
  </div>

</div>

<script>
  const audio = document.getElementById('audioPlayer')
  const albumArt = document.getElementById('albumArt')
  const playBtn = document.getElementById('playBtn')
  const mainPlayIcon = document.getElementById('mainPlayIcon')

  // 格式化时间
  function fmt(s) {
    if (!s || isNaN(s)) return '0:00'
    return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0')
  }

  // 播放/暂停
  function togglePlay() {
    if (audio.paused) audio.play()
    else audio.pause()
  }

  // 设置音量
  function setVolume(val) {
    audio.volume = val
    document.getElementById('volumeLabel').textContent = Math.round(val * 100) + '%'
  }

  // 跳转进度
  function seekAudio(e) {
    const pct = e.offsetX / e.currentTarget.clientWidth
    if (audio.duration) audio.currentTime = pct * audio.duration
  }

  // 选择曲目
  function selectTrack(el) {
    document.querySelectorAll('.playlist-item').forEach(i => i.classList.remove('active'))
    el.classList.add('active')

    audio.src = el.dataset.src
    document.getElementById('trackTitle').textContent = el.dataset.title
    document.getElementById('trackArtist').textContent = el.dataset.artist

    audio.play()
  }

  // 上一曲/下一曲
  function prevTrack() {
    const items = [...document.querySelectorAll('.playlist-item')]
    const current = items.findIndex(i => i.classList.contains('active'))
    selectTrack(items[(current - 1 + items.length) % items.length])
  }

  function nextTrack() {
    const items = [...document.querySelectorAll('.playlist-item')]
    const current = items.findIndex(i => i.classList.contains('active'))
    selectTrack(items[(current + 1) % items.length])
  }


  // ====== 事件监听 ======

  audio.addEventListener('play', () => {
    playBtn.textContent = '⏸️'
    mainPlayIcon.textContent = '⏸️'
    albumArt.classList.add('playing')
  })

  audio.addEventListener('pause', () => {
    playBtn.textContent = '▶️'
    mainPlayIcon.textContent = '▶️'
    albumArt.classList.remove('playing')
  })

  audio.addEventListener('timeupdate', () => {
    if (audio.duration) {
      const pct = (audio.currentTime / audio.duration) * 100
      document.getElementById('audioProgress').style.width = pct + '%'
    }
    document.getElementById('currentTime').textContent = fmt(audio.currentTime)
  })

  audio.addEventListener('loadedmetadata', () => {
    document.getElementById('totalTime').textContent = fmt(audio.duration)
    // 更新列表时长
    document.querySelectorAll('.playlist-item').forEach((item, i) => {
      item.querySelector('.item-duration').textContent = fmt(audio.duration)
    })
  })

  audio.addEventListener('ended', () => nextTrack())

  // 简单可视化(模拟频谱)
  const vizContainer = document.getElementById('visualizer')
  for (let i = 0; i < 30; i++) {
    const bar = document.createElement('div')
    bar.className = 'viz-bar'
    vizContainer.appendChild(bar)
  }
  const bars = vizContainer.querySelectorAll('.viz-bar')

  audio.addEventListener('timeupdate', () => {
    bars.forEach((bar, i) => {
      const h = audio.paused ? 4 : 4 + Math.random() * 40 * (audio.volume || 0.7)
      bar.style.height = h + 'px'
    })
  })
</script>

</body>
</html>

主要属性

属性描述取值默认值
src指定音频文件的 URLURL-
preload指定预加载行为none(不预加载)、metadata(仅预加载元数据)、auto(自动预加载)auto
autoplay是否自动播放autoplay-
loop是否循环播放loop-
muted是否静音muted-
controls是否显示控制条controls-
volume设置音量0.0 到 1.0 之间的数字1.0
crossorigin跨域资源共享anonymoususe-credentials-

音频格式支持

HTML5 <audio> 元素支持多种音频格式:

格式容器编解码器浏览器支持
MP3.mp3MPEG Audio Layer III所有现代浏览器
OGG.oggVorbisFirefox, Chrome, Opera
WAV.wavPCM所有现代浏览器
AAC.m4aAdvanced Audio CodingSafari, iOS, Android
WebM.webmVorbisChrome, Firefox, Opera

API 控制音频播放

基于安全策略,主流的浏览器已经停止音频和视频的自动播放。想让音频自动播放,就需要使用 JavaScript 脚本进行控制。

常用属性

属性类型说明
pausedBoolean是否暂停(只读)
currentTimeNumber当前播放时间(秒)
durationNumber总时长(秒,只读)
volumeNumber音量(0.0 到 1.0)
mutedBoolean是否静音
playbackRateNumber播放速率(1.0 为正常速度)
readyStateNumber就绪状态(0-4)
networkStateNumber网络状态(0-3)
bufferedTimeRanges已缓冲的时间范围(只读)
seekableTimeRanges可跳转的时间范围(只读)

高级属性

属性类型说明
autoplayBoolean是否自动播放
controlsBoolean是否显示控制器
loopBoolean是否循环播放
preloadString预加载策略:none/metadata/auto
srcString媒体资源 URL
currentSrcString实际播放的资源 URL(只读)
defaultPlaybackRateNumber默认播放速率
endedBoolean是否播放结束(只读)
errorMediaError错误对象(只读)
seekingBoolean是否正在跳转(只读)
startTimeNumber播放起始时间(只读)
initialTimeNumber初始播放时间(只读)
playedTimeRanges已播放的时间范围(只读)
audioTracksAudioTrackList音频轨道列表
textTracksTextTrackList文本轨道列表

常用方法

方法说明
play()播放音频(返回 Promise)
pause()暂停播放
load()重新加载音频
canPlayType()检查浏览器是否支持指定格式

高级方法

方法参数返回值说明
fastSeek()time: Numbervoid快速跳转到指定时间
getStartDate()-Date获取媒体时间轴的起始日期
setMediaKeys()mediaKeys: MediaKeysPromise设置 DRM 密钥
addTextTrack()kind, label, languageTextTrack添加文本轨道

readyState 状态值

常量说明
0HAVE_NOTHING没有媒体资源
1HAVE_METADATA已加载元数据
2HAVE_CURRENT_DATA已加载当前帧数据
3HAVE_FUTURE_DATA已加载部分未来数据
4HAVE_ENOUGH_DATA已加载足够数据

networkState 状态值

常量说明
0NETWORK_EMPTY初始状态
1NETWORK_IDLE已完成加载,等待播放
2NETWORK_LOADING正在加载中
3NETWORK_NO_SOURCE未找到合适的源

错误码

错误码常量说明
1MEDIA_ERR_ABORTED用户中止加载
2MEDIA_ERR_NETWORK网络错误
3MEDIA_ERR_DECODE解码错误
4MEDIA_ERR_SRC_NOT_SUPPORTED格式不支持

音频使用示例

javascript
const audio = document.getElementById('myAudio');

// 检查播放状态
if (audio.paused) {
  audio.play();
} else {
  audio.pause();
}

// 设置播放位置
audio.currentTime = 30; // 跳转到 30 秒

// 设置音量
audio.volume = 0.5; // 50% 音量

// 设置播放速率
audio.playbackRate = 1.5; // 1.5 倍速播放

// 检查格式支持
if (audio.canPlayType('audio/mpeg')) {
  console.log('支持 MP3');
}

高级用法

案例-自定义音频播放器

自定义音频播放器界面

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .custom-audio-player {
        max-width: 400px;
        margin: 20px auto;
        font-family: Arial, sans-serif;
      }

      .controls {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-top: 10px;
      }

      .progress-bar {
        flex-grow: 1;
        height: 8px;
        background: #ddd;
        border-radius: 4px;
        margin: 0 10px;
        overflow: hidden;
      }

      #progress {
        height: 100%;
        width: 0;
        background: #4caf50;
        transition: width 0.1s;
      }
    </style>
  </head>
  <body>
    <div class="custom-audio-player">
      <audio
        id="audioPlayer"
        src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"></audio>

      <div class="controls">
        <button id="playBtn">▶</button>
        <button id="pauseBtn">⏸</button>
        <button id="stopBtn">⏹</button>
        <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="1" />
        <span id="currentTime">0:00</span> / <span id="duration">0:00</span>
        <div class="progress-bar">
          <div id="progress"></div>
        </div>
      </div>
    </div>
  </body>

  <script>
    const audio = document.getElementById("audioPlayer")
    const playBtn = document.getElementById("playBtn")
    const pauseBtn = document.getElementById("pauseBtn")
    const stopBtn = document.getElementById("stopBtn")
    const volumeSlider = document.getElementById("volumeSlider")
    const currentTimeDisplay = document.getElementById("currentTime")
    const durationDisplay = document.getElementById("duration")
    const progressBar = document.querySelector(".progress-bar")
    const progress = document.getElementById("progress")

    // 格式化时间显示
    function formatTime(seconds) {
      const mins = Math.floor(seconds / 60)
      const secs = Math.floor(seconds % 60)
      return `${mins}:${secs < 10 ? "0" : ""}${secs}`
    }

    // 更新进度条
    function updateProgress() {
      const percent = (audio.currentTime / audio.duration) * 100
      progress.style.width = `${percent}%`

      currentTimeDisplay.textContent = formatTime(audio.currentTime)
      durationDisplay.textContent = formatTime(audio.duration || 0)
    }

    // 设置进度
    function setProgress(e) {
      const width = this.clientWidth
      const clickX = e.offsetX
      const duration = audio.duration

      audio.currentTime = (clickX / width) * duration
    }

    // 事件监听
    playBtn.addEventListener("click", () => audio.play())
    pauseBtn.addEventListener("click", () => audio.pause())
    stopBtn.addEventListener("click", () => {
      audio.pause()
      audio.currentTime = 0
    })

    volumeSlider.addEventListener("input", () => {
      audio.volume = volumeSlider.value
    })

    audio.addEventListener("timeupdate", updateProgress)
    audio.addEventListener("loadedmetadata", updateProgress)
    audio.addEventListener("ended", () => {
      progress.style.width = "0"
      currentTimeDisplay.textContent = "0:00"
    })

    progressBar.addEventListener("click", setProgress)
  </script>
</html>

高级应用

Web Audio API 集成

Web Audio API 提供了更强大的音频处理能力,可以实现音频可视化、音效处理等高级功能。

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>音频可视化</title>
  <style>
    #visualizer {
      width: 100%;
      height: 200px;
      background: #000;
    }
  </style>
</head>
<body>
  <audio id="audio" src="music.mp3" controls></audio>
  <canvas id="visualizer"></canvas>

  <script>
    const audio = document.getElementById('audio');
    const canvas = document.getElementById('visualizer');
    const ctx = canvas.getContext('2d');

    // 创建音频上下文
    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
    const analyser = audioContext.createAnalyser();
    const source = audioContext.createMediaElementSource(audio);

    // 连接节点
    source.connect(analyser);
    analyser.connect(audioContext.destination);

    // 设置分析器
    analyser.fftSize = 256;
    const bufferLength = analyser.frequencyBinCount;
    const dataArray = new Uint8Array(bufferLength);

    // 绘制可视化
    function draw() {
      requestAnimationFrame(draw);
      
      analyser.getByteFrequencyData(dataArray);
      
      ctx.fillStyle = 'rgb(0, 0, 0)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      
      const barWidth = (canvas.width / bufferLength) * 2.5;
      let barHeight;
      let x = 0;
      
      for (let i = 0; i < bufferLength; i++) {
        barHeight = dataArray[i] / 2;
        
        ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
        ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
        
        x += barWidth + 1;
      }
    }

    // 用户交互后启动
    audio.addEventListener('play', () => {
      if (audioContext.state === 'suspended') {
        audioContext.resume();
      }
      draw();
    });
  </script>
</body>
</html>

音频录制

使用 MediaRecorder API 可以实现音频录制功能:

javascript
// 请求麦克风权限并录制音频
async function recordAudio() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const mediaRecorder = new MediaRecorder(stream);
    const audioChunks = [];

    mediaRecorder.addEventListener('dataavailable', event => {
      audioChunks.push(event.data);
    });

    mediaRecorder.addEventListener('stop', () => {
      const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
      const audioUrl = URL.createObjectURL(audioBlob);
      const audio = new Audio(audioUrl);
      audio.play();
    });

    // 开始录制
    mediaRecorder.start();
    
    // 5秒后停止
    setTimeout(() => {
      mediaRecorder.stop();
      stream.getTracks().forEach(track => track.stop());
    }, 5000);
  } catch (error) {
    console.error('录制失败:', error);
  }
}

音频合成

使用 Web Audio API 可以编程合成音频:

javascript
function playNote(frequency, duration) {
  const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  
  // 创建振荡器
  const oscillator = audioContext.createOscillator();
  const gainNode = audioContext.createGain();
  
  oscillator.connect(gainNode);
  gainNode.connect(audioContext.destination);
  
  // 设置频率和波形
  oscillator.frequency.value = frequency;
  oscillator.type = 'sine'; // sine, square, sawtooth, triangle
  
  // 设置音量包络
  gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
  gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
  
  // 播放
  oscillator.start(audioContext.currentTime);
  oscillator.stop(audioContext.currentTime + duration);
}

// 播放 C4 音符
playNote(261.63, 1);

视频 video

<video> 是 HTML5 引入的多媒体元素,用于在网页中嵌入视频内容。它提供了简单而强大的方式来播放视频文件,无需依赖第三方插件

html
<!-- 最简单的视频播放器 -->
<video src="video.mp4" controls></video>

<!-- 多源视频 -->
<video controls width="600">
  <source src="video.mp4" type="video/mp4">
  <source src="video.webm" type="video/webm">
  <source src="video.ogv" type="video/ogg">
  您的浏览器不支持 video 元素。
</video>

<!-- 自动播放和循环 -->
<video src="background.mp4" autoplay loop muted playsinline></video>
DANGER

注意:大多数浏览器会阻止自动播放带有声音的视频,除非用户与页面进行了交互

<h4>049-video-player-controls.html</h4>
html
<!-- 来源:9-嵌入多媒体元素.md - 视频播放器完整控制 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>视频播放器 — 完整控制演示</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      line-height: 1.6; color: #333;
      background: #1a1a2e; min-height: 100vh; padding: 30px 20px;
    }
    .container { max-width: 900px; margin: 0 auto; }

    h1 { text-align: center; color: white; margin-bottom: 6px; font-size: 28px; }
    .subtitle { text-align: center; color: #888; margin-bottom: 28px; font-size: 14px; }

    /* 视频容器 */
    .video-wrapper {
      background: #000; border-radius: 12px; overflow: hidden;
      box-shadow: 0 10px 40px rgba(0,0,0,0.5); position: relative;
      aspect-ratio: 16/9;
    }

    video {
      width: 100%; height: 100%; display: block;
      background: linear-gradient(135deg, #16213e, #1a1a2e);
    }

    /* 自定义控制栏 */
    .controls {
      display: flex; align-items: center; gap: 12px;
      padding: 14px 18px; background: rgba(22,33,62,0.95);
      backdrop-filter: blur(8px);
    }

    .ctrl-btn {
      width: 42px; height: 42px; border: none; border-radius: 50%;
      background: rgba(255,255,255,0.1); color: white;
      font-size: 18px; cursor: pointer; transition: all 0.2s;
      display: flex; align-items: center; justify-content: center;
    }
    .ctrl-btn:hover { background: rgba(102,126,234,0.7); transform: scale(1.08); }
    .ctrl-btn.play-btn { width: 52px; height: 52px; font-size: 22px; background: #667eea; }
    .ctrl-btn.play-btn:hover { background: #5568d3; }

    /* 进度条 */
    .progress-container {
      flex: 1; display: flex; flex-direction: column; gap: 4px;
    }
    .progress-bar-bg {
      height: 6px; background: rgba(255,255,255,0.15); border-radius: 3px;
      cursor: pointer; position: relative;
    }
    .progress-fill {
      height: 100%; background: linear-gradient(90deg, #667eea, #764ba2);
      border-radius: 3px; width: 0%; transition: width 0.1s linear;
      position: relative;
    }
    .progress-fill::after {
      content:''; position:absolute; right:-6px; top:50%; transform:translateY(-50%);
      width: 14px; height: 14px; background: white; border-radius: 50%;
      box-shadow: 0 2px 6px rgba(0,0,0,0.3); opacity: 0; transition: opacity 0.2s;
    }
    .progress-bar-bg:hover .progress-fill::after { opacity: 1; }

    .time-row { display: flex; justify-content: space-between; font-size: 11px; color: #aaa; }
    .time-current { color: #667eea; font-weight: 600; font-family: 'Monaco', monospace; }
    .time-duration { font-family: 'Monaco', monospace; }

    /* 音量控制 */
    .volume-group { display: flex; align-items: center; gap: 6px; }
    input[type="range"] { width: 80px; accent-color: #667eea; cursor: pointer; }

    /* 倍速选择 */
    .speed-select {
      padding: 6px 10px; border-radius: 6px; border: none;
      background: rgba(255,255,255,0.1); color: white; font-size: 12px;
      cursor: pointer; outline: none;
    }

    /* 状态信息 */
    .status-panel {
      display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
      gap: 16px; margin-top: 24px;
    }
    .status-card {
      background: rgba(255,255,255,0.05); border-radius: 10px;
      padding: 16px; border-left: 3px solid #667eea;
    }
    .status-card h4 { color: #888; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
    .status-card p { color: #ddd; font-family: 'Monaco', monospace; font-size: 13px; }

    /* API 说明 */
    .api-info {
      background: rgba(255,255,255,0.05); border-radius: 10px;
      padding: 20px; margin-top: 24px;
    }
    .api-info h3 { color: white; margin-bottom: 12px; font-size: 16px; }
    .api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
    .api-item {
      background: rgba(0,0,0,0.2); padding: 12px; border-radius: 6px;
      font-size: 12px; color: #bbb; line-height: 1.7;
    }
    .api-item code { color: #667eea; background: rgba(102,126,234,0.15); padding: 1px 5px; border-radius: 3px; }
  </style>
</head>
<body>

<div class="container">
  <h1>🎬 HTML5 视频播放器</h1>
  <p class="subtitle">原生 video 元素 + JavaScript API 完整控制演示</p>

  <!-- 视频播放器 -->
  <div class="video-wrapper">
    <video id="myVideo"
           poster="https://picsum.photos/900/506?random=video-poster"
           preload="metadata">
      <!-- 多格式源:浏览器自动选择支持的格式 -->
      <source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
      <source src="https://www.w3schools.com/html/mov_bbb.webm" type="video/webm">
      您的浏览器不支持 HTML5 视频。
    </video>

    <!-- 控制栏 -->
    <div class="controls">
      <button class="ctrl-btn play-btn" onclick="togglePlay()" title="播放/暂停">▶️</button>

      <div class="progress-container">
        <div class="progress-bar-bg" onclick="seekVideo(event)">
          <div class="progress-fill" id="progressFill"></div>
        </div>
        <div class="time-row">
          <span class="time-current" id="timeCurrent">0:00 / </span>
          <span class="time-duration" id="timeDuration">0:00</span>
        </div>
      </div>

      <div class="volume-group">
        <button class="ctrl-btn" onclick="toggleMute()" title="静音">🔊</button>
        <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="1"
               oninput="changeVolume(this.value)" title="音量">
      </div>

      <select class="speed-select" onchange="changeSpeed(this.value)" title="播放速度">
        <option value="0.5">0.5x</option>
        <option value="0.75">0.75x</option>
        <option value="1" selected>1x</option>
        <option value="1.25">1.25x</option>
        <option value="1.5">1.5x</option>
        <option value="2">2x</option>
      </select>

      <button class="ctrl-btn" onclick="toggleFullscreen()" title="全屏">⛶</button>
    </div>
  </div>


  <!-- 实时状态面板 -->
  <div class="status-panel">
    <div class="status-card">
      <h4>▶️ 播放状态</h4>
      <p id="statusPlaying">已暂停</p>
    </div>
    <div class="status-card">
      <h4>⏱️ 当前时间</h4>
      <p id="statusTime">0.00s</p>
    </div>
    <div class="status-card">
      <h4>⏳ 总时长</h4>
      <p id="statusDuration">--</p>
    </div>
    <div class="status-card">
      <h4>🔈 音量</h4>
      <p id="statusVolume">100%</p>
    </div>
    <div class="status-card">
      <h4>🚀 播放速率</h4>
      <p id="statusSpeed">1.0x</p>
    </div>
    <div class="status-card">
      <h4>📊 缓冲进度</h4>
      <p id="statusBuffered">0%</p>
    </div>
  </div>


  <!-- API 说明 -->
  <div class="api-info">
    <h3>📖 核心 Video API</h3>
    <div class="api-grid">
      <div class="api-item">
        <strong>播放控制:</strong><br>
        <code>play()</code> / <code>pause()</code><br>
        <code>ended</code> 只读属性
      </div>
      <div class="api-item">
        <strong>时间控制:</strong><br>
        <code>currentTime</code> 读写<br>
        <code>duration</code> 只读(总时长)
      </div>
      <div class="api-item">
        <strong>音量控制:</strong><br>
        <code>volume</code> (0-1) 读写<br>
        <code>muted</code> 布尔值读写
      </div>
      <div class="api-item">
        <strong>播放速率:</strong><br>
        <code>playbackRate</code> 读写<br>
        范围:0.25 ~ 16(部分浏览器)
      </div>
      <div class="api-item">
        <strong>核心事件:</strong><br>
        <code>play/pause/ended</code><br>
        <code>timeupdate/loadedmetadata</code>
      </div>
      <div class="api-item">
        <strong>全屏控制:</strong><br>
        <code>requestFullscreen()</code><br>
        <code>exitFullscreen()</code>
      </div>
    </div>
  </div>

</div>

<script>
  const video = document.getElementById('myVideo')
  const playBtn = document.querySelector('.play-btn')

  // 格式化时间
  function formatTime(seconds) {
    if (!seconds || isNaN(seconds)) return '0:00'
    const m = Math.floor(seconds / 60)
    const s = Math.floor(seconds % 60)
    return `${m}:${s.toString().padStart(2, '0')}`
  }

  // 更新状态显示
  function updateStatus() {
    document.getElementById('statusPlaying').textContent = video.paused ? '已暂停' : '正在播放'
    document.getElementById('statusTime').textContent = video.currentTime.toFixed(2) + 's'
    document.getElementById('statusDuration').textContent = video.duration ? formatTime(video.duration) : '--'
    document.getElementById('statusVolume').textContent = Math.round(video.volume * 100) + '%'
    document.getElementById('statusSpeed').textContent = video.playbackRate.toFixed(1) + 'x'

    // 缓冲进度
    if (video.buffered.length > 0) {
      const buffered = (video.buffered.end(video.buffered.length - 1) / video.duration * 100).toFixed(1)
      document.getElementById('statusBuffered').textContent = buffered + '%'
    }
  }

  // 播放/暂停
  function togglePlay() {
    if (video.paused) {
      video.play()
      playBtn.textContent = '⏸️'
    } else {
      video.pause()
      playBtn.textContent = '▶️'
    }
  }

  // 静音切换
  function toggleMute() {
    video.muted = !video.muted
    event.target.textContent = video.muted ? '🔇' : '🔊'
    updateStatus()
  }

  // 音量调节
  function changeVolume(val) {
    video.volume = val
    video.muted = val == 0
    updateStatus()
  }

  // 播放速度
  function changeSpeed(rate) {
    video.playbackRate = parseFloat(rate)
    updateStatus()
  }

  // 进度条点击跳转
  function seekVideo(e) {
    const bar = e.currentTarget
    const percent = e.offsetX / bar.clientWidth
    if (video.duration) {
      video.currentTime = percent * video.duration
    }
  }

  // 全屏切换
  function toggleFullscreen() {
    if (!document.fullscreenElement) {
      video.requestFullscreen().catch(err => console.log('全屏失败:', err))
    } else {
      document.exitFullscreen()
    }
  }


  // ====== 事件监听 ======

  // 时间更新 → 更新进度条和时间显示
  video.addEventListener('timeupdate', () => {
    if (video.duration) {
      const percent = (video.currentTime / video.duration) * 100
      document.getElementById('progressFill').style.width = percent + '%'
    }
    document.getElementById('timeCurrent').textContent = formatTime(video.currentTime) + ' / '
    document.getElementById('statusTime').textContent = video.currentTime.toFixed(2) + 's'
  })

  // 加载元数据 → 显示总时长
  video.addEventListener('loadedmetadata', () => {
    document.getElementById('timeDuration').textContent = formatTime(video.duration)
    document.getElementById('statusDuration').textContent = formatTime(video.duration)
  })

  // 播放结束
  video.addEventListener('ended', () => {
    playBtn.textContent = '🔄'
    updateStatus()
  })

  // 播放/暂停事件
  video.addEventListener('play', () => { playBtn.textContent = '⏸️'; updateStatus() })
  video.addEventListener('pause', () => { playBtn.textContent = '▶️'; updateStatus() })

  // 键盘快捷键
  document.addEventListener('keydown', (e) => {
    if (e.target.tagName === 'INPUT') return

    switch(e.key) {
      case ' ': togglePlay(); break          // 空格键:播放/暂停
      case 'ArrowLeft': video.currentTime -= 5; break   // 左箭头:后退5秒
      case 'ArrowRight': video.currentTime += 5; break  // 右箭头:前进5秒
      case 'ArrowUp': video.volume = Math.min(1, video.volume + 0.1); break  // 上箭头:音量+
      case 'ArrowDown': video.volume = Math.max(0, video.volume - 0.1); break  // 下箭头:音量-
      case 'm': toggleMute(); break            // M键:静音
      case 'f': toggleFullscreen(); break       // F键:全屏
    }
  })
</script>

</body>
</html>

主要属性

属性描述取值默认值
src指定视频文件的 URLURL-
preload指定预加载行为none(不预加载)、metadata(仅预加载元数据)、auto(自动预加载)auto
autoplay是否自动播放autoplay-
loop是否循环播放loop-
muted是否静音muted-
controls是否显示控制条controls-
poster设置视频封面图片URL-
width设置视频宽度像素值-
height设置视频高度像素值-
playsinline在移动设备上内联播放playsinline-
crossorigin跨域资源共享anonymoususe-credentials-
disablePictureInPicture禁用画中画模式disablePictureInPicture-

视频格式支持

HTML5 <video> 元素支持多种视频格式:

格式容器编解码器浏览器支持
MP4.mp4H.264 视频 + AAC 音频所有现代浏览器
WebM.webmVP8/VP9 视频 + Vorbis/Opus 音频Chrome, Firefox, Edge
Ogg.ogvTheora 视频 + Vorbis 音频Firefox, Chrome, Opera
AVI.avi多种编解码器不支持
MOV.movH.264 视频 + AAC 音频Safari

处理视频播放相关事件

在 HTML5 中,当使用 <video><audio> 标签读取或播放媒体时,会触发一系列的事件。

媒体生命周期状态机

媒体元素的加载和播放遵循一个明确的状态机流程:

code
┌──────────────┐
│   HAVE_      │
│  NOTHING (0) │
│  初始状态     │
└──────┬───────┘
       │ loadstart
       ▼
┌──────────────┐
│   HAVE_      │
│  METADATA (1)│
│  已加载元数据 │
└──────┬───────┘
       │ loadedmetadata
       ▼
┌──────────────┐
│   HAVE_      │
│ CURRENT_DATA │
│    (2)       │
│ 当前帧数据    │
└──────┬───────┘
       │ loadeddata
       ▼
┌──────────────┐
│   HAVE_      │
│ FUTURE_DATA  │
│    (3)       │
│ 预加载未来帧  │
└──────┬───────┘
       │ canplay
       ▼
┌──────────────┐
│   HAVE_      │
│ ENOUGH_DATA  │
│    (4)       │
│ 数据充足播放  │
└──────────────┘
  canplaythrough

播放状态转换图

code
        ┌─────────┐
        │  暂停    │
        │ paused  │
        └────┬────┘
             │ play()
             ▼
        ┌─────────┐
        │  播放中  │
        │ playing │◄───────┐
        └────┬────┘        │
             │ waiting     │
             ▼             │ canplay
        ┌─────────┐        │
        │  缓冲中  │────────┘
        │ waiting │
        └────┬────┘
             │ ended
             ▼
        ┌─────────┐
        │  播放完  │
        │  ended  │
        └─────────┘

事件分类

加载事件

事件名称触发时机
loadstart浏览器开始加载媒体数据
progress浏览器正在获取媒体数据
suspend浏览器非主动获取媒体数据,但未完全加载
abort浏览器在完全加载前中止获取媒体数据
error在媒体数据加载过程中出错
emptied媒体元素的数据突然变为未初始化
stalled浏览器获取媒体数据的过程中出现异常
loadedmetadata浏览器已经获取完媒体数据的时长和字节
loadeddata浏览器已加载当前播放位置的媒体数据
canplay浏览器能够开始播放,但可能需要缓冲
canplaythrough浏览器估计可以直接播放完,不需要缓冲

播放事件

事件名称触发时机
play视频即将开始播放
playing已经开始播放
pause暂停播放
waiting播放由于下一帧无效(如未加载)而停止
seeking浏览器正在请求数据(seeking 属性为 true)
seeked浏览器停止请求数据(seeking 属性为 false)
timeupdate当前播放位置改变(注意:原文档中拼写为 tmeupdate,应为 timeupdate)
ended播放已经到达媒体数据的结尾而停止

其他事件

事件名称触发时机
ratechange播放速率被改变
durationchange媒体数据时长被改变
volumechange媒体音量被改变或被静音

事件监听方式

方式 1:使用 addEventListener(推荐)
javascript
const video = document.getElementById('myVideo');

video.addEventListener('play', () => {
  console.log('开始播放');
});

video.addEventListener('pause', () => {
  console.log('暂停播放');
});
方式 2:使用事件属性(不推荐)
html
<video 
  id="myVideo" 
  src="video.mp4" 
  onplay="handlePlay()"
  onpause="handlePause()"
></video>

<script>
  function handlePlay() {
    console.log('开始播放');
  }
  function handlePause() {
    console.log('暂停播放');
  }
</script>

高级用法

案例-自定义视频播放器

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>

    <style>
      .custom-video-player {
        max-width: 600px;
        margin: 20px auto;
        font-family: Arial, sans-serif;
      }

      .controls {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-top: 10px;
      }

      .progress-bar {
        flex-grow: 1;
        height: 8px;
        background: #ddd;
        border-radius: 4px;
        margin: 0 10px;
        overflow: hidden;
      }

      #progress {
        height: 100%;
        width: 0;
        background: #4caf50;
        transition: width 0.1s;
      }
    </style>
  </head>
  <body>
    <div class="custom-video-player">
      <video id="videoPlayer" src="./cat.mp4"></video>

      <div class="controls">
        <button id="playBtn">▶</button>
        <button id="pauseBtn">⏸</button>
        <button id="stopBtn">⏹</button>
        <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="1" />
        <span id="currentTime">0:00</span> / <span id="duration">0:00</span>
        <div class="progress-bar">
          <div id="progress"></div>
        </div>
      </div>
    </div>
  </body>
  <script>
    const video = document.getElementById("videoPlayer")
    const playBtn = document.getElementById("playBtn")
    const pauseBtn = document.getElementById("pauseBtn")
    const stopBtn = document.getElementById("stopBtn")
    const volumeSlider = document.getElementById("volumeSlider")
    const currentTimeDisplay = document.getElementById("currentTime")
    const durationDisplay = document.getElementById("duration")
    const progressBar = document.querySelector(".progress-bar")
    const progress = document.getElementById("progress")

    // 格式化时间显示
    function formatTime(seconds) {
      const mins = Math.floor(seconds / 60)
      const secs = Math.floor(seconds % 60)
      return `${mins}:${secs < 10 ? "0" : ""}${secs}`
    }

    // 更新进度条
    function updateProgress() {
      const percent = (video.currentTime / video.duration) * 100
      progress.style.width = `${percent}%`

      currentTimeDisplay.textContent = formatTime(video.currentTime)
      durationDisplay.textContent = formatTime(video.duration || 0)
    }

    // 设置进度
    function setProgress(e) {
      const width = this.clientWidth
      const clickX = e.offsetX
      const duration = video.duration

      video.currentTime = (clickX / width) * duration
    }

    // 事件监听
    playBtn.addEventListener("click", () => video.play())
    pauseBtn.addEventListener("click", () => video.pause())
    stopBtn.addEventListener("click", () => {
      video.pause()
      video.currentTime = 0
    })

    volumeSlider.addEventListener("input", () => {
      video.volume = volumeSlider.value
    })

    video.addEventListener("timeupdate", updateProgress)
    video.addEventListener("loadedmetadata", updateProgress)
    video.addEventListener("ended", () => {
      progress.style.width = "0"
      currentTimeDisplay.textContent = "0:00"
    })

    progressBar.addEventListener("click", setProgress)
  </script>
</html>

高级应用

视频截图

使用 Canvas 可以实现视频截图功能:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>视频截图</title>
</head>
<body>
  <video id="video" src="video.mp4" controls width="600"></video>
  <button id="capture">截图</button>
  <canvas id="canvas" width="600" height="400"></canvas>
  <a id="download" style="display: none;">下载截图</a>

  <script>
    const video = document.getElementById('video');
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const captureBtn = document.getElementById('capture');
    const downloadLink = document.getElementById('download');

    captureBtn.addEventListener('click', () => {
      // 将视频帧绘制到 canvas
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      
      // 转换为图片并下载
      canvas.toBlob(blob => {
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadLink.download = `screenshot-${Date.now()}.png`;
        downloadLink.style.display = 'inline';
        downloadLink.textContent = '下载截图';
      });
    });
  </script>
</body>
</html>

画中画模式

画中画(Picture-in-Picture)允许视频在浮动窗口中播放:

javascript
const video = document.getElementById('myVideo');

// 进入画中画模式
async function enterPiP() {
  try {
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      await video.requestPictureInPicture();
    }
  } catch (error) {
    console.error('画中画模式失败:', error);
  }
}

// 监听画中画事件
video.addEventListener('enterpictureinpicture', () => {
  console.log('进入画中画模式');
});

video.addEventListener('leavepictureinpicture', () => {
  console.log('退出画中画模式');
});

// 检查浏览器支持
if (document.pictureInPictureEnabled) {
  console.log('浏览器支持画中画');
}

全屏控制

HTML5 Fullscreen API 允许视频全屏播放:

javascript
const video = document.getElementById('myVideo');

// 进入全屏
function toggleFullscreen() {
  if (!document.fullscreenElement) {
    if (video.requestFullscreen) {
      video.requestFullscreen();
    } else if (video.webkitRequestFullscreen) {
      video.webkitRequestFullscreen(); // Safari
    } else if (video.msRequestFullscreen) {
      video.msRequestFullscreen(); // IE11
    }
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }
  }
}

// 监听全屏变化
document.addEventListener('fullscreenchange', () => {
  if (document.fullscreenElement) {
    console.log('进入全屏');
  } else {
    console.log('退出全屏');
  }
});

视频流处理

使用 Canvas 实时处理视频帧:

html
<video id="video" src="video.mp4" controls width="600"></video>
<canvas id="canvas" width="600" height="400"></canvas>

<script>
  const video = document.getElementById('video');
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  // 实时处理视频帧
  function processFrame() {
    if (!video.paused && !video.ended) {
      // 绘制视频帧
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      
      // 获取像素数据
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const data = imageData.data;
      
      // 应用灰度滤镜
      for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
        data[i] = avg;     // R
        data[i + 1] = avg; // G
        data[i + 2] = avg; // B
      }
      
      // 放回处理后的图像
      ctx.putImageData(imageData, 0, 0);
      
      requestAnimationFrame(processFrame);
    }
  }

  video.addEventListener('play', processFrame);
</script>

摄像头访问

使用 getUserMedia API 访问摄像头:

javascript
async function accessCamera() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        facingMode: 'user' // 或 'environment' 用于后置摄像头
      },
      audio: true
    });

    const video = document.getElementById('myVideo');
    video.srcObject = stream;
    video.play();

    // 列出可用的视频输入设备
    const devices = await navigator.mediaDevices.enumerateDevices();
    const videoDevices = devices.filter(device => device.kind === 'videoinput');
    console.log('可用摄像头:', videoDevices);
  } catch (error) {
    console.error('摄像头访问失败:', error);
  }
}

// 切换摄像头
async function switchCamera() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const videoDevices = devices.filter(device => device.kind === 'videoinput');
  
  // 切换到下一个摄像头
  const currentDevice = stream.getVideoTracks()[0];
  const currentIndex = videoDevices.findIndex(d => d.deviceId === currentDevice.getSettings().deviceId);
  const nextIndex = (currentIndex + 1) % videoDevices.length;
  
  const newStream = await navigator.mediaDevices.getUserMedia({
    video: { deviceId: { exact: videoDevices[nextIndex].deviceId } }
  });
  
  video.srcObject = newStream;
}

视频录制

录制视频流并保存:

javascript
async function recordVideo() {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
  const mediaRecorder = new MediaRecorder(stream, {
    mimeType: 'video/webm;codecs=vp9'
  });
  
  const chunks = [];

  mediaRecorder.ondataavailable = e => {
    if (e.data.size > 0) {
      chunks.push(e.data);
    }
  };

  mediaRecorder.onstop = () => {
    const blob = new Blob(chunks, { type: 'video/webm' });
    const url = URL.createObjectURL(blob);
    
    // 创建下载链接
    const a = document.createElement('a');
    a.href = url;
    a.download = `recording-${Date.now()}.webm`;
    a.click();
  };

  // 开始录制
  mediaRecorder.start();

  // 10秒后停止
  setTimeout(() => {
    mediaRecorder.stop();
    stream.getTracks().forEach(track => track.stop());
  }, 10000);
}

为视频添加字幕

要为视频添加字幕,需要使用 HTML5 中新增的 <track> 标签,还需要创建对应于视频的字幕文件。为视频添加字幕后,如果在本地运行文件,则不会显示字幕。你需要在运行该文件之前将其部署到 Web 服务器上

<track> 标签用于给视频添加外部文本轨道,可以是字幕、简短说明、描述等。<track> 标签必须被写在 <video><audio> 标签之间。

track 属性

属性描述必需示例
kind轨道类型subtitlescaptions
src轨道文件的 URLsrc="subtitles.vtt"
srclang轨道语言(字幕时必需)srclang="zh-CN"
label轨道标题(显示在菜单中)label="中文字幕"
default是否为默认轨道default

kind 属性值

说明
subtitles字幕(翻译的对话)
captions字幕(包含音效描述)
descriptions视频描述(用于屏幕阅读器)
chapters章节标题(用于导航)
metadata元数据(不显示给用户)
html
<video id="mainvideo" controls width="90%" src="python.mp4" type="video/mp4">
  <track src="pythonzh.vtt" srclang="zh-CN" label="中文字幕" kind="captions" default />
</video>

创建字幕文件

<track> 标签使用 WebVTT 文件作为字幕文件。该文件是一个 UTF-8 编码的文本文件。文件内容需要按以下格式进行添加。

  • 第一行必须为 WEBVTT,并且后面要接一个空行,即第二行为一个空行
  • 从第三行开始为字幕内容。字幕由标识符、时间范围和字幕文本(每个占一行)组成,其中时间范围和字幕文本是必选的,而标识符为可选。时间范围的格式为“起始时间--> 结束时间”。时间格式为 HH:MM:SS.sss,其中 HH: 是可以省略的
html
WEBVTT Cue-1 00:00:00.150 --> 00:00:01.100 大家好! Cue-2 00:00:01.180 --> 00:00:06.000
从今天开始,我们来学习Python程序设计的课程

在结束时间的右侧,还可以添加设置字幕位置和对齐方式的选项:

属性含义
align设置水平对齐方式,可选值为 start(左对齐)、middle(居中对齐)、cnd(右对齐)
line设置行位置,負数表示从播放器的底部开始数,正数表示从播放器的顶部开始数
position设置左侧的边距,采用百分比表示
size设置字幕的宽度占整休播放器窗口宽度的百分比,采用百分比表示

在字幕文本中,可以添加内联样式,常用的内联样式有 <i></i> 表示斜体;<b></b> 表示粗体;<u></u> 表示添加下画线;<c></c> 表示定义 CSS 样式,将字幕文本使用 <c></c> 括起来,然后在 HTML 文件中,通过定义 CSS 样式来改变字幕文本的样式

html
00:00:01.180 --> 00:00:06.000 <c>从今天开始,我们来学习Python程序设计的课程。</c>

在 HTML 文件中,可以通过下面的 CSS 代码设置字幕文本的颜色为白色

html
<style>
  video::cue(c) {
    color: white;
    font-size: 28px;
  }
</style>

播放字幕视频

下面项目需要在服务器中运行,使用 vscode 的 Open with liver server 功能

嵌入 PDF 文档

object 元素

<object> 是 HTML 中用于嵌入外部资源的通用元素,它可以包含替代内容(当嵌入失败时显示),并且支持参数传递

主要属性:

属性描述示例
data指定要嵌入资源的 URLdata="example.pdf"
type指定嵌入内容的 MIME 类型type="application/pdf"
width设置嵌入内容的宽度width="600"
height设置嵌入内容的高度height="400"
name为对象指定名称name="pdfViewer"
usemap关联客户端图像映射usemap="#map1"
form关联的表单 IDform="myForm"
classid指定 ActiveX 控件的 CLSID(仅 IE)classid="clsid:..."

基本语法:

html
<!-- 嵌入 PDF 文件 -->
<object data="document.pdf" type="application/pdf" width="100%" height="600px">
  <p>您的浏览器不支持PDF查看,请<a href="document.pdf">下载PDF文件</a>查看。</p>
</object>

<!-- 嵌入 SVG 图像 -->
<object data="image.svg" type="image/svg+xml" width="300" height="200">
  您的浏览器不支持SVG图像。
</object>

embed 元素

<embed> 是一个更简单的元素,专门用于嵌入外部资源。它没有结束标签,且不像 <object> 那样支持替代内容

主要属性:

属性描述示例
src指定要嵌入资源的 URLsrc="example.mp3"
type指定嵌入内容的 MIME 类型type="audio/mpeg"
width设置嵌入内容的宽度width="300"
height设置嵌入内容的高度height="50"
autoplay是否自动播放autoplay
loop是否循环播放loop
muted是否静音muted
pluginspage指定插件下载页面(已过时)pluginspage="http://..."

基本语法:

html
<!-- 嵌入音频文件 -->
<embed src="music.mp3" type="audio/mpeg" width="300" height="50">

<!-- 嵌入视频文件 -->
<embed src="video.mp4" type="video/mp4" width="600" height="400">

<!-- 嵌入 PDF 文件 -->
<embed src="document.pdf" type="application/pdf" width="100%" height="600px">

注意:<embed> 的参数传递方式不如 <object> 灵活,通常通过 URL 参数传递

iframe 元素

<iframe>(Inline Frame)用于在当前页面中嵌入另一个 HTML 页面,常用于嵌入地图、视频、广告等内容。

基本语法

html
<iframe src="https://example.com" width="800" height="600"></iframe>

主要属性

属性描述示例
src嵌入页面的 URLsrc="https://example.com"
width宽度width="800"width="100%"
height高度height="600"
name框架名称name="myFrame"
sandbox安全沙箱sandbox="allow-scripts"
allowfullscreen允许全屏allowfullscreen
loading懒加载loading="lazy"
referrerpolicy引荐来源策略referrerpolicy="no-referrer"
srcdoc内联 HTML 内容srcdoc="<p>Hello</p>"
title可访问性标题title="嵌入的地图"

iframe 使用示例

html
<!-- 嵌入外部网页 -->
<iframe 
  src="https://www.example.com" 
  width="100%" 
  height="600"
  title="示例网站"
  loading="lazy"
></iframe>

<!-- 嵌入 YouTube 视频 -->
<iframe 
  width="560" 
  height="315" 
  src="https://www.youtube.com/embed/VIDEO_ID" 
  frameborder="0" 
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
  allowfullscreen
></iframe>

<!-- 嵌入地图 -->
<iframe 
  src="https://www.google.com/maps/embed?pb=..." 
  width="600" 
  height="450" 
  style="border:0;" 
  allowfullscreen="" 
  loading="lazy"
></iframe>

<!-- 使用 srcdoc 嵌入内联内容 -->
<iframe 
  srcdoc="<h1>Hello World</h1><p>这是内联内容</p>" 
  width="400" 
  height="200"
></iframe>

sandbox 属性

sandbox 属性用于限制 iframe 的权限,提升安全性:

html
<!-- 完全限制 -->
<iframe src="untrusted.html" sandbox></iframe>

<!-- 允许脚本执行 -->
<iframe src="content.html" sandbox="allow-scripts"></iframe>

<!-- 允许多个权限 -->
<iframe 
  src="content.html" 
  sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>

sandbox 可选值

  • allow-scripts:允许执行脚本
  • allow-same-origin:允许同源访问
  • allow-forms:允许提交表单
  • allow-popups:允许弹出窗口
  • allow-top-navigation:允许导航顶级窗口
  • allow-modals:允许显示模态对话框
  • allow-pointer-lock:允许指针锁定
  • allow-presentation:允许演示 API

iframe 安全策略

安全威胁与防护

iframe 嵌入第三方内容存在多种安全风险,需要采取相应的防护措施:

code
┌──────────────────────────────────────────────────────┐
│                   安全威胁矩阵                          │
├──────────────┬───────────────┬───────────────────────┤
│    威胁类型   │    风险等级    │      防护措施          │
├──────────────┼───────────────┼───────────────────────┤
│ XSS 攻击     │     高        │ sandbox, CSP          │
│ 点击劫持     │     中        │ X-Frame-Options       │
│ 钓鱼攻击     │     高        │ sandbox, 验证来源      │
│ 数据泄露     │     高        │ sandbox, 同源策略      │
│ 恶意重定向   │     中        │ sandbox               │
└──────────────┴───────────────┴───────────────────────┘

1. sandbox 安全沙箱

sandbox 属性通过限制 iframe 的能力来增强安全性:

html
<!-- 最高安全级别:完全隔离 -->
<iframe 
  src="https://untrusted-site.com" 
  sandbox
></iframe>

<!-- 允许脚本但禁止同源访问 -->
<iframe 
  src="https://example.com/widget" 
  sandbox="allow-scripts"
></iframe>

<!-- 允许表单提交和脚本(适用于登录小部件) -->
<iframe 
  src="https://auth.example.com/login" 
  sandbox="allow-scripts allow-forms allow-same-origin"
></iframe>

<!-- 允许弹出窗口(适用于支付网关) -->
<iframe 
  src="https://payment.example.com" 
  sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
></iframe>

sandbox 权限组合建议

使用场景推荐权限组合说明
嵌入广告sandbox最高安全级别,完全隔离
第三方小部件sandbox="allow-scripts"允许脚本但隔离源
登录/注册表单sandbox="allow-scripts allow-forms allow-same-origin"允许表单提交和脚本
支付网关sandbox="allow-scripts allow-forms allow-popups allow-same-origin"允许弹出窗口
内容展示sandbox="allow-scripts allow-same-origin"允许同源访问

2. Content Security Policy (CSP)

使用 CSP 进一步限制 iframe 的行为:

html
<!-- 在父页面设置 CSP -->
<meta http-equiv="Content-Security-Policy" 
      content="frame-src 'self' https://trusted-site.com;">

<!-- 或在 HTTP 响应头中设置 -->
<!--
Content-Security-Policy: 
  frame-src 'self' https://trusted-site.com;
  frame-ancestors 'self';
-->

CSP 指令说明

指令说明示例
frame-src限制可嵌入的 iframe 来源frame-src 'self' https://youtube.com
frame-ancestors限制当前页面可被谁嵌入frame-ancestors 'self'
child-src限制 worker 和 iframe 来源child-src 'self'

3. X-Frame-Options 防护

防止页面被恶意嵌入到 iframe 中(点击劫持防护):

html
<!-- HTTP 响应头设置 -->
<!-- 
  X-Frame-Options: DENY                    禁止任何嵌入
  X-Frame-Options: SAMEORIGIN              仅允许同源嵌入
  X-Frame-Options: ALLOW-FROM uri          允许特定源嵌入(已废弃)
-->

注意X-Frame-Options 已逐渐被 CSP 的 frame-ancestors 取代。

4. XSS 防护最佳实践

html
<!-- ✅ 推荐:综合使用多种安全策略 -->
<iframe 
  src="https://example.com"
  sandbox="allow-scripts allow-same-origin"
  referrerpolicy="no-referrer"
  loading="lazy"
  title="安全嵌入的内容"
></iframe>

<!-- 父页面设置 CSP -->
<meta http-equiv="Content-Security-Policy" 
      content="frame-src https://example.com;">

防护检查清单

  • 使用 sandbox 属性限制权限
  • 设置合适的 CSP 策略
  • 使用 referrerpolicy 控制引用信息
  • 为 iframe 添加 title 属性(可访问性)
  • 使用 HTTPS 协议
  • 验证和清理 iframe URL
  • 监控 iframe 加载状态和错误

5. 跨域通信安全

使用 postMessage 进行安全的跨域通信:

javascript
// 父页面:发送消息
const iframe = document.getElementById('myIframe');

// 验证 iframe 加载完成
iframe.addEventListener('load', () => {
  // 发送消息到 iframe
  iframe.contentWindow.postMessage(
    { type: 'INIT', data: { userId: 123 } },
    'https://trusted-site.com' // 明确指定目标源
  );
});

// 接收来自 iframe 的消息
window.addEventListener('message', (event) => {
  // 验证消息来源
  if (event.origin !== 'https://trusted-site.com') {
    return; // 拒绝不可信来源的消息
  }
  
  // 验证消息结构
  if (event.data && event.data.type === 'RESPONSE') {
    console.log('收到来自 iframe 的消息:', event.data);
  }
});

// iframe 内部:接收和发送消息
window.addEventListener('message', (event) => {
  // 验证父窗口来源
  if (event.origin !== 'https://parent-site.com') {
    return;
  }
  
  if (event.data.type === 'INIT') {
    // 处理消息
    console.log('收到初始化数据:', event.data.data);
    
    // 发送响应
    event.source.postMessage(
      { type: 'RESPONSE', status: 'success' },
      event.origin
    );
  }
});

6. 常见安全漏洞示例

漏洞 1:未验证的消息来源

javascript
// ❌ 危险:未验证消息来源
window.addEventListener('message', (event) => {
  document.body.innerHTML = event.data; // XSS 漏洞
});

// ✅ 安全:验证消息来源
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted-site.com') {
    return;
  }
  // 处理可信消息
});

漏洞 2:过于宽松的 sandbox

html
<!-- ❌ 危险:允许所有权限 -->
<iframe 
  src="https://untrusted-site.com" 
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation"
></iframe>

<!-- ✅ 安全:最小权限原则 -->
<iframe 
  src="https://untrusted-site.com" 
  sandbox="allow-scripts"
></iframe>

漏洞 3:点击劫持

html
<!-- 攻击者页面 -->
<iframe 
  src="https://victim-site.com/delete" 
  style="opacity: 0; position: absolute; top: 0; left: 0;"
></iframe>
<button>领取奖品</button> <!-- 实际点击的是 iframe 中的删除按钮 -->

<!-- 防护:在 victim-site.com 设置响应头 -->
<!-- X-Frame-Options: SAMEORIGIN -->
<!-- 或 Content-Security-Policy: frame-ancestors 'self' -->

响应式 iframe

html
<!-- 使用 CSS 实现响应式 -->
<div class="video-container">
  <iframe 
    src="https://www.youtube.com/embed/VIDEO_ID" 
    frameborder="0" 
    allowfullscreen
  ></iframe>
</div>

<style>
  .video-container {
    position: relative;
    padding-bottom: 56.25%; /* 16:9 宽高比 */
    height: 0;
    overflow: hidden;
  }
  .video-container iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
</style>

现代媒体 API

Picture-in-Picture API(画中画)

Picture-in-Picture(PiP)API 允许视频在浮动窗口中播放,用户可以在其他页面或应用中继续观看视频。

基本用法:

html
<video id="myVideo" src="movie.mp4" controls></video>
<button id="pipBtn">切换画中画</button>

<script>
const video = document.getElementById('myVideo');
const btn = document.getElementById('pipBtn');

btn.addEventListener('click', async () => {
  try {
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      await video.requestPictureInPicture();
    }
  } catch (err) {
    console.error('画中画失败:', err);
  }
});

video.addEventListener('enterpictureinpicture', () => {
  btn.textContent = '退出画中画';
});

video.addEventListener('leavepictureinpicture', () => {
  btn.textContent = '切换画中画';
});
</script>

检测支持:

javascript
if ('pictureInPictureEnabled' in document) {
  console.log('浏览器支持画中画');
}

关键属性与事件:

属性 / 事件说明
document.pictureInPictureEnabled当前页面是否允许画中画
document.pictureInPictureElement当前处于画中画模式的元素(无则为 null
video.requestPictureInPicture()请求进入画中画,返回 Promise
document.exitPictureInPicture()退出画中画,返回 Promise
enterpictureinpicture 事件进入画中画时触发
leavepictureinpicture 事件退出画中画时触发

控制画中画窗口大小:

javascript
video.addEventListener('enterpictureinpicture', (e) => {
  const pipWindow = e.pictureInPictureWindow;
  console.log('窗口宽度:', pipWindow.width);
  console.log('窗口高度:', pipWindow.height);

  pipWindow.addEventListener('resize', () => {
    console.log('窗口大小变化:', pipWindow.width, pipWindow.height);
  });
});
TIP

画中画窗口始终置顶,用户可拖拽和调整大小。适用于视频教程、会议、直播等需要同时操作其他内容的场景。

Media Session API

Media Session API 允许网页自定义系统级媒体控制(如通知栏、锁屏界面的播放控制按钮和元数据),让用户在浏览器外也能操控媒体播放。

设置媒体元数据:

javascript
if ('mediaSession' in navigator) {
  navigator.mediaSession.metadata = new MediaMetadata({
    title: '春风十里',
    artist: '鹿先森乐队',
    album: '所有的酒都不如你',
    artwork: [
      { src: 'cover-96.png',   sizes: '96x96',   type: 'image/png' },
      { src: 'cover-128.png',  sizes: '128x128',  type: 'image/png' },
      { src: 'cover-256.png',  sizes: '256x256',  type: 'image/png' },
      { src: 'cover-512.png',  sizes: '512x512',  type: 'image/png' },
    ]
  });
}

设置播放控制处理程序:

javascript
navigator.mediaSession.setActionHandler('play', () => {
  video.play();
});

navigator.mediaSession.setActionHandler('pause', () => {
  video.pause();
});

navigator.mediaSession.setActionHandler('seekbackward', (details) => {
  video.currentTime = Math.max(0, video.currentTime - (details.seekOffset || 10));
});

navigator.mediaSession.setActionHandler('seekforward', (details) => {
  video.currentTime = Math.min(video.duration, video.currentTime + (details.seekOffset || 10));
});

navigator.mediaSession.setActionHandler('previoustrack', () => {
  playPreviousTrack();
});

navigator.mediaSession.setActionHandler('nexttrack', () => {
  playNextTrack();
});

更新播放状态:

javascript
video.addEventListener('play', () => {
  navigator.mediaSession.playbackState = 'playing';
});

video.addEventListener('pause', () => {
  navigator.mediaSession.playbackState = 'paused';
});
TIP

Media Session API 在移动端尤为实用——用户锁屏或切换应用后,仍可通过系统通知栏控制播放、查看曲目信息。桌面端浏览器也会在媒体通知中显示这些信息。

性能优化

1. 懒加载媒体

html
<!-- 音频懒加载 -->
<audio src="audio.mp3" controls preload="none"></audio>

<!-- 视频懒加载 -->
<video src="video.mp4" controls preload="metadata" loading="lazy"></video>

<!-- iframe 懒加载 -->
<iframe src="https://example.com" loading="lazy"></iframe>

2. 使用合适的 preload 值

html
<!-- 不预加载(节省带宽) -->
<audio src="audio.mp3" preload="none"></audio>

<!-- 仅预加载元数据(推荐) -->
<video src="video.mp4" preload="metadata"></video>

<!-- 自动预加载(谨慎使用) -->
<audio src="background.mp3" preload="auto"></audio>

3. 提供多种格式

html
<!-- 提供多种格式,浏览器选择最佳 -->
<video controls>
  <source src="video.webm" type="video/webm">
  <source src="video.mp4" type="video/mp4">
  <source src="video.ogv" type="video/ogg">
</video>

4. 使用 CDN

html
<!-- 使用 CDN 加速媒体加载 -->
<video src="https://cdn.example.com/video.mp4" controls></video>

5. 压缩媒体文件

  • 使用适当的编码格式
  • 压缩音频/视频文件大小
  • 考虑使用 WebP/WebM 等现代格式

浏览器兼容性

多媒体元素支持

特性ChromeFirefoxSafariEdgeIE11
<audio>✅ 3+✅ 3.5+✅ 3.1+✅ 12+✅ 9+
<video>✅ 3+✅ 3.5+✅ 3.1+✅ 12+✅ 9+
<track>✅ 23+✅ 31+✅ 6+✅ 12+✅ 10+
Web Audio API✅ 35+✅ 25+✅ 14.1+✅ 12+
MediaRecorder✅ 47+✅ 25+✅ 14.1+✅ 79+
Picture-in-Picture✅ 70+✅ 13+✅ 79+
MediaStream✅ 21+✅ 17+✅ 11+✅ 12+

音频格式兼容性

格式ChromeFirefoxSafariEdgeIE11
MP3
WAV
OGG
AAC
WebM Audio

视频格式兼容性

格式ChromeFirefoxSafariEdgeIE11
MP4 (H.264)
WebM (VP8/VP9)
Ogg (Theora)
MOV

特性检测

在使用高级功能前,建议进行特性检测:

javascript
// 检测音频格式支持
function checkAudioSupport() {
  const audio = document.createElement('audio');
  
  return {
    mp3: audio.canPlayType('audio/mpeg') !== '',
    wav: audio.canPlayType('audio/wav') !== '',
    ogg: audio.canPlayType('audio/ogg') !== '',
    aac: audio.canPlayType('audio/aac') !== ''
  };
}

// 检测视频格式支持
function checkVideoSupport() {
  const video = document.createElement('video');
  
  return {
    mp4: video.canPlayType('video/mp4') !== '',
    webm: video.canPlayType('video/webm') !== '',
    ogg: video.canPlayType('video/ogg') !== ''
  };
}

// 检测 Web Audio API
const hasWebAudio = !!(window.AudioContext || window.webkitAudioContext);

// 检测 MediaRecorder
const hasMediaRecorder = typeof MediaRecorder !== 'undefined';

// 检测画中画
const hasPiP = document.pictureInPictureEnabled;

// 检测摄像头访问
async function checkCameraSupport() {
  try {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.some(device => device.kind === 'videoinput');
  } catch {
    return false;
  }
}

Polyfill 方案

1. HTML5 媒体播放器

对于不支持 HTML5 媒体元素的旧浏览器,可以使用 Flash 或其他技术的 polyfill:

html
<!-- 使用 video.js 作为后备方案 -->
<link href="https://vjs.zencdn.net/7.20.3/video-js.css" rel="stylesheet">
<script src="https://vjs.zencdn.net/7.20.3/video.min.js"></script>

<video id="my-video" class="video-js" controls preload="auto" 
       poster="poster.jpg" data-setup="{}">
  <source src="video.mp4" type="video/mp4">
  <source src="video.webm" type="video/webm">
  <p class="vjs-no-js">
    您的浏览器不支持视频播放,请<a href="video.mp4">下载视频</a>
  </p>
</video>

2. Web Audio API Polyfill

javascript
// 为旧浏览器添加 Web Audio API 支持
if (!window.AudioContext) {
  window.AudioContext = window.webkitAudioContext || 
                        window.mozAudioContext || 
                        window.oAudioContext || 
                        window.msAudioContext;
}

3. MediaRecorder Polyfill

html
<!-- 使用第三方库提供 MediaRecorder 支持 -->
<script src="https://cdn.jsdelivr.net/npm/audio-recorder-polyfill@0.4.1/dist/index.min.js"></script>
<script>
  if (typeof MediaRecorder === 'undefined') {
    window.MediaRecorder = AudioRecorderPolyfill;
  }
</script>

4. 自定义降级方案

html
<video controls>
  <!-- 现代浏览器首选 WebM -->
  <source src="video.webm" type="video/webm">
  
  <!-- 大多数浏览器支持 MP4 -->
  <source src="video.mp4" type="video/mp4">
  
  <!-- Flash 后备(已过时,不推荐) -->
  <object data="video.swf" type="application/x-shockwave-flash">
    <param name="movie" value="video.swf">
    
    <!-- 最终降级:显示下载链接 -->
    <p>您的浏览器不支持视频播放</p>
    <a href="video.mp4">下载视频</a>
  </object>
</video>

功能降级策略

javascript
class MediaPlayer {
  constructor(options) {
    this.options = options;
    this.player = null;
  }

  init() {
    // 优先使用原生 HTML5
    if (this.supportsHTML5()) {
      this.createHTML5Player();
    }
    // 降级到第三方库
    else if (this.supportsFlash()) {
      this.createFlashPlayer();
    }
    // 最终降级:显示下载链接
    else {
      this.showDownloadLink();
    }
  }

  supportsHTML5() {
    const video = document.createElement('video');
    return video.canPlayType && video.canPlayType('video/mp4') !== '';
  }

  supportsFlash() {
    return navigator.plugins.namedItem('Shockwave Flash') !== null;
  }

  createHTML5Player() {
    this.player = document.createElement('video');
    this.player.src = this.options.src;
    this.player.controls = true;
    document.getElementById(this.options.container).appendChild(this.player);
  }

  showDownloadLink() {
    const link = document.createElement('a');
    link.href = this.options.src;
    link.textContent = '下载媒体文件';
    document.getElementById(this.options.container).appendChild(link);
  }
}

可访问性

音频可访问性

html
<!-- 提供文本替代 -->
<audio controls>
  <source src="audio.mp3" type="audio/mpeg">
  您的浏览器不支持音频播放。
  <a href="transcript.txt">查看文字稿</a>
</audio>

视频可访问性

html
<!-- 提供字幕和描述 -->
<video controls>
  <source src="video.mp4" type="video/mp4">
  <track 
    kind="captions" 
    src="captions.vtt" 
    srclang="zh-CN" 
    label="中文字幕" 
    default
  >
  <track 
    kind="descriptions" 
    src="descriptions.vtt" 
    srclang="zh-CN"
  >
  您的浏览器不支持视频播放。
</video>

iframe 可访问性

html
<!-- 始终提供 title 属性 -->
<iframe 
  src="https://example.com" 
  title="嵌入的示例网站"
></iframe>

最佳实践

1. 始终提供替代内容

html
<!-- ✅ 推荐 -->
<video controls>
  <source src="video.mp4" type="video/mp4">
  您的浏览器不支持视频播放。
</video>

<!-- ❌ 避免 -->
<video src="video.mp4" controls></video>

2. 使用语义化的 track 元素

html
<video controls>
  <source src="video.mp4" type="video/mp4">
  <track 
    kind="subtitles" 
    src="subtitles.vtt" 
    srclang="zh-CN" 
    label="中文字幕" 
    default
  >
</video>

3. 处理自动播放限制

javascript
// 处理自动播放失败
const video = document.getElementById('myVideo');
const playPromise = video.play();

if (playPromise !== undefined) {
  playPromise
    .then(() => {
      // 自动播放成功
    })
    .catch(error => {
      // 自动播放被阻止,需要用户交互
      console.log('自动播放被阻止');
    });
}

4. 优化移动端体验

html
<!-- 移动端内联播放 -->
<video 
  src="video.mp4" 
  controls 
  playsinline
  webkit-playsinline
></video>

5. 使用适当的媒体格式

  • 音频:优先使用 MP3(兼容性最好)或 WebM(文件更小)
  • 视频:优先使用 MP4(H.264)或 WebM(VP9)

6. 错误处理

基础错误处理

javascript
const video = document.getElementById('myVideo');

video.addEventListener('error', (e) => {
  const error = video.error;
  if (error) {
    switch (error.code) {
      case error.MEDIA_ERR_ABORTED:
        console.error('用户中止加载');
        break;
      case error.MEDIA_ERR_NETWORK:
        console.error('网络错误');
        break;
      case error.MEDIA_ERR_DECODE:
        console.error('解码错误');
        break;
      case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
        console.error('格式不支持');
        break;
    }
  }
});

高级错误处理

javascript
class MediaErrorHandler {
  constructor(mediaElement) {
    this.media = mediaElement;
    this.setupErrorHandlers();
  }

  setupErrorHandlers() {
    // 加载错误
    this.media.addEventListener('error', (e) => {
      this.handleError(this.media.error);
    });

    // 网络状态变化
    this.media.addEventListener('stalled', () => {
      console.warn('媒体加载停滞');
      this.handleStalled();
    });

    // 等待数据
    this.media.addEventListener('waiting', () => {
      console.log('缓冲中...');
      this.showBufferingIndicator();
    });

    // 加载超时
    this.setupLoadTimeout();
  }

  handleError(error) {
    if (!error) return;

    const errorMap = {
      [MediaError.MEDIA_ERR_ABORTED]: {
        message: '用户中止加载',
        action: 'retry'
      },
      [MediaError.MEDIA_ERR_NETWORK]: {
        message: '网络错误',
        action: 'retry'
      },
      [MediaError.MEDIA_ERR_DECODE]: {
        message: '解码错误',
        action: 'fallback'
      },
      [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: {
        message: '格式不支持',
        action: 'fallback'
      }
    };

    const errorInfo = errorMap[error.code];
    console.error(`媒体错误: ${errorInfo.message}`, error);

    // 根据错误类型采取不同措施
    switch (errorInfo.action) {
      case 'retry':
        this.retryLoad();
        break;
      case 'fallback':
        this.loadFallback();
        break;
      default:
        this.showErrorMessage(errorInfo.message);
    }
  }

  handleStalled() {
    // 尝试恢复加载
    setTimeout(() => {
      if (this.media.networkState === HTMLMediaElement.NETWORK_LOADING) {
        this.media.load();
      }
    }, 3000);
  }

  retryLoad() {
    // 重试加载
    const src = this.media.src;
    this.media.src = '';
    this.media.src = src;
    this.media.load();
  }

  loadFallback() {
    // 尝试备用格式
    const sources = this.media.querySelectorAll('source');
    const currentSource = Array.from(sources).find(
      s => s.src === this.media.currentSrc
    );
    
    if (currentSource) {
      currentSource.remove();
      this.media.load();
    }
  }

  showBufferingIndicator() {
    // 显示缓冲指示器
    const indicator = document.createElement('div');
    indicator.className = 'buffering-indicator';
    indicator.textContent = '缓冲中...';
    this.media.parentNode.appendChild(indicator);

    this.media.addEventListener('playing', () => {
      indicator.remove();
    }, { once: true });
  }

  showErrorMessage(message) {
    // 显示错误信息
    const errorDiv = document.createElement('div');
    errorDiv.className = 'media-error';
    errorDiv.innerHTML = `
      <p>播放失败: ${message}</p>
      <button onclick="location.reload()">刷新页面</button>
    `;
    this.media.parentNode.replaceChild(errorDiv, this.media);
  }

  setupLoadTimeout() {
    let loadTimeout;

    this.media.addEventListener('loadstart', () => {
      loadTimeout = setTimeout(() => {
        if (this.media.readyState === 0) {
          console.error('加载超时');
          this.handleError({ code: MediaError.MEDIA_ERR_NETWORK });
        }
      }, 30000); // 30秒超时
    });

    this.media.addEventListener('loadedmetadata', () => {
      clearTimeout(loadTimeout);
    });
  }
}

// 使用
const video = document.getElementById('myVideo');
new MediaErrorHandler(video);

调试工具函数

javascript
// 媒体状态调试器
class MediaDebugger {
  constructor(mediaElement) {
    this.media = mediaElement;
    this.setupLogging();
  }

  setupLogging() {
    // 记录所有事件
    const events = [
      'loadstart', 'progress', 'suspend', 'abort', 'error',
      'emptied', 'stalled', 'loadedmetadata', 'loadeddata',
      'canplay', 'canplaythrough', 'playing', 'waiting',
      'seeking', 'seeked', 'ended', 'durationchange',
      'timeupdate', 'play', 'pause', 'ratechange',
      'resize', 'volumechange'
    ];

    events.forEach(event => {
      this.media.addEventListener(event, (e) => {
        console.log(`[Media Event] ${event}`, {
          currentTime: this.media.currentTime,
          duration: this.media.duration,
          readyState: this.getReadyState(),
          networkState: this.getNetworkState(),
          buffered: this.getBufferedRanges()
        });
      });
    });
  }

  getReadyState() {
    const states = [
      'HAVE_NOTHING',
      'HAVE_METADATA',
      'HAVE_CURRENT_DATA',
      'HAVE_FUTURE_DATA',
      'HAVE_ENOUGH_DATA'
    ];
    return states[this.media.readyState];
  }

  getNetworkState() {
    const states = [
      'NETWORK_EMPTY',
      'NETWORK_IDLE',
      'NETWORK_LOADING',
      'NETWORK_NO_SOURCE'
    ];
    return states[this.media.networkState];
  }

  getBufferedRanges() {
    const ranges = [];
    for (let i = 0; i < this.media.buffered.length; i++) {
      ranges.push({
        start: this.media.buffered.start(i),
        end: this.media.buffered.end(i)
      });
    }
    return ranges;
  }

  // 导出媒体信息
  getMediaInfo() {
    return {
      src: this.media.currentSrc,
      duration: this.media.duration,
      currentTime: this.media.currentTime,
      volume: this.media.volume,
      muted: this.media.muted,
      playbackRate: this.media.playbackRate,
      readyState: this.getReadyState(),
      networkState: this.getNetworkState(),
      videoWidth: this.media.videoWidth,
      videoHeight: this.media.videoHeight,
      textTracks: this.media.textTracks.length,
      buffered: this.getBufferedRanges()
    };
  }

  // 性能监控
  monitorPerformance() {
    const startTime = performance.now();
    
    this.media.addEventListener('loadedmetadata', () => {
      const loadTime = performance.now() - startTime;
      console.log(`元数据加载时间: ${loadTime}ms`);
    });

    this.media.addEventListener('canplay', () => {
      const readyTime = performance.now() - startTime;
      console.log(`可播放时间: ${readyTime}ms`);
    });
  }
}

// 使用
const video = document.getElementById('myVideo');
const debugger = new MediaDebugger(video);
debugger.monitorPerformance();

// 在控制台查看媒体信息
console.log(debugger.getMediaInfo());

性能监控面板

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>媒体性能监控</title>
  <style>
    .monitor-panel {
      position: fixed;
      top: 10px;
      right: 10px;
      background: rgba(0, 0, 0, 0.8);
      color: #fff;
      padding: 15px;
      border-radius: 8px;
      font-family: monospace;
      font-size: 12px;
      max-width: 300px;
      z-index: 9999;
    }
    .monitor-panel h3 {
      margin: 0 0 10px 0;
      font-size: 14px;
    }
    .monitor-panel .stat {
      margin: 5px 0;
    }
    .monitor-panel .label {
      color: #888;
    }
    .monitor-panel .value {
      color: #0f0;
      float: right;
    }
  </style>
</head>
<body>
  <video id="video" src="video.mp4" controls width="600"></video>
  
  <div class="monitor-panel">
    <h3>媒体性能监控</h3>
    <div class="stat">
      <span class="label">当前时间:</span>
      <span class="value" id="currentTime">0.00s</span>
    </div>
    <div class="stat">
      <span class="label">总时长:</span>
      <span class="value" id="duration">0.00s</span>
    </div>
    <div class="stat">
      <span class="label">缓冲进度:</span>
      <span class="value" id="buffered">0%</span>
    </div>
    <div class="stat">
      <span class="label">网络状态:</span>
      <span class="value" id="networkState">-</span>
    </div>
    <div class="stat">
      <span class="label">就绪状态:</span>
      <span class="value" id="readyState">-</span>
    </div>
    <div class="stat">
      <span class="label">播放速率:</span>
      <span class="value" id="playbackRate">1.0x</span>
    </div>
  </div>

  <script>
    const video = document.getElementById('video');
    const panel = {
      currentTime: document.getElementById('currentTime'),
      duration: document.getElementById('duration'),
      buffered: document.getElementById('buffered'),
      networkState: document.getElementById('networkState'),
      readyState: document.getElementById('readyState'),
      playbackRate: document.getElementById('playbackRate')
    };

    function updatePanel() {
      panel.currentTime.textContent = video.currentTime.toFixed(2) + 's';
      panel.duration.textContent = (video.duration || 0).toFixed(2) + 's';
      
      // 计算缓冲进度
      if (video.buffered.length > 0) {
        const bufferedEnd = video.buffered.end(video.buffered.length - 1);
        const bufferedPercent = (bufferedEnd / video.duration * 100).toFixed(1);
        panel.buffered.textContent = bufferedPercent + '%';
      }

      panel.networkState.textContent = [
        'NETWORK_EMPTY',
        'NETWORK_IDLE',
        'NETWORK_LOADING',
        'NETWORK_NO_SOURCE'
      ][video.networkState];

      panel.readyState.textContent = [
        'HAVE_NOTHING',
        'HAVE_METADATA',
        'HAVE_CURRENT_DATA',
        'HAVE_FUTURE_DATA',
        'HAVE_ENOUGH_DATA'
      ][video.readyState];

      panel.playbackRate.textContent = video.playbackRate + 'x';
    }

    // 定期更新面板
    setInterval(updatePanel, 100);

    // 事件触发时更新
    video.addEventListener('timeupdate progress loadstart loadedmetadata canplay', updatePanel);
  </script>
</body>
</html>

常见问题排查

问题 1:音频/视频无法播放

可能原因

  1. 格式不支持
  2. 文件路径错误
  3. CORS 跨域问题
  4. 文件损坏

解决方案

html
<!-- 提供多种格式 -->
<audio controls>
  <source src="audio.mp3" type="audio/mpeg">
  <source src="audio.ogg" type="audio/ogg">
  您的浏览器不支持音频播放。
</audio>

<!-- 检查控制台错误 -->
<!-- 使用开发者工具 Network 标签检查文件加载 -->

问题 2:自动播放不工作

原因

浏览器阻止自动播放

解决方案

html
<!-- 使用 muted 属性 -->
<video src="video.mp4" autoplay muted></video>

<!-- 或通过用户交互触发 -->
<button onclick="video.play()">播放视频</button>

问题 3:移动端视频全屏播放

原因

iOS Safari 默认全屏播放

解决方案

html
<video 
  src="video.mp4" 
  controls 
  playsinline
  webkit-playsinline
></video>

问题 4:字幕不显示

可能原因

  1. 文件路径错误
  2. CORS 问题
  3. 文件格式错误
  4. 需要在服务器环境运行

解决方案

html
<!-- 确保在服务器环境运行 -->
<!-- 检查字幕文件路径 -->
<video controls>
  <source src="video.mp4" type="video/mp4">
  <track 
    src="./subtitles.vtt" 
    kind="subtitles" 
    srclang="zh-CN" 
    label="中文字幕" 
    default
  >
</video>

问题 5:iframe 内容被阻止

原因

X-Frame-Options 或 CSP 限制

解决方案

html
<!-- 检查目标网站是否允许嵌入 -->
<!-- 某些网站(如 YouTube)提供专门的嵌入 URL -->
<iframe 
  src="https://www.youtube.com/embed/VIDEO_ID" 
  allowfullscreen
></iframe>

问题 6:PDF 无法显示

原因

浏览器不支持或 CORS 问题

解决方案

html
<!-- 使用 object 并提供替代内容 -->
<object 
  data="document.pdf" 
  type="application/pdf" 
  width="100%" 
  height="600px"
>
  <p>
    您的浏览器不支持PDF查看,
    <a href="document.pdf">点击下载PDF文件</a>。
  </p>
</object>

<!-- 或使用 Google Docs Viewer -->
<iframe 
  src="https://docs.google.com/viewer?url=YOUR_PDF_URL&embedded=true" 
  width="100%" 
  height="600px"
></iframe>

调试技巧

javascript
// 检查媒体元素状态
const video = document.getElementById('myVideo');

console.log('播放状态:', video.paused ? '暂停' : '播放');
console.log('当前时间:', video.currentTime);
console.log('总时长:', video.duration);
console.log('音量:', video.volume);
console.log('就绪状态:', video.readyState);
console.log('网络状态:', video.networkState);

// 监听所有事件
const events = [
  'loadstart', 'progress', 'suspend', 'abort', 'error',
  'emptied', 'stalled', 'play', 'pause', 'loadedmetadata',
  'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough',
  'seeking', 'seeked', 'timeupdate', 'ended', 'ratechange',
  'durationchange', 'volumechange'
];

events.forEach(event => {
  video.addEventListener(event, () => {
    console.log(`事件: ${event}`);
  });
});

总结

HTML5 多媒体元素为现代 Web 应用提供了强大而灵活的媒体处理能力。通过合理使用这些元素和相关 API,开发者可以构建出功能丰富、性能优良的多媒体应用。

核心要点回顾

1. 媒体元素选择

需求场景推荐元素说明
音频播放<audio>原生支持,API 完善
视频播放<video>支持多种格式和字幕
嵌入外部页面<iframe>注意安全策略
嵌入 PDF 等文档<object>提供替代内容

2. 最佳实践清单

性能优化

  • 使用 preload="metadata" 减少初始加载
  • 使用 loading="lazy" 实现懒加载
  • 提供多种格式以兼容不同浏览器
  • 使用 CDN 加速媒体文件加载
  • 压缩媒体文件,使用现代格式

安全防护

  • 使用 sandbox 属性限制 iframe 权限
  • 设置合适的 CSP 策略
  • 验证 postMessage 的消息来源
  • 使用 HTTPS 协议
  • 避免 XSS 攻击,不直接插入用户内容

可访问性

  • 为媒体添加字幕和说明
  • 使用 <track> 元素提供文本轨道
  • 为 iframe 添加 title 属性
  • 提供替代内容和下载链接
  • 支持键盘导航

错误处理

  • 监听 error 事件并妥善处理
  • 提供格式降级方案
  • 实现加载超时和重试机制
  • 显示友好的错误提示
  • 记录错误日志便于调试

3. 技术发展趋势

  1. AV1 编码:新一代视频编码,压缩效率更高
  2. WebCodecs API:底层音视频编解码能力
  3. Media Session API:系统级媒体控制集成
  4. WebNN API:基于神经网络的媒体处理
  5. WebTransport:低延迟流媒体传输

开发建议

初学者

  1. 从基础的 <audio><video> 开始
  2. 熟悉常用属性和事件
  3. 学习使用 JavaScript 控制媒体播放
  4. 了解格式兼容性问题

进阶开发者

  1. 掌握 Web Audio API 音频处理
  2. 学习 Canvas 视频处理和截图
  3. 实现自定义媒体播放器
  4. 优化移动端播放体验

高级开发者

  1. 深入理解媒体编解码技术
  2. 实现自适应码率流媒体
  3. 开发实时音视频应用
  4. 性能监控和优化

参考资料

官方文档:

开源项目:

工具资源:

学习资源:

补充示例

<h4>051-video-complete-demo.html</h4>
html
<!DOCTYPE html>
<!--
  来源:基础知识/9-嵌入多媒体元素.md
  演示:video 元素完整属性、source 多源回退、track 字幕轨道
-->
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Video 完整属性与字幕演示</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, sans-serif; padding: 30px; background: #f0f2f5; max-width: 900px; margin: 0 auto; }
    h1 { color: #1a1a1a; margin-bottom: 25px; }
    .section { background: white; padding: 25px; margin-bottom: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.06); }
    h2 { color: #333; font-size: 17px; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 8px; }

    .video-container {
      position: relative; background: #000; border-radius: 10px; overflow: hidden;
      max-width: 700px; aspect-ratio: 16/9;
    }
    video {
      width: 100%; height: 100%; display: block;
    }

    /* 自定义控制栏 */
    .custom-controls {
      position: absolute; bottom: 0; left: 0; right: 0;
      background: linear-gradient(transparent, rgba(0,0,0,0.8));
      padding: 15px 12px 10px; display: flex; align-items: center; gap: 10px;
      opacity: 0; transition: opacity 0.3s;
    }
    .video-container:hover .custom-controls { opacity: 1; }

    .ctrl-btn {
      background: none; border: none; color: white; cursor: pointer;
      font-size: 18px; padding: 4px 8px; border-radius: 4px; transition: background 0.2s;
    }
    .ctrl-btn:hover { background: rgba(255,255,255,0.2); }

    .progress-wrap {
      flex: 1; height: 4px; background: rgba(255,255,255,0.3); border-radius: 2px;
      cursor: pointer; position: relative;
    }
    .progress-bar {
      height: 100%; background: #3498db; border-radius: 2px; width: 0%;
      position: relative;
    }
    .progress-bar::after {
      content: ''; position: absolute; right: -6px; top: -3px;
      width: 10px; height: 10px; background: white; border-radius: 50%;
    }

    .time-display { color: white; font-size: 12px; font-variant-numeric: tabular-nums; min-width: 80px; text-align: right; }

    .volume-wrap { display: flex; align-items: center; gap: 4px; }
    .volume-slider { width: 60px; height: 4px; -webkit-appearance: none; background: rgba(255,255,255,0.3); border-radius: 2px; outline: none; }
    .volume-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; background: white; border-radius: 50%; cursor: pointer; }

    table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 15px 0; }
    th, td { padding: 8px 12px; text-align: left; border: 1px solid #dee2e6; }
    th { background: #34495e; color: white; }

    .attr-tag { display: inline-block; background: #e3f2fd; color: #1976d2; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-family: monospace; }
    button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; background: #3498db; color: white; }
    button:hover { background: #2980b9; }

    pre { background: #263238; color: #eceff1; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; line-height: 1.6; }

    .track-list { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; }
    .track-btn {
      padding: 6px 14px; border: 2px solid #ddd; background: white; border-radius: 20px;
      cursor: pointer; font-size: 12px; transition: all 0.2s;
    }
    .track-btn.active { border-color: #3498db; background: #ebf5fb; color: #3498db; font-weight: bold; }

    .info-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
    .info-card { background: #f8f9fa; padding: 12px; border-radius: 8px; font-size: 13px; }
    .info-card label { color: #888; font-size: 11px; display: block; margin-bottom: 3px; }
    .info-card value { font-weight: bold; color: #333; }
  </style>
</head>
<body>
  <h1>🎬 Video 完整属性与字幕轨道演示</h1>

  <!-- ========== Video 完整属性 ========== -->
  <div class="section">
    <h2>1. Video 元素 — 完整属性展示</h2>
    <p>以下视频展示了 &lt;video&gt; 的核心属性。由于没有实际视频文件,使用 Canvas 绘制占位画面:</p>

    <div class="video-container" id="main-video-container">
      <canvas id="video-canvas" width="1280" height="720"></canvas>
      <!-- 实际使用时替换为:
      <video id="demo-video" controls crossorigin="anonymous"
             poster="poster.jpg" preload="metadata"
             playsinline width="1280" height="720">
        <source src="video.mp4" type="video/mp4">
        <source src="video.webm" type="video/webm">
        您的浏览器不支持 video 元素。
      </video>
      -->

      <!-- 自定义控制栏 -->
      <div class="custom-controls">
        <button class="ctrl-btn" id="btn-play-pause" onclick="togglePlay()">▶</button>
        <button class="ctrl-btn" onclick="skip(-10)">⏪</button>
        <button class="ctrl-btn" onclick="skip(10)">⏩</button>
        <div class="progress-wrap" id="progress-wrap" onclick="seek(event)">
          <div class="progress-bar" id="progress-bar"></div>
        </div>
        <span class="time-display" id="time-display">0:00 / 0:00</span>
        <div class="volume-wrap">
          <button class="ctrl-btn" id="btn-mute" onclick="toggleMute()">🔊</button>
          <input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.1" value="1" oninput="setVolume(this.value)">
        </div>
        <button class="ctrl-btn" id="btn-fullscreen" onclick="toggleFullscreen()">⛶</button>
      </div>

      <!-- 字幕选择 -->
      <div style="position:absolute;top:10px;right:10px;">
        <select id="track-select" onchange="switchTrack(this.value)"
                style="padding:4px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.3);background:rgba(0,0,0,0.5);color:white;font-size:12px;">
          <option value="-1">关闭字幕</option>
          <option value="0" selected>中文字幕</option>
          <option value="1">English Subtitles</option>
        </select>
      </div>
    </div>

    <div class="track-list" style="margin-top:12px;">
      <button class="track-btn active" onclick="setPlaybackRate(1)">正常 1x</button>
      <button class="track-btn" onclick="setPlaybackRate(1.5)">快进 1.5x</button>
      <button class="track-btn" onclick="setPlaybackRate(2)">2x 倍速</button>
      <button class="track-btn" onclick="setPlaybackRate(0.5)">慢速 0.5x</button>
      <button class="track-btn" onclick="toggleLoop()">循环播放</button>
      <picture style="margin-left:auto;">
    </div>

    <h3 style="margin:18px 0 12px;">Video 属性速查表</h3>
    <table>
      <thead>
        <tr><th>属性</th><th>类型</th><th>默认值</th><th>说明</th></tr>
      </thead>
      <tbody>
        <tr><td><code>src</code></td><td>URL</td><td>-</td><td>视频源地址</td></tr>
        <tr><td><code>controls</code></td><td>布尔</td><td>false</td><td>显示浏览器原生控制条</td></tr>
        <tr><td><code>autoplay</code></td><td>布尔</td><td>false</td><td>自动播放(需 muted 或用户交互)</td></tr>
        <tr><td><code>muted</code></td><td>布尔</td><td>false</td><td>静音(配合 autoplay 使用)</td></tr>
        <tr><td><code>loop</code></td><td>布尔</td><td>false</td><td>循环播放</td></tr>
        <tr><td><code>poster</code></td><td>URL</td><td>-</td><td>封面图片地址</td></tr>
        <tr><td><code>preload</code></td><td>关键字</td><td>auto</td><td>none/metadata/auto 预加载策略</td></tr>
        <tr><td><code>playsinline</code></td><td>布尔</td><td>false</td><td>移动端内联播放(不全屏)</td></tr>
        <tr><td><code>width/height</code></td><td>像素</td><td>-</td><td>显示尺寸</td></tr>
        <tr><td><code>crossorigin</code></td><td>关键字</td><td>-</td><td>CORS 跨域设置</td></tr>
      </tbody>
    </table>
  </div>

  <!-- ========== Source 多源回退 ========== -->
  <div class="section">
    <h2>2. Source 多源回退机制</h2>
    <p>不同浏览器支持不同的视频格式,&lt;source&gt; 让浏览器自动选择最佳格式:</p>

    <pre>&lt;video controls poster="cover.jpg"&gt;
  &lt;!-- 浏览器按顺序尝试,选第一个支持的格式 --&gt;
  &lt;source src="video.mp4" type="video/mp4" codecs="avc1.42E01E,mp4a.40.2"&gt;
    ⭐ MP4 (H.264) — 兼容性最好,Safari/Chrome/Firefox/Edge 全支持

  &lt;source src="video.webm" type="video/webm" codecs="vp9,vorbis"&gt;
    🌐 WebM (VP9) — 开源,Chrome/Firefox 支持,质量更好

  &lt;source src="video.ogg" type="video/ogg" codecs="theora,vorbis"&gt;
    📼 Ogg Theora — 老格式,仅 Firefox

  &lt;!-- 全都不支持时显示 --&gt;
  您的浏览器不支持 HTML5 视频,请
  &lt;a href="video.mp4"&gt;下载视频&lt;/a&gt;。
&lt;/video&gt;</pre>

    <table>
      <thead>
        <tr><th>格式</th><th>容器</th><th>编码</th><th>Chrome</th><th>Firefox</th><th>Safari</th><th>Edge</th></tr>
      </thead>
      <tbody>
        <tr><td>MP4</td><td>.mp4</td><td>H.264 + AAC</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td></tr>
        <tr><td>WebM</td><td>.webm</td><td>VP9 + Opus</td><td>✅</td><td>✅</td><td>⚠️ 部分</td><td>✅</td></tr>
        <tr><td>Ogg</td><td>.ogg</td><td>Theora + Vorbis</td><td>✅</td><td>✅</td><td>❌</td><td>✅</td></tr>
      </tbody>
    </table>
  </div>

  <!-- ========== Track 字幕轨道 ========== -->
  <div class="section">
    <h2>3. Track 字幕轨道(WebVTT 格式)</h2>

    <pre>&lt;video controls&gt;
  &lt;source src="movie.mp4" type="video/mp4"&gt;

  &lt;!-- 中文字幕 --&gt;
  &lt;track kind="subtitles" src="subs-zh.vtt" srclang="zh" label="中文"
         default&gt;

  &lt;!-- 英文字幕 --&gt;
  &lt;track kind="subtitles" src="subs-en.vtt" srclang="en" label="English"&gt;

  &lt;!-- 字幕描述(供屏幕阅读器)--&gt;
  &lt;track kind="descriptions" src="desc.vtt" srclang="zh" label="描述"&gt;

  &lt;!-- 章节标记 --&gt;
  &lt;track kind="chapters" src="chapters.vtt" srclang="zh" label="章节"&gt;
&lt;/video&gt;</pre>

    <h3 style="margin:18px 0 12px;">WebVTT 文件格式示例 (subs-zh.vtt)</h3>
    <pre style="font-size:11px;">WEBVTT

00:00:01.000 --> 00:00:04.000
欢迎观看 HTML5 多媒体教程

00:00:05.500 --> 00:00:09.000
今天我们将学习 video 元素的所有属性

00:00:10.000 --> 00:00:14.500
包括 controls、autoplay、muted 等等

00:00:15.000 --> 00:00:19.000
以及 track 字幕轨道的使用方法

-- 样式设置(可选)
STYLE
::cue {
  background-color: rgba(0, 0, 0, 0.8);
  color: white;
  font-size: 18px;
}</pre>

    <h3 style="margin:18px 0 12px;">Track kind 类型说明</h3>
    <table>
      <thead>
        <tr><th>kind 值</th><th>用途</th><th>是否对用户可见</th><th>典型场景</th></tr>
      </thead>
      <tbody>
        <tr><td><code>subtitles</code></td><td>翻译字幕</td><td>✅ 可切换显示</td><td>多语言字幕</td></tr>
        <tr><td><code>captions</code></td><td>隐藏式字幕</td><td>✅ 可切换显示</td><td>听障辅助、对话转录</td></tr>
        <tr><td><code>descriptions</code></td><td>视觉描述</td><td>🔈 语音朗读</td><td>屏幕阅读器描述画面</td></tr>
        <tr><td><code>chapters</code></td><td>章节标题</td><td>📑 导航菜单</td><td>长视频分章节跳转</td></tr>
        <tr><td><code>metadata</code></td><td>元数据</td><td>❌ 不显示</td><td>JS 脚本驱动数据</td></tr>
      </tbody>
    </table>
  </div>

  <!-- ========== Video API ========== -->
  <div class="section">
    <h2>4. Video JavaScript API</h2>
    <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:15px;">
      <button onclick="showVideoAPI()">📋 查看 API 属性</button>
      <button onclick="simulateVideoEvents()" style="background:#27ae60;">🎬 模拟事件流</button>
    </div>
    <div class="info-grid" id="video-api-info"></div>
    <pre id="video-events-log" style="max-height:200px;margin-top:15px;"></pre>
  </div>

  <script>
    // ===== Canvas 占位绘制 =====
    const canvas = document.getElementById('video-canvas');
    const ctx = canvas.getContext('2d');
    let animFrame = 0;

    function drawPlaceholder() {
      // 渐变背景
      const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
      grad.addColorStop(0, '#1a1a2e');
      grad.addColorStop(1, '#16213e');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // 网格线
      ctx.strokeStyle = 'rgba(52,152,219,0.15)';
      ctx.lineWidth = 1;
      for (let i = 0; i < canvas.width; i += 40) {
        ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height); ctx.stroke();
      }
      for (let i = 0; i < canvas.height; i += 40) {
        ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke();
      }

      // 中心图标
      ctx.fillStyle = 'rgba(52,152,219,0.8)';
      ctx.beginPath();
      ctx.arc(canvas.width / 2, canvas.height / 2, 60 + Math.sin(animFrame * 0.02) * 5, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = 'white';
      ctx.font = 'bold 40px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('▶', canvas.width / 2, canvas.height / 2);

      // 文字
      ctx.fillStyle = 'rgba(255,255,255,0.7)';
      ctx.font = '16px sans-serif';
      ctx.fillText('HTML5 Video Demo', canvas.width / 2, canvas.height / 2 + 110);
      ctx.font = '13px sans-serif';
      ctx.fillStyle = 'rgba(255,255,255,0.4)';
      ctx.fillText('实际使用时请替换为真实视频文件', canvas.width / 2, canvas.height / 2 + 135);

      // 进度条动画
      if (window._playing) {
        window._currentTime = (window._currentTime || 0) + 0.016 * (window._playRate || 1);
        if (window._currentTime >= 120) window._currentTime = window._loop ? 0 : 120;
        updateProgressUI();
      }

      animFrame++;
      requestAnimationFrame(drawPlaceholder);
    }
    drawPlaceholder();

    // ===== 模拟播放状态 =====
    let _currentTime = 0, _duration = 120, _playing = false, _playRate = 1, _loop = false, _volume = 1, _muted = false;

    function togglePlay() {
      _playing = !_playing;
      document.getElementById('btn-play-pause').textContent = _playing ? '⏸' : '▶';
    }
    function toggleMute() {
      _muted = !_muted;
      document.getElementById('btn-mute').textContent = _muted ? '🔇' : '🔊';
    }
    function setVolume(v) { _volume = parseFloat(v); }
    function setPlaybackRate(r) {
      _playRate = r;
      document.querySelectorAll('.track-btn').forEach(b => b.classList.remove('active'));
      event.target.classList.add('active');
    }
    function toggleLoop() { _loop = !_loop; event.target.classList.toggle('active'); }
    function skip(sec) { _currentTime = Math.max(0, Math.min(_duration, _currentTime + sec)); }
    function seek(e) {
      const wrap = document.getElementById('progress-wrap');
      const rect = wrap.getBoundingClientRect();
      _currentTime = ((e.clientX - rect.left) / rect.width) * _duration;
    }
    function switchTrack(val) {
      console.log(`切换到字幕轨道: ${val === '-1' ? '关闭' : val}`);
    }

    function updateProgressUI() {
      const pct = (_currentTime / _duration) * 100;
      document.getElementById('progress-bar').style.width = pct + '%';

      const fmt = s => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
      document.getElementById('time-display').textContent = `${fmt(_currentTime)} / ${fmt(_duration)}`;
    }

    function toggleFullscreen() {
      const container = document.getElementById('main-video-container');
      if (document.fullscreenElement) {
        document.exitFullscreen();
      } else {
        container.requestFullscreen?.() || container.webkitRequestFullscreen?.();
      }
    }

    function showVideoAPI() {
      const info = document.getElementById('video-api-info');
      info.innerHTML = `
        <div class="info-card"><label>currentTime</label><value>${_currentTime.toFixed(1)}s</value></div>
        <div class="info-card"><label>duration</label><value>${_duration}s</value></div>
        <div class="info-card"><label>paused</label><value>${!_playing}</value></div>
        <div class="info-card"><label>ended</label><value>${_currentTime >= _duration && !_loop}</value></div>
        <div class="info-card"><label>volume</label><value>${(_muted ? 0 : _volume * 100).toFixed(0)}%</value></div>
        <div class="info-card"><label>muted</label><value>${_muted}</value></div>
        <div class="info-card"><label>playbackRate</label><value>${_playRate}x</value></div>
        <div class="info-card"><label>loop</label><value>${_loop}</value></div>
        <div class="info-card"><label>readyState</label><value>4 (HAVE_ENOUGH_DATA)</value></div>
        <div class="info-card"><label>networkState</label><value>1 (IDLE)</value></div>
        <div class="info-card"><label>videoWidth</label><value>${canvas.width}px</value></div>
        <div class="info-card"><label>videoHeight</label><value>${canvas.height}px</value></div>`;
    }

    function simulateVideoEvents() {
      const events = [
        { t: 0, e: 'loadstart' },
        { t: 200, e: 'durationchange' },
        { t: 400, e: 'loadedmetadata' },
        { t: 600, e: 'loadeddata' },
        { t: 800, e: 'canplay' },
        { t: 1000, e: 'canplaythrough' },
        { t: 1200, e: 'play' },
        { t: 1400, e: 'playing' },
        { t: 3000, e: 'timeupdate' },
        { t: 5000, e: 'timeupdate' },
        { t: 8000, e: 'pause' },
        { t: 8500, e: 'waiting' },
        { t: 9500, e: 'playing' },
        { t: 15000, e: 'ended' }
      ];

      const log = document.getElementById('video-events-log');
      log.textContent = '';
      let idx = 0;

      const timer = setInterval(() => {
        if (idx >= events.length) { clearInterval(timer); return; }
        log.textContent += `[+${events[idx].t}ms] "${events[idx].e}"\n`;
        log.scrollTop = log.scrollHeight;
        idx++;
      }, 300);
    }
  </script>
</body>
</html>
<h4>052-audio-custom-embed-compare.html</h4>
html
<!DOCTYPE html>
<!--
  来源:基础知识/9-嵌入多媒体元素.md
  演示:Audio 自定义播放控件、embed/object/iframe 对比、figure/figcaption
-->
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Audio 自定义控件与嵌入方式对比</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, sans-serif; padding: 30px; background: #f0f2f5; max-width: 900px; margin: 0 auto; }
    h1 { color: #1a1a1a; margin-bottom: 25px; }
    .section { background: white; padding: 25px; margin-bottom: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.06); }
    h2 { color: #333; font-size: 17px; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 8px; }

    /* 自定义音频播放器 */
    .audio-player {
      background: linear-gradient(135deg, #1a1a2e, #16213e); border-radius: 16px;
      padding: 24px; max-width: 550px; color: white;
    }
    .player-header { display: flex; align-items: center; gap: 15px; margin-bottom: 18px; }
    .album-art {
      width: 64px; height: 64px; border-radius: 10px; background: linear-gradient(135deg, #667eea, #764ba2);
      display: flex; align-items: center; justify-content: center; font-size: 28px;
    }
    .track-info h3 { font-size: 16px; margin-bottom: 3px; }
    .track-info p { font-size: 12px; color: rgba(255,255,255,0.6); }

    .progress-section { margin-bottom: 15px; }
    .progress-bar-bg {
      height: 6px; background: rgba(255,255,255,0.2); border-radius: 3px;
      cursor: pointer; position: relative;
    }
    .progress-bar-fill {
      height: 100%; background: linear-gradient(90deg, #3498db, #2ecc71);
      border-radius: 3px; width: 35%; position: relative; transition: width 0.1s linear;
    }
    .time-row { display: flex; justify-content: space-between; font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 6px; font-variant-numeric: tabular-nums; }

    .controls { display: flex; align-items: center; justify-content: center; gap: 18px; }
    .ctrl-btn {
      background: none; border: none; color: white; cursor: pointer;
      font-size: 22px; transition: transform 0.2s, opacity 0.2s; padding: 5px;
    }
    .ctrl-btn:hover { transform: scale(1.15); opacity: 0.85; }
    .ctrl-btn.play-btn { font-size: 36px; }
    .ctrl-btn.active { color: #3498db; }

    .volume-control { display: flex; align-items: center; gap: 8px; margin-top: 15px; justify-content: center; }
    .vol-slider {
      width: 100px; height: 4px; -webkit-appearance: none; background: rgba(255,255,255,0.3);
      border-radius: 2px; outline: none;
    }
    .vol-slider::-webkit-slider-thumb {
      -webkit-appearance: none; width: 14px; height: 14px; background: white;
      border-radius: 50%; cursor: pointer;
    }

    /* 可视化波形 */
    .visualizer {
      display: flex; align-items: flex-end; justify-content: center; gap: 3px;
      height: 40px; margin-top: 15px;
    }
    .viz-bar {
      width: 4px; background: linear-gradient(to top, #3498db, #2ecc71);
      border-radius: 2px; transition: height 0.1s ease;
    }

    /* iframe/embed/object 对比 */
    .embed-compare { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin: 15px 0; }
    @media (max-width: 650px) { .embed-compare { grid-template-columns: 1fr; } }
    .embed-box {
      border: 2px solid #e0e0e0; border-radius: 10px; overflow: hidden;
      background: #fafafa; min-height: 180px;
    }
    .embed-box header {
      background: #34495e; color: white; padding: 8px 12px; font-size: 13px; font-weight: bold;
    }
    .embed-body { padding: 15px; font-size: 12px; color: #666; }

    /* figure/figcaption */
    figure {
      background: white; border-radius: 10px; overflow: hidden;
      box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin: 15px 0;
    }
    figure img { width: 100%; height: auto; display: block; }
    figcaption {
      padding: 12px 16px; font-size: 13px; color: #555; background: #f8f9fa;
      border-top: 1px solid #eee; line-height: 1.5;
    }

    table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 15px 0; }
    th, td { padding: 10px 12px; text-align: left; border: 1px solid #dee2e6; }
    th { background: #34495e; color: white; }

    pre { background: #263238; color: #eceff1; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; line-height: 1.6; }
  </style>
</head>
<body>
  <h1>🎵 Audio 自定义控件 & 嵌入方式对比</h1>

  <!-- ========== 自定义 Audio 播放器 ========== -->
  <div class="section">
    <h2>1. 自定义 Audio 播放控件</h2>
    <p>完全自定义 UI 的音频播放器,模拟真实音乐播放体验:</p>

    <div class="audio-player">
      <div class="player-header">
        <div class="album-art">🎵</div>
        <div class="track-info">
          <h3>HTML5 Tutorial Theme</h3>
          <p>Web Developer • Multimedia Demo</p>
        </div>
      </div>

      <div class="progress-section">
        <div class="progress-bar-bg" id="prog-bg" onclick="seekAudio(event)">
          <div class="progress-bar-fill" id="prog-fill"></div>
        </div>
        <div class="time-row">
          <span id="cur-time">0:42</span>
          <span id="dur-time">3:28</span>
        </div>
      </div>

      <div class="visualizer" id="visualizer">
        <!-- JS 动态生成波形条 -->
      </div>

      <div class="controls">
        <button class="ctrl-btn" onclick="skipAudio(-10)" title="后退10秒">⏮</button>
        <button class="ctrl-btn" onclick="skipAudio(-5)" title="后退5秒">⏪</button>
        <button class="ctrl-btn play-btn" id="play-btn" onclick="togglePlayAudio()">▶</button>
        <button class="ctrl-btn" onclick="skipAudio(5)" title="前进5秒">⏩</button>
        <button class="ctrl-btn" onclick="skipAudio(10)" title="前进10秒">⏭</button>
        <button class="ctrl-btn" id="shuffle-btn" onclick="this.classList.toggle('active')" title="随机播放">🔀</button>
        <button class="ctrl-btn" id="repeat-btn" onclick="this.classList.toggle('active')" title="循环播放">🔁</button>
      </div>

      <div class="volume-control">
        <span style="font-size:14px;">🔉</span>
        <input type="range" class="vol-slider" id="vol-slider" min="0" max="1" step="0.05" value="0.8" oninput="changeVolume(this.value)">
        <span id="vol-label" style="font-size:12px;color:rgba(255,255,255,0.6);min-width:28px;">80%</span>
      </div>
    </div>

    <pre style="margin-top:15px;font-size:11px;">// 核心代码结构:
// &lt;audio id="myAudio" src="music.mp3"&gt;&lt;/audio&gt;
//
// myAudio.play()           → 返回 Promise,开始播放
// myAudio.pause()          → 暂停
// myAudio.currentTime     → 当前时间(可读写)
// myAudio.duration         → 总时长(只读)
// myAudio.volume           → 音量 0.0~1.0
// myAudio.playbackRate     → 播放速率(1=正常)
// myAudio.muted            → 是否静音
// myAudio.loop             → 循环播放
//
// 事件: play, pause, ended, timeupdate,
//       loadedmetadata, canplaythrough, error</pre>
  </div>

  <!-- ========== embed / object / iframe 对比 ========== -->
  <div class="section">
    <h2>2. 嵌入方式对比:iframe vs embed vs object</h2>

    <div class="embed-compare">
      <div class="embed-box">
        <header>&lt;iframe&gt;</header>
        <div class="embed-body">
          <strong>推荐用于:</strong>嵌入网页<br><br>
          ✅ 安全沙箱 (sandbox)<br>
          ✅ lazy loading<br>
          ✅ 完整 DOM 访问<br>
          ✅ SEO 友好<br>
          ❌ 不能嵌入 PDF/Flash<br>
          <pre style="margin-top:8px;background:#f4f4f4;color:#333;padding:6px;font-size:10px;border-radius:4px;">&lt;iframe src="url"
  sandbox="allow-scripts"
  loading="lazy"
  width="100%"
  height="400"&gt;
&lt;/iframe&gt;</pre>
        </div>
      </div>
      <div class="embed-box">
        <header>&lt;embed&gt;</header>
        <div class="embed-body">
          <strong>推荐用于:</strong>嵌入媒体/PDF<br><br>
          ✅ 自闭合标签<br>
          ✅ PDF 显示<br>
          ✅ 简单易用<br>
          ❌ 无备用内容<br>
          ❌ 无法脚本访问<br>
          ❌ 已被废弃趋势<br>
          <pre style="margin-top:8px;background:#f4f4f4;color:#333;padding:6px;font-size:10px;border-radius:4px;">&lt;embed src="doc.pdf"
  type="application/pdf"
  width="100%"
  height="500"&gt;</pre>
        </div>
      </div>
      <div class="embed-box">
        <header>&lt;object&gt;</header>
        <div class="embed-body">
          <strong>推荐用于:</strong>复杂嵌入<br><br>
          ✅ 支持备用内容<br>
          ✅ 参数配置 (param)<br>
          ✅ PDF/SWF/SVG<br>
          ❌ 较复杂<br>
          ❌ Flash 已死<br>
          <pre style="margin-top:8px;background:#f4f4f4;color:#333;padding:6px;font-size:10px;border-radius:4px;">&lt;object data="doc.pdf"
  type="application/pdf"
  width="100%" height="500"&gt;
  &lt;p&gt;不支持 PDF&lt;/p&gt;
&lt;/object&gt;</pre>
        </div>
      </div>
    </div>

    <h3 style="margin:18px 0 12px;">iframe sandbox 安全属性详解</h3>
    <table>
      <thead>
        <tr><th>sandbox 值</th><th>允许的操作</th><th>安全级别</th></tr>
      </thead>
      <tbody>
        <tr><td>(空)</td><td>禁止几乎所有功能</td><td>🔒 最严格</td></tr>
        <tr><td><code>allow-scripts</code></td><td>允许执行 JavaScript</td><td>⚠️</td></tr>
        <tr><td><code>allow-same-origin</code></td><td>视为同源(可访问 cookie 等)</td><td>⚠️</td></tr>
        <tr><td><code>allow-forms</code></td><td>允许提交表单</td><td>✅ 低风险</td></tr>
        <tr><td><code>allow-popups</code></td><td>允许弹出窗口</td><td>⚠️</td></tr>
        <tr><td><code>allow-popups-to-escape-sandbox</code></td><td>弹窗不受 sandbox 限制</td><td>⚠️</td></tr>
        <tr><td><code>allow-downloads</code></td><td>允许下载</td><td>✅</td></tr>
        <tr><td><code>allow-modals</code></td><td>允许 showModal()</td><td>✅</td></tr>
        <tr><td><code>allow-orientation-lock</code></td><td>锁定屏幕方向</td><td>✅</td></tr>
        <tr><td><code>allow-presentation</code></td><td>允许 Presentation API</td><td>✅</td></tr>
      </tbody>
    </table>
  </div>

  <!-- ========== figure / figcaption ========== -->
  <div class="section">
    <h2>3. figure / figcaption 语义化图文组合</h2>
    <p><code>&lt;figure&gt;</code> 用于包裹独立的图文内容,<code>&lt;figcaption&gt;</code> 提供标题或说明:</p>

    <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;">
      <figure>
        <div style="height:160px;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center;color:white;font-size:48px;">🖼️</div>
        <figcaption><strong>图 1:</strong> 图片标题放在这里,可以是多行文本描述。</figcaption>
      </figure>
      <figure>
        <div style="height:160px;background:linear-gradient(135deg,#f093fb,#f5576c);display:flex;align-items:center;justify-content:center;color:white;font-size:48px;">📊</div>
        <figcaption><strong>图 2:</strong> 图表展示 2024 年前端技术趋势分布。</figcaption>
      </figure>
      <figure>
        <div style="height:160px;background:linear-gradient(135deg,#4facfe,#00f2fe);display:flex;align-items:center;justify-content:center;color:white;font-size:48px;">💻</div>
        <figcaption><strong>代码示例:</strong> figure 不仅可用于图片,也可包裹代码块、引用等内容。</figcaption>
      </figure>
    </div>

    <pre style="margin-top:15px;">&lt;figure&gt;
  &lt;img src="photo.jpg" alt="描述"&gt;
  &lt;figcaption&gt;
    &lt;strong&gt;图 1:&lt;/strong&gt; 这是图片的标题和详细说明。
  &lt;/figcaption&gt;
&lt;/figure&gt;

// 特点:
// - 语义化:明确表示这是带说明的独立内容
// - 可移除:不影响文档主体理解时可整体移除
// - 支持多种子元素:img, video, audio, table, code, pre, svg, canvas...
// - SEO 有利:搜索引擎能正确关联图与说明</pre>
  </div>

  <script>
    // ===== 音频播放器逻辑 =====
    let isPlaying = false, currentTime = 42, duration = 208; // 3:28
    let volume = 0.8, playTimer = null;

    // 初始化可视化
    const vizContainer = document.getElementById('visualizer');
    for (let i = 0; i < 32; i++) {
      const bar = document.createElement('div');
      bar.className = 'viz-bar';
      bar.style.height = '4px';
      vizContainer.appendChild(bar);
    }

    function animateVisualizer() {
      if (!isPlaying) return;
      const bars = vizContainer.querySelectorAll('.viz-bar');
      bars.forEach(bar => {
        const h = isPlaying ? Math.random() * 36 + 4 : 4;
        bar.style.height = h + 'px';
      });
      requestAnimationFrame(animateVisualizer);
    }

    function togglePlayAudio() {
      isPlaying = !isPlaying;
      document.getElementById('play-btn').textContent = isPlaying ? '⏸' : '▶';
      if (isPlaying) {
        playTimer = setInterval(() => {
          currentTime += 0.1;
          if (currentTime >= duration) currentTime = 0;
          updateTimeUI();
        }, 100);
        animateVisualizer();
      } else {
        clearInterval(playTimer);
        const bars = vizContainer.querySelectorAll('.viz-bar');
        bars.forEach(b => b.style.height = '4px');
      }
    }

    function skipAudio(sec) {
      currentTime = Math.max(0, Math.min(duration, currentTime + sec));
      updateTimeUI();
    }

    function changeVolume(v) {
      volume = parseFloat(v);
      document.getElementById('vol-label').textContent = Math.round(volume * 100) + '%';
    }

    function seekAudio(e) {
      const bg = document.getElementById('prog-bg');
      const rect = bg.getBoundingClientRect();
      currentTime = ((e.clientX - rect.left) / rect.width) * duration;
      updateTimeUI();
    }

    function updateTimeUI() {
      const pct = (currentTime / duration) * 100;
      document.getElementById('prog-fill').style.width = pct + '%';

      const fmt = s => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
      document.getElementById('cur-time').textContent = fmt(currentTime);
    }
  </script>
</body>
</html>
<h4>053-media-session-api.html</h4>
html
<!DOCTYPE html>
<!--
  来源:基础知识/9-嵌入多媒体元素.md
  演示:Media Session API(媒体元信息 + 播放控制通知栏集成)
-->
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Media Session API 演示</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, sans-serif; padding: 30px; background: #f0f2f5; max-width: 900px; margin: 0 auto; }
    h1 { color: #1a1a1a; margin-bottom: 25px; }
    .section { background: white; padding: 25px; margin-bottom: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.06); }
    h2 { color: #333; font-size: 17px; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 8px; }

    .music-player {
      background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460);
      border-radius: 16px; padding: 30px; color: white; max-width: 500px;
    }

    .track-display {
      display: flex; gap: 16px; align-items: center; margin-bottom: 20px;
    }
    .album-art {
      width: 90px; height: 90px; border-radius: 12px;
      display: flex; align-items: center; justify-content: center; font-size: 36px;
      flex-shrink: 0;
    }
    .track-info h3 { font-size: 17px; margin-bottom: 4px; }
    .track-info p { font-size: 13px; color: rgba(255,255,255,0.6); }

    .progress-section { margin-bottom: 18px; }
    .progress-bar-bg {
      height: 5px; background: rgba(255,255,255,0.2); border-radius: 3px;
      cursor: pointer;
    }
    .progress-bar-fill {
      height: 100%; background: linear-gradient(90deg, #e74c3c, #f39c12);
      border-radius: 3px; width: 35%; position: relative;
    }
    .time-row { display: flex; justify-content: space-between; font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 6px; font-variant-numeric: tabular-nums; }

    .controls { display: flex; align-items: center; justify-content: center; gap: 20px; }
    .ctrl-btn {
      background: none; border: none; color: white; cursor: pointer;
      font-size: 22px; transition: transform 0.15s; padding: 6px;
    }
    .ctrl-btn:hover { transform: scale(1.15); }
    .ctrl-btn.play-btn { font-size: 40px; }
    .ctrl-btn.active { color: #e74c3c; }

    .playlist {
      margin-top: 20px; max-height: 200px; overflow-y: auto;
    }
    .playlist-item {
      display: flex; align-items: center; gap: 12px; padding: 10px 12px;
      border-radius: 8px; cursor: pointer; transition: background 0.2s;
    }
    .playlist-item:hover { background: rgba(255,255,255,0.08); }
    .playlist-item.active { background: rgba(231,76,60,0.15); }
    .playlist-item .num { width: 24px; text-align: center; font-size: 13px; color: rgba(255,255,255,0.4); }
    .playlist-item.active .num { color: #e74c3c; font-weight: bold; }
    .playlist-item .info { flex: 1; }
    .playlist-item .info div { font-size: 13px; }
    .playlist-item .info span { font-size: 11px; color: rgba(255,255,255,0.45); }
    .playlist-item .duration { font-size: 12px; color: rgba(255,255,255,0.4); }

    pre { background: #263238; color: #eceff1; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; line-height: 1.6; }
    table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 15px 0; }
    th, td { padding: 10px 12px; text-align: left; border: 1px solid #dee2e6; }
    th { background: #34495e; color: white; }

    .api-status {
      display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px;
      border-radius: 20px; font-size: 13px; font-weight: 600;
    }
    .api-supported { background: #eafaf1; color: #27ae60; }
    .api-not-supported { background: #fef5f5; color: #e74c3c; }
  </style>
</head>
<body>
  <h1>🎵 Media Session API 演示</h1>

  <div class="section">
    <p style="margin-bottom:15px;">
      Media Session API 让网页媒体可以与系统/浏览器的媒体控制面板集成。
      设置后,系统的媒体控制中心(如 macOS 控制中心、Windows 音量混合器)会显示当前播放的媒体信息:
    </p>

    <div style="margin-bottom:15px;">
      浏览器支持状态:
      <span id="ms-support" class="api-status api-not-supported">检测中...</span>
    </div>

    <!-- 音乐播放器 -->
    <div class="music-player">
      <div class="track-display">
        <div class="album-art" id="album-art" style="background:linear-gradient(135deg,#667eea,#764ba2);">🎵</div>
        <div class="track-info">
          <h3 id="track-title">HTML5 Tutorial Theme</h3>
          <p id="track-artist">Web Developer • Multimedia Demo Album</p>
        </div>
      </div>

      <div class="progress-section">
        <div class="progress-bar-bg" onclick="seekTo(event)">
          <div class="progress-bar-fill" id="prog-fill"></div>
        </div>
        <div class="time-row">
          <span id="cur-time">0:42</span>
          <span id="dur-time">3:28</span>
        </div>
      </div>

      <div class="controls">
        <button class="ctrl-btn" onclick="prevTrack()">⏮</button>
        <button class="ctrl-btn" onclick="rewind()">⏪</button>
        <button class="ctrl-btn play-btn" id="play-btn" onclick="togglePlay()">▶</button>
        <button class="ctrl-btn" onclick="forward()">⏩</button>
        <button class="ctrl-btn" onclick="nextTrack()">⏭</button>
        <button class="ctrl-btn" id="shuffle-btn" onclick="toggleShuffle()" title="随机播放">🔀</button>
        <button class="ctrl-btn" id="repeat-btn" onclick="toggleRepeat()" title="循环模式">🔁</button>
      </div>

      <div class="playlist" id="playlist"></div>
    </div>
  </div>

  <!-- ========== Media Session API 说明 ========== -->
  <div class="section">
    <h2>Media Session API 核心功能</h2>

    <table>
      <thead><tr><th>功能</th><th>API</th><th>说明</th></tr></thead>
      <tbody>
        <tr><td>媒体元信息</td><td><code>metadata</code></td><td>标题、艺术家、专辑名、封面图</td></tr>
        <tr><td>播放状态</td><td><code>playbackState</code></td><td>'none' | 'paused' | 'playing'</td></tr>
        <tr><td>操作处理</td><td><code>setActionHandler()</code></td><td>播放、暂停、上/下一首、快进等</td></tr>
        <tr><td>位置信息</td><td><code>setPositionState()</code></td><td>当前时间、总时长、播放速率</td></tr>
      </tbody>
    </table>

    <h3 style="margin:18px 0 12px;">支持的媒体操作(Action Handlers)</h3>
    <pre style="font-size:11px;">// 完整的 Action Handler 列表:
navigator.mediaSession.setActionHandler('play', () => { /* 播放 */ });
navigator.mediaSession.setActionHandler('pause', () => { /* 暂停 */ });
navigator.mediaSession.setActionHandler('stop', () => { /* 停止 */ });
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
  // details.seekOffsetSec — 默认 10 秒
  skipTime(- (details.seekOffsetSec || 10));
});
navigator.mediaSession.setActionHandler('seekforward', (details) => {
  // details.seekOffsetSec — 默认 10 秒
  skipTime(details.seekOffsetSec || 10);
});
navigator.mediaSession.setActionHandler('previoustrack', () => { /* 上一首 */ });
navigator.mediaSession.setActionHandler('nexttrack', () => { /* 下一首 */ });
// Chrome 120+ 新增:
navigator.mediaSession.setActionHandler('skipad', () => { /* 跳过广告 */ });
navigator.mediaSession.setActionHandler('togglemicrophone', () => { /* 麦克风切换 */ });</pre>

    <h3 style="margin:18px 0 12px;">设置媒体元信息代码</h3>
    <pre style="font-size:11px;">// 设置媒体元信息(显示在系统媒体控制面板)
if ('mediaSession' in navigator) {
  navigator.mediaSession.metadata = new MediaMetadata({
    title: '歌曲名称',
    artist: '歌手名称',
    album: '专辑名称',
    artwork: [
      { src: 'cover-96x96.png', sizes: '96x96', type: 'image/png' },
      { src: 'cover-128x128.png', sizes: '128x128', type: 'image/png' },
      { src: 'cover-192x192.png', sizes: '192x192', type: 'image/png' },
      { src: 'cover-256x256.png', sizes: '256x256', type: 'image/png' },
      { src: 'cover-512x512.png', sizes: '512x512', type: 'image/png' },
    ]
  });

  // 更新播放状态
  navigator.mediaSession.playbackState = 'playing';

  // 更新进度信息
  navigator.mediaSession.setPositionState({
    duration: 208,       // 总时长(秒)
    position: 42,         // 当前位置(秒)
    playbackRate: 1.0     // 播放速率
  });
}</pre>
  </div>

  <script>
    // ===== 歌曲数据 =====
    const playlist = [
      { title: 'HTML5 Tutorial Theme', artist: 'Web Developer', album: 'Multimedia Demo', duration: '3:28', dur: 208, color: 'linear-gradient(135deg,#667eea,#764ba2)', emoji: '🎵' },
      { title: 'CSS Animation Beat', artist: 'Style Master', album: 'Frontend Vibes', duration: '4:12', dur: 252, color: 'linear-gradient(135deg,#f093fb,#f5576c)', emoji: '🎨' },
      { title: 'JavaScript Symphony', artist: 'Code Composer', album: 'Dev Tunes', duration: '3:45', dur: 225, color: 'linear-gradient(135deg,#4facfe,#00f2fe)', emoji: '💻' },
      { title: 'Node.js Lullaby', artist: 'Backend Band', album: 'Server Songs', duration: '5:01', dur: 301, color: 'linear-gradient(135deg,#43e97b,#38f9d7)', emoji: '🟢' },
      { title: 'React Rhythm', artist: 'Component Crew', album: 'Framework Flow', duration: '3:33', dur: 213, color: 'linear-gradient(135deg,#fa709a,#fee140)', emoji: '⚛️' },
    ];

    let currentTrack = 0, isPlaying = false, currentTime = 42, playTimer = null;

    // ===== 初始化 =====
    function init() {
      // 检测支持
      const supportEl = document.getElementById('ms-support');
      if ('mediaSession' in navigator) {
        supportEl.className = 'api-status api-supported';
        supportEl.textContent = '✅ Media Session API 支持';
        setupMediaSession();
      } else {
        supportEl.textContent = '❌ 当前浏览器不支持';
      }

      // 渲染播放列表
      const listEl = document.getElementById('playlist');
      playlist.forEach((track, i) => {
        const item = document.createElement('div');
        item.className = `playlist-item${i === currentTrack ? ' active' : ''}`;
        item.onclick = () => switchTrack(i);
        item.innerHTML = `
          <span class="num">${i + 1}</span>
          <div class="info"><div>${track.title}</div><span>${track.artist}</span></div>
          <span class="duration">${track.duration}</span>`;
        listEl.appendChild(item);
      });

      updateTrackUI();
    }

    // ===== Media Session 设置 =====
    function setupMediaSession() {
      const track = playlist[currentTrack];

      // 设置元信息
      navigator.mediaSession.metadata = new MediaMetadata({
        title: track.title,
        artist: track.artist,
        album: track.album,
        artwork: [
          { src: '', sizes: '96x96', type: 'image/png',  // 使用空白占位
            backgroundColor: '#667eea' },
          { src: '', sizes: '128x128', type: 'image/png',
            backgroundColor: '#667eea' },
          { src: '', sizes: '512x512', type: 'image/png',
            backgroundColor: '#667eea' },
        ]
      });

      // 操作处理器
      navigator.mediaSession.setActionHandler('play', togglePlay);
      navigator.mediaSession.setActionHandler('pause', togglePlay);
      navigator.mediaSession.setActionHandler('previoustrack', prevTrack);
      navigator.mediaSession.setActionHandler('nexttrack', nextTrack);
      navigator.mediaSession.setActionHandler('seekbackward', (d) => rewind());
      navigator.mediaSession.setActionHandler('seekforward', (d) => forward());

      // 进度更新
      updatePositionState();
    }

    function updatePositionState() {
      if ('mediaSession' in navigator && navigator.mediaSession.setPositionState) {
        navigator.mediaSession.setPositionState({
          duration: playlist[currentTrack].dur,
          position: currentTime,
          playbackRate: 1.0
        });
      }
    }

    // ===== 播放控制 =====
    function togglePlay() {
      isPlaying = !isPlaying;
      document.getElementById('play-btn').textContent = isPlaying ? '⏸' : '▶';

      if ('mediaSession' in navigator) {
        navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
      }

      if (isPlaying) {
        playTimer = setInterval(() => {
          currentTime += 0.1;
          if (currentTime >= playlist[currentTrack].dur) {
            nextTrack();
            return;
          }
          updateTimeUI();
          updatePositionState();
        }, 100);
      } else {
        clearInterval(playTimer);
      }
    }

    function switchTrack(index) {
      currentTrack = index;
      currentTime = 0;
      updateTimeUI();

      // 更新 UI
      document.querySelectorAll('.playlist-item').forEach((el, i) => {
        el.classList.toggle('active', i === index);
      });
      updateTrackUI();
      setupMediaSession();

      if (isPlaying) {
        // 继续播放新曲目
      }
    }

    function prevTrack() {
      currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
      switchTrack(currentTrack);
    }

    function nextTrack() {
      currentTrack = (currentTrack + 1) % playlist.length;
      switchTrack(currentTrack);
    }

    function rewind() { currentTime = Math.max(0, currentTime - 10); updateTimeUI(); updatePositionState(); }
    function forward() { currentTime = Math.min(playlist[currentTrack].dur, currentTime + 10); updateTimeUI(); updatePositionState(); }

    function seekTo(e) {
      const bar = e.currentTarget;
      const rect = bar.getBoundingClientRect();
      currentTime = ((e.clientX - rect.left) / rect.width) * playlist[currentTrack].dur;
      updateTimeUI();
      updatePositionState();
    }

    function toggleShuffle() { document.getElementById('shuffle-btn').classList.toggle('active'); }
    function toggleRepeat() { document.getElementById('repeat-btn').classList.toggle('active'); }

    function updateTrackUI() {
      const track = playlist[currentTrack];
      document.getElementById('track-title').textContent = track.title;
      document.getElementById('track-artist').textContent = `${track.artist} • ${track.album}`;
      document.getElementById('album-art').style.background = track.color;
      document.getElementById('album-art').textContent = track.emoji;
    }

    function updateTimeUI() {
      const pct = (currentTime / playlist[currentTrack].dur) * 100;
      document.getElementById('prog-fill').style.width = pct + '%';
      const fmt = s => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
      document.getElementById('cur-time').textContent = fmt(currentTime);
    }

    init();
  </script>
</body>
</html>