嵌入多媒体元素
HTML5 提供了多种元素用于在网页中嵌入多媒体内容,包括音频、视频、PDF 文档和其他外部资源。这些元素使网页能够展示丰富的媒体内容,无需依赖第三方插件。
概述
HTML5 多媒体元素主要包括:
| 元素 | 用途 | 说明 |
|---|---|---|
<audio> | 嵌入音频 | 播放音频文件 |
<video> | 嵌入视频 | 播放视频文件 |
<track> | 媒体轨道 | 为视频添加字幕、说明等 |
<source> | 媒体源 | 为 audio/video 提供多个源 |
<object> | 嵌入外部资源 | 嵌入 PDF、Flash、SVG 等 |
<embed> | 嵌入外部资源 | 简单的嵌入方式 |
<iframe> | 嵌入其他网页 | 嵌入其他 HTML 页面 |
技术架构
HTML5 多媒体技术栈采用分层架构设计:
┌─────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ JavaScript API、事件处理、状态管理 │
├─────────────────────────────────────────────────────┤
│ DOM 层 (DOM Layer) │
│ HTMLMediaElement、HTMLVideoElement、HTMLAudioElement │
├─────────────────────────────────────────────────────┤
│ 媒体引擎层 (Media Engine) │
│ 解码器、缓冲管理、播放控制、同步机制 │
├─────────────────────────────────────────────────────┤
│ 平台层 (Platform Layer) │
│ 操作系统多媒体框架、硬件加速、音频输出 │
└─────────────────────────────────────────────────────┘核心特性
1. 原生支持
- 无插件依赖:无需安装 Flash、QuickTime 等第三方插件
- 跨平台兼容:支持桌面端和移动端浏览器
- 硬件加速:利用 GPU 进行视频解码和渲染
2. 丰富的 API
- 播放控制:
play()、pause()、load()等方法 - 状态管理:
currentTime、volume、playbackRate等属性 - 事件监听:
play、pause、ended、timeupdate等事件 - 高级功能:画中画、全屏、媒体流等
3. 多格式支持
- 音频格式:MP3、WAV、OGG、AAC、WebM Audio
- 视频格式:MP4(H.264)、WebM(VP8/VP9)、Ogg(Theora)
- 自适应码率:HLS、DASH 等流媒体协议
4. 可访问性
- 字幕支持:通过
<track>元素添加字幕、说明 - 键盘控制:支持键盘导航和操作
- 屏幕阅读器:提供语义化的描述信息
音频 audio
<audio> 是 HTML5 引入的多媒体元素,用于在网页中嵌入音频内容。提供简单而强大的方式来播放音频文件,无需依赖第三方插件
<!-- 最简单的音频播放器 -->
<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>注意:大多数浏览器会阻止自动播放带有声音的音频,除非用户与页面进行了交互
<!-- 来源: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 | 指定音频文件的 URL | URL | - |
preload | 指定预加载行为 | none(不预加载)、metadata(仅预加载元数据)、auto(自动预加载) | auto |
autoplay | 是否自动播放 | autoplay | - |
loop | 是否循环播放 | loop | - |
muted | 是否静音 | muted | - |
controls | 是否显示控制条 | controls | - |
volume | 设置音量 | 0.0 到 1.0 之间的数字 | 1.0 |
crossorigin | 跨域资源共享 | anonymous、use-credentials | - |
音频格式支持
HTML5 <audio> 元素支持多种音频格式:
| 格式 | 容器 | 编解码器 | 浏览器支持 |
|---|---|---|---|
| MP3 | .mp3 | MPEG Audio Layer III | 所有现代浏览器 |
| OGG | .ogg | Vorbis | Firefox, Chrome, Opera |
| WAV | .wav | PCM | 所有现代浏览器 |
| AAC | .m4a | Advanced Audio Coding | Safari, iOS, Android |
| WebM | .webm | Vorbis | Chrome, Firefox, Opera |
API 控制音频播放
基于安全策略,主流的浏览器已经停止音频和视频的自动播放。想让音频自动播放,就需要使用 JavaScript 脚本进行控制。
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
paused | Boolean | 是否暂停(只读) |
currentTime | Number | 当前播放时间(秒) |
duration | Number | 总时长(秒,只读) |
volume | Number | 音量(0.0 到 1.0) |
muted | Boolean | 是否静音 |
playbackRate | Number | 播放速率(1.0 为正常速度) |
readyState | Number | 就绪状态(0-4) |
networkState | Number | 网络状态(0-3) |
buffered | TimeRanges | 已缓冲的时间范围(只读) |
seekable | TimeRanges | 可跳转的时间范围(只读) |
高级属性
| 属性 | 类型 | 说明 |
|---|---|---|
autoplay | Boolean | 是否自动播放 |
controls | Boolean | 是否显示控制器 |
loop | Boolean | 是否循环播放 |
preload | String | 预加载策略:none/metadata/auto |
src | String | 媒体资源 URL |
currentSrc | String | 实际播放的资源 URL(只读) |
defaultPlaybackRate | Number | 默认播放速率 |
ended | Boolean | 是否播放结束(只读) |
error | MediaError | 错误对象(只读) |
seeking | Boolean | 是否正在跳转(只读) |
startTime | Number | 播放起始时间(只读) |
initialTime | Number | 初始播放时间(只读) |
played | TimeRanges | 已播放的时间范围(只读) |
audioTracks | AudioTrackList | 音频轨道列表 |
textTracks | TextTrackList | 文本轨道列表 |
常用方法
| 方法 | 说明 |
|---|---|
play() | 播放音频(返回 Promise) |
pause() | 暂停播放 |
load() | 重新加载音频 |
canPlayType() | 检查浏览器是否支持指定格式 |
高级方法
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
fastSeek() | time: Number | void | 快速跳转到指定时间 |
getStartDate() | - | Date | 获取媒体时间轴的起始日期 |
setMediaKeys() | mediaKeys: MediaKeys | Promise | 设置 DRM 密钥 |
addTextTrack() | kind, label, language | TextTrack | 添加文本轨道 |
readyState 状态值
| 值 | 常量 | 说明 |
|---|---|---|
| 0 | HAVE_NOTHING | 没有媒体资源 |
| 1 | HAVE_METADATA | 已加载元数据 |
| 2 | HAVE_CURRENT_DATA | 已加载当前帧数据 |
| 3 | HAVE_FUTURE_DATA | 已加载部分未来数据 |
| 4 | HAVE_ENOUGH_DATA | 已加载足够数据 |
networkState 状态值
| 值 | 常量 | 说明 |
|---|---|---|
| 0 | NETWORK_EMPTY | 初始状态 |
| 1 | NETWORK_IDLE | 已完成加载,等待播放 |
| 2 | NETWORK_LOADING | 正在加载中 |
| 3 | NETWORK_NO_SOURCE | 未找到合适的源 |
错误码
| 错误码 | 常量 | 说明 |
|---|---|---|
| 1 | MEDIA_ERR_ABORTED | 用户中止加载 |
| 2 | MEDIA_ERR_NETWORK | 网络错误 |
| 3 | MEDIA_ERR_DECODE | 解码错误 |
| 4 | MEDIA_ERR_SRC_NOT_SUPPORTED | 格式不支持 |
音频使用示例
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');
}高级用法
案例-自定义音频播放器

<!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 提供了更强大的音频处理能力,可以实现音频可视化、音效处理等高级功能。
<!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 可以实现音频录制功能:
// 请求麦克风权限并录制音频
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 可以编程合成音频:
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 引入的多媒体元素,用于在网页中嵌入视频内容。它提供了简单而强大的方式来播放视频文件,无需依赖第三方插件
<!-- 最简单的视频播放器 -->
<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>注意:大多数浏览器会阻止自动播放带有声音的视频,除非用户与页面进行了交互
<!-- 来源: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 | 指定视频文件的 URL | URL | - |
preload | 指定预加载行为 | none(不预加载)、metadata(仅预加载元数据)、auto(自动预加载) | auto |
autoplay | 是否自动播放 | autoplay | - |
loop | 是否循环播放 | loop | - |
muted | 是否静音 | muted | - |
controls | 是否显示控制条 | controls | - |
poster | 设置视频封面图片 | URL | - |
width | 设置视频宽度 | 像素值 | - |
height | 设置视频高度 | 像素值 | - |
playsinline | 在移动设备上内联播放 | playsinline | - |
crossorigin | 跨域资源共享 | anonymous、use-credentials | - |
disablePictureInPicture | 禁用画中画模式 | disablePictureInPicture | - |
视频格式支持
HTML5 <video> 元素支持多种视频格式:
| 格式 | 容器 | 编解码器 | 浏览器支持 |
|---|---|---|---|
| MP4 | .mp4 | H.264 视频 + AAC 音频 | 所有现代浏览器 |
| WebM | .webm | VP8/VP9 视频 + Vorbis/Opus 音频 | Chrome, Firefox, Edge |
| Ogg | .ogv | Theora 视频 + Vorbis 音频 | Firefox, Chrome, Opera |
| AVI | .avi | 多种编解码器 | 不支持 |
| MOV | .mov | H.264 视频 + AAC 音频 | Safari |
处理视频播放相关事件
在 HTML5 中,当使用 <video> 或 <audio> 标签读取或播放媒体时,会触发一系列的事件。
媒体生命周期状态机
媒体元素的加载和播放遵循一个明确的状态机流程:
┌──────────────┐
│ HAVE_ │
│ NOTHING (0) │
│ 初始状态 │
└──────┬───────┘
│ loadstart
▼
┌──────────────┐
│ HAVE_ │
│ METADATA (1)│
│ 已加载元数据 │
└──────┬───────┘
│ loadedmetadata
▼
┌──────────────┐
│ HAVE_ │
│ CURRENT_DATA │
│ (2) │
│ 当前帧数据 │
└──────┬───────┘
│ loadeddata
▼
┌──────────────┐
│ HAVE_ │
│ FUTURE_DATA │
│ (3) │
│ 预加载未来帧 │
└──────┬───────┘
│ canplay
▼
┌──────────────┐
│ HAVE_ │
│ ENOUGH_DATA │
│ (4) │
│ 数据充足播放 │
└──────────────┘
canplaythrough播放状态转换图
┌─────────┐
│ 暂停 │
│ 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(推荐)
const video = document.getElementById('myVideo');
video.addEventListener('play', () => {
console.log('开始播放');
});
video.addEventListener('pause', () => {
console.log('暂停播放');
});方式 2:使用事件属性(不推荐)
<video
id="myVideo"
src="video.mp4"
onplay="handlePlay()"
onpause="handlePause()"
></video>
<script>
function handlePlay() {
console.log('开始播放');
}
function handlePause() {
console.log('暂停播放');
}
</script>高级用法
案例-自定义视频播放器
<!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 可以实现视频截图功能:
<!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)允许视频在浮动窗口中播放:
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 允许视频全屏播放:
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 实时处理视频帧:
<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 访问摄像头:
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;
}视频录制
录制视频流并保存:
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 | 轨道类型 | 是 | subtitles、captions |
src | 轨道文件的 URL | 是 | src="subtitles.vtt" |
srclang | 轨道语言(字幕时必需) | 否 | srclang="zh-CN" |
label | 轨道标题(显示在菜单中) | 否 | label="中文字幕" |
default | 是否为默认轨道 | 否 | default |
kind 属性值
| 值 | 说明 |
|---|---|
subtitles | 字幕(翻译的对话) |
captions | 字幕(包含音效描述) |
descriptions | 视频描述(用于屏幕阅读器) |
chapters | 章节标题(用于导航) |
metadata | 元数据(不显示给用户) |
<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:是可以省略的
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 样式来改变字幕文本的样式
00:00:01.180 --> 00:00:06.000 <c>从今天开始,我们来学习Python程序设计的课程。</c>在 HTML 文件中,可以通过下面的 CSS 代码设置字幕文本的颜色为白色
<style>
video::cue(c) {
color: white;
font-size: 28px;
}
</style>播放字幕视频
下面项目需要在服务器中运行,使用 vscode 的 Open with liver server 功能
嵌入 PDF 文档
object 元素
<object> 是 HTML 中用于嵌入外部资源的通用元素,它可以包含替代内容(当嵌入失败时显示),并且支持参数传递
主要属性:
| 属性 | 描述 | 示例 |
|---|---|---|
data | 指定要嵌入资源的 URL | data="example.pdf" |
type | 指定嵌入内容的 MIME 类型 | type="application/pdf" |
width | 设置嵌入内容的宽度 | width="600" |
height | 设置嵌入内容的高度 | height="400" |
name | 为对象指定名称 | name="pdfViewer" |
usemap | 关联客户端图像映射 | usemap="#map1" |
form | 关联的表单 ID | form="myForm" |
classid | 指定 ActiveX 控件的 CLSID(仅 IE) | classid="clsid:..." |
基本语法:
<!-- 嵌入 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 | 指定要嵌入资源的 URL | src="example.mp3" |
type | 指定嵌入内容的 MIME 类型 | type="audio/mpeg" |
width | 设置嵌入内容的宽度 | width="300" |
height | 设置嵌入内容的高度 | height="50" |
autoplay | 是否自动播放 | autoplay |
loop | 是否循环播放 | loop |
muted | 是否静音 | muted |
pluginspage | 指定插件下载页面(已过时) | pluginspage="http://..." |
基本语法:
<!-- 嵌入音频文件 -->
<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 页面,常用于嵌入地图、视频、广告等内容。
基本语法
<iframe src="https://example.com" width="800" height="600"></iframe>主要属性
| 属性 | 描述 | 示例 |
|---|---|---|
src | 嵌入页面的 URL | src="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 使用示例
<!-- 嵌入外部网页 -->
<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 的权限,提升安全性:
<!-- 完全限制 -->
<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 嵌入第三方内容存在多种安全风险,需要采取相应的防护措施:
┌──────────────────────────────────────────────────────┐
│ 安全威胁矩阵 │
├──────────────┬───────────────┬───────────────────────┤
│ 威胁类型 │ 风险等级 │ 防护措施 │
├──────────────┼───────────────┼───────────────────────┤
│ XSS 攻击 │ 高 │ sandbox, CSP │
│ 点击劫持 │ 中 │ X-Frame-Options │
│ 钓鱼攻击 │ 高 │ sandbox, 验证来源 │
│ 数据泄露 │ 高 │ sandbox, 同源策略 │
│ 恶意重定向 │ 中 │ sandbox │
└──────────────┴───────────────┴───────────────────────┘1. sandbox 安全沙箱
sandbox 属性通过限制 iframe 的能力来增强安全性:
<!-- 最高安全级别:完全隔离 -->
<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 的行为:
<!-- 在父页面设置 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 中(点击劫持防护):
<!-- HTTP 响应头设置 -->
<!--
X-Frame-Options: DENY 禁止任何嵌入
X-Frame-Options: SAMEORIGIN 仅允许同源嵌入
X-Frame-Options: ALLOW-FROM uri 允许特定源嵌入(已废弃)
-->注意:X-Frame-Options 已逐渐被 CSP 的 frame-ancestors 取代。
4. XSS 防护最佳实践
<!-- ✅ 推荐:综合使用多种安全策略 -->
<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 进行安全的跨域通信:
// 父页面:发送消息
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:未验证的消息来源
// ❌ 危险:未验证消息来源
window.addEventListener('message', (event) => {
document.body.innerHTML = event.data; // XSS 漏洞
});
// ✅ 安全:验证消息来源
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') {
return;
}
// 处理可信消息
});漏洞 2:过于宽松的 sandbox
<!-- ❌ 危险:允许所有权限 -->
<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:点击劫持
<!-- 攻击者页面 -->
<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
<!-- 使用 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 允许视频在浮动窗口中播放,用户可以在其他页面或应用中继续观看视频。
基本用法:
<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>检测支持:
if ('pictureInPictureEnabled' in document) {
console.log('浏览器支持画中画');
}关键属性与事件:
| 属性 / 事件 | 说明 |
|---|---|
document.pictureInPictureEnabled | 当前页面是否允许画中画 |
document.pictureInPictureElement | 当前处于画中画模式的元素(无则为 null) |
video.requestPictureInPicture() | 请求进入画中画,返回 Promise |
document.exitPictureInPicture() | 退出画中画,返回 Promise |
enterpictureinpicture 事件 | 进入画中画时触发 |
leavepictureinpicture 事件 | 退出画中画时触发 |
控制画中画窗口大小:
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);
});
});画中画窗口始终置顶,用户可拖拽和调整大小。适用于视频教程、会议、直播等需要同时操作其他内容的场景。
Media Session API
Media Session API 允许网页自定义系统级媒体控制(如通知栏、锁屏界面的播放控制按钮和元数据),让用户在浏览器外也能操控媒体播放。
设置媒体元数据:
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' },
]
});
}设置播放控制处理程序:
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();
});更新播放状态:
video.addEventListener('play', () => {
navigator.mediaSession.playbackState = 'playing';
});
video.addEventListener('pause', () => {
navigator.mediaSession.playbackState = 'paused';
});Media Session API 在移动端尤为实用——用户锁屏或切换应用后,仍可通过系统通知栏控制播放、查看曲目信息。桌面端浏览器也会在媒体通知中显示这些信息。
性能优化
1. 懒加载媒体
<!-- 音频懒加载 -->
<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 值
<!-- 不预加载(节省带宽) -->
<audio src="audio.mp3" preload="none"></audio>
<!-- 仅预加载元数据(推荐) -->
<video src="video.mp4" preload="metadata"></video>
<!-- 自动预加载(谨慎使用) -->
<audio src="background.mp3" preload="auto"></audio>3. 提供多种格式
<!-- 提供多种格式,浏览器选择最佳 -->
<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
<!-- 使用 CDN 加速媒体加载 -->
<video src="https://cdn.example.com/video.mp4" controls></video>5. 压缩媒体文件
- 使用适当的编码格式
- 压缩音频/视频文件大小
- 考虑使用 WebP/WebM 等现代格式
浏览器兼容性
多媒体元素支持
| 特性 | Chrome | Firefox | Safari | Edge | IE11 |
|---|---|---|---|---|---|
<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+ | ❌ |
音频格式兼容性
| 格式 | Chrome | Firefox | Safari | Edge | IE11 |
|---|---|---|---|---|---|
| MP3 | ✅ | ✅ | ✅ | ✅ | ✅ |
| WAV | ✅ | ✅ | ✅ | ✅ | ✅ |
| OGG | ✅ | ✅ | ❌ | ✅ | ❌ |
| AAC | ✅ | ✅ | ✅ | ✅ | ✅ |
| WebM Audio | ✅ | ✅ | ❌ | ✅ | ❌ |
视频格式兼容性
| 格式 | Chrome | Firefox | Safari | Edge | IE11 |
|---|---|---|---|---|---|
| MP4 (H.264) | ✅ | ✅ | ✅ | ✅ | ✅ |
| WebM (VP8/VP9) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Ogg (Theora) | ✅ | ✅ | ❌ | ✅ | ❌ |
| MOV | ❌ | ❌ | ✅ | ❌ | ❌ |
特性检测
在使用高级功能前,建议进行特性检测:
// 检测音频格式支持
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:
<!-- 使用 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
// 为旧浏览器添加 Web Audio API 支持
if (!window.AudioContext) {
window.AudioContext = window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext;
}3. MediaRecorder Polyfill
<!-- 使用第三方库提供 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. 自定义降级方案
<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>功能降级策略
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);
}
}可访问性
音频可访问性
<!-- 提供文本替代 -->
<audio controls>
<source src="audio.mp3" type="audio/mpeg">
您的浏览器不支持音频播放。
<a href="transcript.txt">查看文字稿</a>
</audio>视频可访问性
<!-- 提供字幕和描述 -->
<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 可访问性
<!-- 始终提供 title 属性 -->
<iframe
src="https://example.com"
title="嵌入的示例网站"
></iframe>最佳实践
1. 始终提供替代内容
<!-- ✅ 推荐 -->
<video controls>
<source src="video.mp4" type="video/mp4">
您的浏览器不支持视频播放。
</video>
<!-- ❌ 避免 -->
<video src="video.mp4" controls></video>2. 使用语义化的 track 元素
<video controls>
<source src="video.mp4" type="video/mp4">
<track
kind="subtitles"
src="subtitles.vtt"
srclang="zh-CN"
label="中文字幕"
default
>
</video>3. 处理自动播放限制
// 处理自动播放失败
const video = document.getElementById('myVideo');
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
// 自动播放成功
})
.catch(error => {
// 自动播放被阻止,需要用户交互
console.log('自动播放被阻止');
});
}4. 优化移动端体验
<!-- 移动端内联播放 -->
<video
src="video.mp4"
controls
playsinline
webkit-playsinline
></video>5. 使用适当的媒体格式
- 音频:优先使用 MP3(兼容性最好)或 WebM(文件更小)
- 视频:优先使用 MP4(H.264)或 WebM(VP9)
6. 错误处理
基础错误处理
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;
}
}
});高级错误处理
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);调试工具函数
// 媒体状态调试器
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());性能监控面板
<!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:音频/视频无法播放
可能原因
- 格式不支持
- 文件路径错误
- CORS 跨域问题
- 文件损坏
解决方案
<!-- 提供多种格式 -->
<audio controls>
<source src="audio.mp3" type="audio/mpeg">
<source src="audio.ogg" type="audio/ogg">
您的浏览器不支持音频播放。
</audio>
<!-- 检查控制台错误 -->
<!-- 使用开发者工具 Network 标签检查文件加载 -->问题 2:自动播放不工作
原因
浏览器阻止自动播放
解决方案
<!-- 使用 muted 属性 -->
<video src="video.mp4" autoplay muted></video>
<!-- 或通过用户交互触发 -->
<button onclick="video.play()">播放视频</button>问题 3:移动端视频全屏播放
原因
iOS Safari 默认全屏播放
解决方案
<video
src="video.mp4"
controls
playsinline
webkit-playsinline
></video>问题 4:字幕不显示
可能原因
- 文件路径错误
- CORS 问题
- 文件格式错误
- 需要在服务器环境运行
解决方案
<!-- 确保在服务器环境运行 -->
<!-- 检查字幕文件路径 -->
<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 限制
解决方案
<!-- 检查目标网站是否允许嵌入 -->
<!-- 某些网站(如 YouTube)提供专门的嵌入 URL -->
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
allowfullscreen
></iframe>问题 6:PDF 无法显示
原因
浏览器不支持或 CORS 问题
解决方案
<!-- 使用 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>调试技巧
// 检查媒体元素状态
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. 技术发展趋势
- AV1 编码:新一代视频编码,压缩效率更高
- WebCodecs API:底层音视频编解码能力
- Media Session API:系统级媒体控制集成
- WebNN API:基于神经网络的媒体处理
- WebTransport:低延迟流媒体传输
开发建议
初学者
- 从基础的
<audio>和<video>开始 - 熟悉常用属性和事件
- 学习使用 JavaScript 控制媒体播放
- 了解格式兼容性问题
进阶开发者
- 掌握 Web Audio API 音频处理
- 学习 Canvas 视频处理和截图
- 实现自定义媒体播放器
- 优化移动端播放体验
高级开发者
- 深入理解媒体编解码技术
- 实现自适应码率流媒体
- 开发实时音视频应用
- 性能监控和优化
参考资料
官方文档:
开源项目:
- Video.js - 开源 HTML5 视频播放器
- Howler.js - 现代Web音频库
- Plyr - 轻量级 HTML5 媒体播放器
- MediaElement.js - HTML5 音视频播放器
工具资源:
学习资源:
补充示例
<h4>051-video-complete-demo.html</h4><!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>以下视频展示了 <video> 的核心属性。由于没有实际视频文件,使用 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>不同浏览器支持不同的视频格式,<source> 让浏览器自动选择最佳格式:</p>
<pre><video controls poster="cover.jpg">
<!-- 浏览器按顺序尝试,选第一个支持的格式 -->
<source src="video.mp4" type="video/mp4" codecs="avc1.42E01E,mp4a.40.2">
⭐ MP4 (H.264) — 兼容性最好,Safari/Chrome/Firefox/Edge 全支持
<source src="video.webm" type="video/webm" codecs="vp9,vorbis">
🌐 WebM (VP9) — 开源,Chrome/Firefox 支持,质量更好
<source src="video.ogg" type="video/ogg" codecs="theora,vorbis">
📼 Ogg Theora — 老格式,仅 Firefox
<!-- 全都不支持时显示 -->
您的浏览器不支持 HTML5 视频,请
<a href="video.mp4">下载视频</a>。
</video></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><video controls>
<source src="movie.mp4" type="video/mp4">
<!-- 中文字幕 -->
<track kind="subtitles" src="subs-zh.vtt" srclang="zh" label="中文"
default>
<!-- 英文字幕 -->
<track kind="subtitles" src="subs-en.vtt" srclang="en" label="English">
<!-- 字幕描述(供屏幕阅读器)-->
<track kind="descriptions" src="desc.vtt" srclang="zh" label="描述">
<!-- 章节标记 -->
<track kind="chapters" src="chapters.vtt" srclang="zh" label="章节">
</video></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><!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;">// 核心代码结构:
// <audio id="myAudio" src="music.mp3"></audio>
//
// 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><iframe></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;"><iframe src="url"
sandbox="allow-scripts"
loading="lazy"
width="100%"
height="400">
</iframe></pre>
</div>
</div>
<div class="embed-box">
<header><embed></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;"><embed src="doc.pdf"
type="application/pdf"
width="100%"
height="500"></pre>
</div>
</div>
<div class="embed-box">
<header><object></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;"><object data="doc.pdf"
type="application/pdf"
width="100%" height="500">
<p>不支持 PDF</p>
</object></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><figure></code> 用于包裹独立的图文内容,<code><figcaption></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;"><figure>
<img src="photo.jpg" alt="描述">
<figcaption>
<strong>图 1:</strong> 这是图片的标题和详细说明。
</figcaption>
</figure>
// 特点:
// - 语义化:明确表示这是带说明的独立内容
// - 可移除:不影响文档主体理解时可整体移除
// - 支持多种子元素: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><!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>