跳到主要内容

其他✅

ResizeObserver 作用是什么

答案

核心概念

ResizeObserver 是一种原生 Web API,用于高效、精准地监听 DOM 元素尺寸的变化(宽高等),无论变化来源于内容、样式还是窗口调整。它能在元素尺寸发生变化时自动回调,适合响应式布局、动态组件等场景。

详细解释

  • 传统的 window.resize 事件只能监听窗口变化,无法直接感知单个元素的尺寸变化,且需轮询或定时器,效率低。
  • ResizeObserver 通过回调机制,能实时捕获元素尺寸变化,避免性能浪费和延迟。
  • 支持同时监听多个元素,回调参数为变化元素的列表,可批量处理。

代码示例

const target = document.querySelector('.resizable')
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
// entry.contentRect 包含新尺寸
console.log('size:', entry.contentRect.width, entry.contentRect.height)
}
})
observer.observe(target)

常见误区

  • 只监听元素本身的尺寸变化,不会响应子元素内容变化,除非影响父元素尺寸。
  • 回调是异步批量触发,避免在回调中频繁操作 DOM。
  • 兼容性较好,但 IE 不支持,需注意降级处理。
提示

推荐在响应式组件、图表自适应、弹窗等场景优先使用 ResizeObserver,替代定时器和 resize 事件监听。

延伸阅读

IntersectionObserver api?

答案

核心概念:

IntersectionObserver 是一个高性能的 Web API,用于异步监听元素与视口或指定根元素的交叉状态变化。它避免了频繁的滚动事件监听和DOM查询,适用于懒加载、无限滚动、广告曝光统计等场景。当目标元素的可见性发生变化时,会触发回调函数,提供详细的交叉信息。

实际示例:

// 1. 基本用法:懒加载图片
function setupLazyLoading () {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src // 加载真实图片
img.classList.remove('lazy')
observer.unobserve(img) // 停止观察已加载的图片
}
})
})

// 观察所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img)
})
}

// 2. 无限滚动实现
class InfiniteScroll {
constructor (container, loadMore) {
this.container = container
this.loadMore = loadMore
this.isLoading = false
this.setupObserver()
}

setupObserver () {
const options = {
root: null,
rootMargin: '100px', // 提前100px触发
threshold: 0
}

this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.isLoading) {
this.handleLoadMore()
}
})
}, options)

// 创建观察目标(底部加载指示器)
this.sentinel = document.createElement('div')
this.container.appendChild(this.sentinel)
this.observer.observe(this.sentinel)
}

async handleLoadMore () {
this.isLoading = true
try {
await this.loadMore()
} finally {
this.isLoading = false
}
}
}

// 3. 元素动画触发
function setupScrollAnimations () {
const animationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in')
} else {
entry.target.classList.remove('animate-in')
}
})
}, {
threshold: 0.3, // 30%可见时触发
rootMargin: '-50px' // 视口缩小50px
})

document.querySelectorAll('.animate-on-scroll').forEach(el => {
animationObserver.observe(el)
})
}

// 4. 广告曝光统计
function trackAdViews () {
const adObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const adId = entry.target.dataset.adId
const visibleRatio = entry.intersectionRatio

// 50%以上可见才算有效曝光
if (visibleRatio > 0.5) {
console.log(`广告 ${adId} 曝光,可见度: ${(visibleRatio * 100).toFixed(1)}%`)

// 发送曝光统计
fetch('/api/ad-view', {
method: 'POST',
body: JSON.stringify({ adId, visibleRatio })
})

// 停止观察已统计的广告
adObserver.unobserve(entry.target)
}
}
})
}, {
threshold: [0.5, 1.0] // 50%和100%可见时都触发
})

document.querySelectorAll('.ad-container').forEach(ad => {
adObserver.observe(ad)
})
}

// 使用示例
setupLazyLoading()
setupScrollAnimations()
trackAdViews()

// 无限滚动示例
const infiniteScroll = new InfiniteScroll(
document.getElementById('content'),
async () => {
const response = await fetch('/api/more-content')
const html = await response.text()
document.getElementById('content').insertAdjacentHTML('beforeend', html)
}
)

面试官视角:

要点清单:

  • 理解IntersectionObserver的性能优势
  • 掌握基本配置选项(root、rootMargin、threshold)
  • 了解典型应用场景的实现方式

加分项:

  • 提及与传统scroll事件监听的性能对比
  • 了解多个threshold值的使用技巧
  • 知道如何处理浏览器兼容性问题

常见失误:

  • 不理解异步回调的特性
  • 忽略unobserve的重要性导致内存泄漏
  • threshold配置不当影响触发时机

延伸阅读:

MutationObserver

答案

核心概念:

MutationObserver 是一个强大的 Web API,用于监听 DOM 树的变化并异步响应这些变化。它可以观察元素的添加、删除、属性变化、文本内容修改等,是现代前端开发中处理动态DOM变化的核心工具。相比传统的DOM事件,它提供了更全面和高效的DOM变化监听机制。

实际示例:

// 1. 基本用法:监听DOM变化
function setupDOMWatcher () {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
switch (mutation.type) {
case 'childList':
console.log('子节点发生变化:', mutation)
if (mutation.addedNodes.length > 0) {
console.log('新增节点:', mutation.addedNodes)
}
if (mutation.removedNodes.length > 0) {
console.log('删除节点:', mutation.removedNodes)
}
break

case 'attributes':
console.log('属性变化:', {
target: mutation.target,
attributeName: mutation.attributeName,
oldValue: mutation.oldValue,
newValue: mutation.target.getAttribute(mutation.attributeName)
})
break

case 'characterData':
console.log('文本内容变化:', mutation)
break
}
})
})

// 配置观察选项
const config = {
childList: true, // 观察子节点变化
attributes: true, // 观察属性变化
attributeOldValue: true, // 记录属性旧值
characterData: true, // 观察文本内容变化
subtree: true // 观察所有后代节点
}

// 开始观察
observer.observe(document.body, config)

return observer
}

// 2. 实际应用:单页应用路由监听
class SPARouteWatcher {
constructor () {
this.setupObserver()
}

setupObserver () {
this.observer = new MutationObserver((mutations) => {
let titleChanged = false
let urlChanged = false

mutations.forEach(mutation => {
if (mutation.type === 'childList' &&
mutation.target === document.head) {
// 检查title变化
const titleEl = document.querySelector('title')
if (titleEl && titleEl !== this.lastTitle) {
titleChanged = true
this.lastTitle = titleEl
}
}
})

// 检查URL变化
if (this.lastUrl !== window.location.href) {
urlChanged = true
this.lastUrl = window.location.href
}

if (titleChanged || urlChanged) {
this.handleRouteChange()
}
})

this.observer.observe(document.head, {
childList: true,
subtree: true
})

this.lastUrl = window.location.href
}

handleRouteChange () {
// 发送页面浏览统计
console.log('路由变化:', {
url: window.location.href,
title: document.title,
timestamp: Date.now()
})

// 执行页面级的初始化逻辑
this.initPageFeatures()
}

initPageFeatures () {
// 重新初始化页面功能
// 如:埋点、懒加载、动画等
}
}

// 3. 性能监控:监听DOM性能指标
class DOMPerformanceMonitor {
constructor () {
this.mutationCount = 0
this.largeChangeThreshold = 50
this.setupObserver()
}

setupObserver () {
this.observer = new MutationObserver((mutations) => {
this.mutationCount += mutations.length

// 检查是否有大量DOM变化
const largeMutations = mutations.filter(m =>
m.addedNodes.length > 10 || m.removedNodes.length > 10
)

if (largeMutations.length > 0) {
console.warn('检测到大量DOM变化,可能影响性能:', largeMutations)
this.reportPerformanceIssue(largeMutations)
}

// 定期重置计数器
this.throttledReset()
})

this.observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false // 不监听属性变化,减少噪音
})
}

throttledReset = (() => {
let timeout
return () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
if (this.mutationCount > this.largeChangeThreshold) {
console.log(`在过去的时间段内发生了 ${this.mutationCount} 次DOM变化`)
}
this.mutationCount = 0
}, 5000)
}
})()

reportPerformanceIssue (mutations) {
// 发送性能警告到监控系统
fetch('/api/performance-warning', {
method: 'POST',
body: JSON.stringify({
type: 'excessive-dom-mutations',
count: mutations.length,
timestamp: Date.now(),
url: window.location.href
})
})
}

disconnect () {
this.observer.disconnect()
}
}

// 4. 第三方脚本监控
function monitorThirdPartyScripts () {
const scriptObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
const src = node.src || 'inline'
console.log('检测到新的脚本:', src)

// 检查是否为未授权的第三方脚本
if (src && !isAuthorizedScript(src)) {
console.warn('未授权的第三方脚本:', src)
// 可以选择移除或报告
reportUnauthorizedScript(src)
}
}
})
})
})

scriptObserver.observe(document.documentElement, {
childList: true,
subtree: true
})
}

function isAuthorizedScript (src) {
const authorizedDomains = [
'cdn.example.com',
'analytics.google.com',
'static.example.com'
]

return authorizedDomains.some(domain => src.includes(domain))
}

// 使用示例
const domWatcher = setupDOMWatcher()
const routeWatcher = new SPARouteWatcher()
const performanceMonitor = new DOMPerformanceMonitor()
monitorThirdPartyScripts()

面试官视角:

要点清单:

  • 理解MutationObserver的异步特性和性能优势
  • 掌握不同观察类型的配置和使用场景
  • 了解如何合理控制观察范围避免性能问题

加分项:

  • 提及在单页应用中的路由监听应用
  • 了解性能监控和安全监控的实际用途
  • 知道何时应该断开观察以避免内存泄漏

常见失误:

  • 过度观察导致性能问题
  • 忘记调用disconnect()造成内存泄漏
  • 不理解异步回调的批量处理特性

延伸阅读:

PerformanceObserver 如何测量页面性能

答案

核心概念:

PerformanceObserver 是 Web Performance API 的核心组件,提供异步、高效的性能数据收集机制。它可以监听各种性能条目类型(如导航、资源、绘制、交互等),实时收集关键性能指标。相比轮询 performance.getEntries(),它提供事件驱动的数据收集方式,减少性能开销并确保数据的及时性。

实际示例:

// 1. 核心Web性能指标监控
class WebVitalsMonitor {
constructor () {
this.metrics = {}
this.initObservers()
}

initObservers () {
// 监听FCP和LCP (绘制性能)
this.observePaint()

// 监听FID和其他交互性能
this.observeInteraction()

// 监听CLS (布局稳定性)
this.observeLayoutShift()

// 监听资源加载性能
this.observeResources()

// 监听导航性能
this.observeNavigation()
}

observePaint () {
const paintObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime
console.log('FCP:', entry.startTime.toFixed(2), 'ms')
}

if (entry.name === 'largest-contentful-paint') {
this.metrics.lcp = entry.startTime
console.log('LCP:', entry.startTime.toFixed(2), 'ms')

// LCP超过2.5秒发出警告
if (entry.startTime > 2500) {
this.reportPerformanceIssue('lcp', entry.startTime)
}
}
}
})

paintObserver.observe({
type: 'paint',
buffered: true // 获取观察器创建前的条目
})

// LCP监听
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.lcp = lastEntry.startTime
console.log('LCP (Latest):', lastEntry.startTime.toFixed(2), 'ms')
})

lcpObserver.observe({
type: 'largest-contentful-paint',
buffered: true
})
}

observeInteraction () {
// First Input Delay (FID)
const fidObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime
console.log('FID:', this.metrics.fid.toFixed(2), 'ms')

// FID超过100ms发出警告
if (this.metrics.fid > 100) {
this.reportPerformanceIssue('fid', this.metrics.fid)
}
}
})

fidObserver.observe({
type: 'first-input',
buffered: true
})
}

observeLayoutShift () {
let clsValue = 0

const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 只计算意外的布局偏移
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}

this.metrics.cls = clsValue
console.log('CLS:', clsValue.toFixed(4))

// CLS超过0.1发出警告
if (clsValue > 0.1) {
this.reportPerformanceIssue('cls', clsValue)
}
})

clsObserver.observe({
type: 'layout-shift',
buffered: true
})
}

observeResources () {
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 分析慢资源
const loadTime = entry.responseEnd - entry.startTime

if (loadTime > 3000) { // 超过3秒的资源
console.warn('慢资源警告:', {
name: entry.name,
loadTime: loadTime.toFixed(2),
size: entry.transferSize,
type: entry.initiatorType
})
}

// 统计不同类型资源的性能
this.analyzeResourceType(entry)
}
})

resourceObserver.observe({
type: 'resource',
buffered: true
})
}

observeNavigation () {
const navObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 计算关键导航时间
const timings = {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ssl: entry.connectEnd - entry.secureConnectionStart,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
domParse: entry.domContentLoadedEventEnd - entry.responseEnd,
total: entry.loadEventEnd - entry.startTime
}

this.metrics.navigation = timings
console.log('导航性能:', timings)

// TTFB超过800ms发出警告
if (timings.ttfb > 800) {
this.reportPerformanceIssue('ttfb', timings.ttfb)
}
}
})

navObserver.observe({
type: 'navigation',
buffered: true
})
}

analyzeResourceType (entry) {
const resourceTypes = ['script', 'link', 'img', 'fetch', 'xmlhttprequest']
const type = entry.initiatorType

if (resourceTypes.includes(type)) {
if (!this.metrics.resources) {
this.metrics.resources = {}
}

if (!this.metrics.resources[type]) {
this.metrics.resources[type] = { count: 0, totalTime: 0, totalSize: 0 }
}

this.metrics.resources[type].count++
this.metrics.resources[type].totalTime += entry.duration
this.metrics.resources[type].totalSize += entry.transferSize || 0
}
}

reportPerformanceIssue (metric, value) {
// 发送性能问题报告
fetch('/api/performance-issues', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metric,
value,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
})
}

getMetricsSummary () {
return {
webVitals: {
fcp: this.metrics.fcp,
lcp: this.metrics.lcp,
fid: this.metrics.fid,
cls: this.metrics.cls
},
navigation: this.metrics.navigation,
resources: this.metrics.resources
}
}
}

// 2. 用户自定义性能标记
class CustomPerformanceTracker {
constructor () {
this.startTimes = new Map()
this.setupObserver()
}

setupObserver () {
const measureObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('自定义测量:', {
name: entry.name,
duration: entry.duration.toFixed(2),
start: entry.startTime.toFixed(2)
})

// 发送自定义性能数据
this.sendCustomMetric(entry)
}
})

measureObserver.observe({
type: 'measure',
buffered: true
})
}

startTimer (name) {
performance.mark(`${name}-start`)
this.startTimes.set(name, performance.now())
}

endTimer (name) {
if (this.startTimes.has(name)) {
performance.mark(`${name}-end`)
performance.measure(name, `${name}-start`, `${name}-end`)
this.startTimes.delete(name)
}
}

sendCustomMetric (entry) {
// 发送到分析系统
if (typeof gtag !== 'undefined') {
gtag('event', 'custom_performance', {
event_category: 'Performance',
event_label: entry.name,
value: Math.round(entry.duration)
})
}
}
}

// 使用示例
const webVitalsMonitor = new WebVitalsMonitor()
const customTracker = new CustomPerformanceTracker()

// 测量自定义操作
customTracker.startTimer('api-call')
fetch('/api/data')
.then(response => response.json())
.then(data => {
customTracker.endTimer('api-call')
})

// 页面卸载时发送性能报告
window.addEventListener('beforeunload', () => {
const metrics = webVitalsMonitor.getMetricsSummary()
navigator.sendBeacon('/api/performance-report', JSON.stringify(metrics))
})

面试官视角:

要点清单:

  • 理解PerformanceObserver的异步性能数据收集机制
  • 掌握Core Web Vitals的监控和分析方法
  • 了解不同性能条目类型的应用场景

加分项:

  • 提及Web性能指标的优化目标和行业标准
  • 了解如何结合RUM(真实用户监控)进行性能分析
  • 知道如何处理性能数据的上报和分析

常见失误:

  • 不理解buffered选项的作用和重要性
  • 忽略observer的disconnect导致内存泄漏
  • 过度收集性能数据影响页面性能

延伸阅读:

requestAnimationFrame 是什么,有什么作用?

答案
方法主要用途执行时机优势典型场景
requestAnimationFrame浏览器动画帧调度重绘前与刷新率同步,节能流畅JS动画、进度条等

补充说明

  • requestAnimationFrame 会在浏览器每次重绘前自动调用回调,通常约每秒60次,能保证动画与屏幕刷新同步,减少卡顿和掉帧。
  • 返回的ID可用于 cancelAnimationFrame 取消动画,适合实现高性能动画循环。
  • 与 setTimeout/setInterval 相比,requestAnimationFrame 更节能且动画更平滑,页面切换到后台时会自动暂停,节省资源。
let id
function loop () {
// 动画逻辑
id = requestAnimationFrame(loop)
}
loop()
// 取消动画
cancelAnimationFrame(id)
提示

推荐所有涉及 DOM 动画的场景优先使用 requestAnimationFrame,提升性能和流畅度。

延伸阅读

canvas 是如何处理复杂事件交互的?

答案

核心概念

Canvas 本身不具备 DOM 事件绑定能力,复杂事件交互需通过监听容器事件并结合图形坐标判定实现。常见做法是监听鼠标或触摸事件,计算事件点在 canvas 内的坐标,再判断是否命中特定图形,实现点击、悬停、拖拽等交互。

详细实现步骤

  • 监听事件:在 canvas 或其父容器上监听 mousemovemousedownmouseuptouchstart 等事件。
  • 坐标转换:通过 getBoundingClientRect 获取 canvas 的位置,将事件的 clientX/Y 转换为 canvas 内部坐标。
  • 命中检测:遍历所有图形对象,判断事件点是否落在某个图形区域(如圆形、矩形等)。
  • 状态管理:可维护图形对象数组,记录每个图形的状态(如选中、悬停),实现动态交互效果。

代码示例

const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
const shapes = [
{ type: 'circle', x: 100, y: 100, radius: 50 },
{ type: 'rectangle', x: 200, y: 200, width: 100, height: 50 }
]

canvas.addEventListener('mousemove', function (event) {
const rect = canvas.getBoundingClientRect()
const mouseX = event.clientX - rect.left
const mouseY = event.clientY - rect.top
shapes.forEach(shape => {
if (shape.type === 'circle' && isPointInCircle(mouseX, mouseY, shape)) {
ctx.fillStyle = 'red'
} else {
ctx.fillStyle = 'blue'
}
drawShape(shape)
})
})

function isPointInCircle (x, y, { x: cx, y: cy, radius }) {
const dx = x - cx
const dy = y - cy
return dx * dx + dy * dy <= radius * radius
}

function drawShape (shape) {
if (shape.type === 'circle') {
ctx.beginPath()
ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2)
ctx.fill()
} else if (shape.type === 'rectangle') {
ctx.fillRect(shape.x, shape.y, shape.width, shape.height)
}
}

开发建议

  • 复杂交互建议封装图形对象和事件处理逻辑,或使用如 Fabric.js、Konva.js 等第三方库简化开发。
  • 需注意每次交互后需清空并重绘 canvas,避免残影。

延伸阅读

HTML5 <video><audio> 元素如何使用?

答案

核心概念:

HTML5 媒体元素提供原生的音视频播放功能:

  • <video> - 视频播放元素,支持多种格式和控制
  • <audio> - 音频播放元素,轻量级音频解决方案
  • controls 属性 - 显示播放控制界面
  • autoplay 属性 - 自动播放(需考虑用户体验)
  • loop 属性 - 循环播放
  • muted 属性 - 静音播放(绕过autoplay限制)
  • poster 属性 - 视频封面图片
  • 支持多种格式兼容和响应式适配

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTML5 媒体元素使用示例</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
            color: #333;
            background: #f8f9fa;
        }

        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 20px rgba(0,0,0,0.1);
        }

        h1 {
            color: #2c3e50;
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 15px;
            border-bottom: 3px solid #e74c3c;
        }

        .demo-section {
            margin: 40px 0;
            padding: 25px;
            background: #f8f9fa;
            border-radius: 8px;
            border-left: 4px solid #e74c3c;
        }

        .demo-section h2 {
            color: #e74c3c;
            margin-bottom: 20px;
        }

        .media-demo {
            background: white;
            padding: 20px;
            margin: 15px 0;
            border-radius: 6px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            position: relative;
        }

        .element-tag {
            position: absolute;
            top: -10px;
            right: 10px;
            background: #e74c3c;
            color: white;
            padding: 2px 8px;
            font-size: 11px;
            border-radius: 3px;
            font-weight: bold;
        }

        /* 媒体元素样式 */
        video, audio {
            width: 100%;
            max-width: 600px;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        }

        .media-container {
            text-align: center;
            margin: 20px 0;
        }

        .media-info {
            background: #e8f4f8;
            border: 1px solid #bee5eb;
            border-radius: 6px;
            padding: 15px;
            margin: 15px 0;
            font-size: 14px;
        }

        .media-info h4 {
            margin-top: 0;
            color: #0c5460;
        }

        /* 自定义媒体播放器样式 */
        .custom-player {
            background: #2c3e50;
            border-radius: 8px;
            padding: 20px;
            color: white;
        }

        .custom-player video {
            width: 100%;
            background: black;
            border-radius: 4px;
        }

        .custom-controls {
            display: flex;
            align-items: center;
            gap: 15px;
            margin-top: 15px;
            padding: 10px;
            background: #34495e;
            border-radius: 6px;
        }

        .control-btn {
            background: #3498db;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }

        .control-btn:hover {
            background: #2980b9;
        }

        .control-btn:disabled {
            background: #7f8c8d;
            cursor: not-allowed;
        }

        .progress-container {
            flex: 1;
            height: 6px;
            background: #7f8c8d;
            border-radius: 3px;
            cursor: pointer;
            position: relative;
        }

        .progress-bar {
            height: 100%;
            background: #e74c3c;
            border-radius: 3px;
            width: 0%;
            transition: width 0.1s;
        }

        .time-display {
            font-size: 14px;
            color: #ecf0f1;
            min-width: 80px;
        }

        .volume-container {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .volume-slider {
            width: 80px;
        }

        /* 响应式视频容器 */
        .responsive-video {
            position: relative;
            width: 100%;
            height: 0;
            padding-bottom: 56.25%; /* 16:9 比例 */
            background: #000;
            border-radius: 8px;
            overflow: hidden;
        }

        .responsive-video video {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        /* 音频播放器样式 */
        .audio-player {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 12px;
            padding: 20px;
            color: white;
            text-align: center;
        }

        .audio-player audio {
            width: 100%;
            margin: 15px 0;
        }

        .audio-info {
            margin-bottom: 15px;
        }

        .audio-title {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 5px;
        }

        .audio-artist {
            opacity: 0.8;
            font-size: 14px;
        }

        /* 特性展示网格 */
        .features-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 20px;
            margin: 25px 0;
        }

        .feature-card {
            background: white;
            border: 1px solid #e1e5e9;
            border-radius: 8px;
            padding: 20px;
            text-align: center;
        }

        .feature-card h4 {
            color: #2c3e50;
            margin-bottom: 15px;
        }

        .feature-icon {
            font-size: 48px;
            margin-bottom: 15px;
        }

        /* 代码示例 */
        .code-example {
            background: #f8f9fa;
            border: 1px solid #e9ecef;
            border-radius: 6px;
            padding: 15px;
            margin: 15px 0;
            font-family: 'Courier New', monospace;
            font-size: 13px;
            overflow-x: auto;
        }

        /* 状态指示器 */
        .status-indicator {
            display: inline-block;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            margin-right: 8px;
        }

        .status-ready { background: #28a745; }
        .status-loading { background: #ffc107; }
        .status-error { background: #dc3545; }

        /* 提示框 */
        .tip-box {
            background: #d1ecf1;
            border: 1px solid #bee5eb;
            border-radius: 6px;
            padding: 15px;
            margin: 20px 0;
        }

        .tip-box strong {
            color: #0c5460;
        }

        /* 响应式 */
        @media (max-width: 768px) {
            .custom-controls {
                flex-wrap: wrap;
                gap: 10px;
            }
            
            .features-grid {
                grid-template-columns: 1fr;
            }
            
            .time-display {
                min-width: auto;
                font-size: 12px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>HTML5 媒体元素完整指南</h1>

        <!-- 视频元素基础用法 -->
        <div class="demo-section">
            <h2>1. &lt;video&gt; 视频元素基础用法</h2>

            <div class="media-demo">
                <span class="element-tag">VIDEO</span>
                <h3>基础视频播放器</h3>
                <div class="media-container">
                    <!-- 使用生成的测试视频 -->
                    <video id="basicVideo" controls poster="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjMzOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMzQ5OGRiIi8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyNCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7op4bpopHlsIHpnaI8L3RleHQ+Cjwvc3ZnPg==">
                        <source src="data:video/mp4;base64," type="video/mp4">
                        您的浏览器不支持视频标签。
                    </video>
                </div>
                
                <div class="media-info">
                    <h4>基础属性说明:</h4>
                    <ul>
                        <li><code>controls</code> - 显示播放控制界面</li>
                        <li><code>poster</code> - 视频加载前显示的封面图</li>
                        <li><code>width/height</code> - 设置视频尺寸</li>
                        <li><code>preload</code> - 预加载策略(none/metadata/auto)</li>
                    </ul>
                </div>
                
                <div class="code-example">
&lt;video controls poster="cover.jpg" preload="metadata"&gt;
  &lt;source src="video.mp4" type="video/mp4"&gt;
  &lt;source src="video.webm" type="video/webm"&gt;
  您的浏览器不支持视频标签。
&lt;/video&gt;
                </div>
            </div>

            <div class="media-demo">
                <span class="element-tag">VIDEO</span>
                <h3>响应式视频容器</h3>
                <div class="responsive-video">
                    <video controls>
                        <source src="data:video/mp4;base64," type="video/mp4">
                        您的浏览器不支持视频标签。
                    </video>
                </div>
                
                <div class="media-info">
                    <h4>响应式设计要点:</h4>
                    <ul>
                        <li>使用相对单位和百分比设置尺寸</li>
                        <li>保持宽高比使用padding-bottom技巧</li>
                        <li>考虑移动端的数据流量和性能</li>
                    </ul>
                </div>
            </div>
        </div>

        <!-- 音频元素用法 -->
        <div class="demo-section">
            <h2>2. &lt;audio&gt; 音频元素</h2>

            <div class="media-demo">
                <span class="element-tag">AUDIO</span>
                <h3>音频播放器</h3>
                <div class="audio-player">
                    <div class="audio-info">
                        <div class="audio-title">示例音频文件</div>
                        <div class="audio-artist">HTML5 Audio Demo</div>
                    </div>
                    <audio id="basicAudio" controls>
                        <source src="data:audio/mp3;base64," type="audio/mp3">
                        <source src="data:audio/ogg;base64," type="audio/ogg">
                        您的浏览器不支持音频标签。
                    </audio>
                </div>
                
                <div class="media-info">
                    <h4>音频特有属性:</h4>
                    <ul>
                        <li>通常不需要poster属性</li>
                        <li>可以设置为背景音乐(谨慎使用autoplay)</li>
                        <li>支持多种音频格式:MP3、OGG、WAV等</li>
                        <li>移动端可能需要用户交互才能播放</li>
                    </ul>
                </div>
            </div>
        </div>

        <!-- 自定义媒体控制器 -->
        <div class="demo-section">
            <h2>3. 自定义媒体控制器</h2>

            <div class="media-demo">
                <span class="element-tag">CUSTOM</span>
                <h3>自定义视频播放器</h3>
                <div class="custom-player">
                    <video id="customVideo" width="100%">
                        <source src="data:video/mp4;base64," type="video/mp4">
                        您的浏览器不支持视频标签。
                    </video>
                    
                    <div class="custom-controls">
                        <button class="control-btn" id="playPauseBtn">▶️</button>
                        <button class="control-btn" id="stopBtn">⏹️</button>
                        <div class="progress-container" id="progressContainer">
                            <div class="progress-bar" id="progressBar"></div>
                        </div>
                        <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
                        <div class="volume-container">
                            <span>🔊</span>
                            <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.1" value="1">
                        </div>
                        <button class="control-btn" id="fullscreenBtn"></button>
                    </div>
                </div>
                
                <div class="media-info">
                    <h4>自定义控制器优势:</h4>
                    <ul>
                        <li>统一的跨浏览器用户体验</li>
                        <li>符合网站设计风格</li>
                        <li>可添加自定义功能(字幕、倍速等)</li>
                        <li>更好的移动端适配</li>
                    </ul>
                </div>
            </div>
        </div>

        <!-- 媒体元素特性 -->
        <div class="demo-section">
            <h2>4. 媒体元素特性展示</h2>

            <div class="features-grid">
                <div class="feature-card">
                    <div class="feature-icon">🎬</div>
                    <h4>多格式支持</h4>
                    <p>MP4、WebM、OGG等多种格式,提供最佳兼容性</p>
                </div>

                <div class="feature-card">
                    <div class="feature-icon">📱</div>
                    <h4>响应式设计</h4>
                    <p>自适应不同屏幕尺寸,优化移动端体验</p>
                </div>

                <div class="feature-card">
                    <div class="feature-icon"></div>
                    <h4>性能优化</h4>
                    <p>智能预加载策略,减少带宽消耗</p>
                </div>

                <div class="feature-card">
                    <div class="feature-icon">🎛️</div>
                    <h4>API 控制</h4>
                    <p>丰富的JavaScript API,实现复杂交互</p>
                </div>

                <div class="feature-card">
                    <div class="feature-icon"></div>
                    <h4>无障碍访问</h4>
                    <p>支持字幕、音频描述等辅助功能</p>
                </div>

                <div class="feature-card">
                    <div class="feature-icon">🔒</div>
                    <h4>安全策略</h4>
                    <p>遵循现代浏览器的自动播放策略</p>
                </div>
            </div>
        </div>

        <!-- 浏览器支持和最佳实践 -->
        <div class="demo-section">
            <h2>5. 浏览器支持检测</h2>

            <div class="media-demo">
                <span class="element-tag">DETECT</span>
                <h3>格式支持检测</h3>
                <div id="formatSupport">
                    <h4>您的浏览器支持情况:</h4>
                    <div id="supportList"></div>
                </div>
                
                <div class="code-example">
// 检测视频格式支持
const video = document.createElement('video');
const formats = {
    'MP4': 'video/mp4',
    'WebM': 'video/webm',
    'OGG': 'video/ogg'
};

Object.entries(formats).forEach(([name, type]) => {
    const support = video.canPlayType(type);
    console.log(`${name}: ${support}`);
});
                </div>
            </div>
        </div>

        <div class="tip-box">
            <strong>💡 媒体元素最佳实践:</strong>
            <ul>
                <li><strong>格式兼容性</strong>:提供多种格式确保跨浏览器支持</li>
                <li><strong>预加载策略</strong>:根据内容重要性选择合适的preload值</li>
                <li><strong>自动播放</strong>:遵循浏览器政策,静音视频更容易自动播放</li>
                <li><strong>移动优化</strong>:考虑数据流量,提供画质选择</li>
                <li><strong>无障碍访问</strong>:提供字幕track和音频描述</li>
                <li><strong>性能监控</strong>:监听加载和错误事件</li>
            </ul>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            console.log('=== HTML5 媒体元素演示 ===');
            
            // 格式支持检测
            checkFormatSupport();
            
            // 初始化自定义播放器
            initCustomPlayer();
            
            // 媒体事件监听演示
            setupMediaEventListeners();
        });

        // 检测媒体格式支持
        function checkFormatSupport() {
            const video = document.createElement('video');
            const audio = document.createElement('audio');
            const supportList = document.getElementById('supportList');
            
            const formats = {
                '视频格式': {
                    'MP4 (H.264)': 'video/mp4; codecs="avc1.42E01E"',
                    'WebM (VP8)': 'video/webm; codecs="vp8"',
                    'WebM (VP9)': 'video/webm; codecs="vp9"',
                    'OGG (Theora)': 'video/ogg; codecs="theora"'
                },
                '音频格式': {
                    'MP3': 'audio/mpeg',
                    'AAC': 'audio/mp4; codecs="mp4a.40.2"',
                    'OGG Vorbis': 'audio/ogg; codecs="vorbis"',
                    'WebM Opus': 'audio/webm; codecs="opus"'
                }
            };
            
            let html = '';
            
            Object.entries(formats).forEach(([category, formatList]) => {
                html += `<h5>${category}:</h5><ul>`;
                Object.entries(formatList).forEach(([name, type]) => {
                    const element = category === '视频格式' ? video : audio;
                    const support = element.canPlayType(type);
                    const status = support === 'probably' ? 'ready' : 
                                 support === 'maybe' ? 'loading' : 'error';
                    const text = support === 'probably' ? '完全支持' :
                                support === 'maybe' ? '可能支持' : '不支持';
                    
                    html += `<li><span class="status-indicator status-${status}"></span>${name}: ${text}</li>`;
                });
                html += '</ul>';
            });
            
            supportList.innerHTML = html;
        }

        // 初始化自定义播放器
        function initCustomPlayer() {
            const video = document.getElementById('customVideo');
            const playPauseBtn = document.getElementById('playPauseBtn');
            const stopBtn = document.getElementById('stopBtn');
            const progressContainer = document.getElementById('progressContainer');
            const progressBar = document.getElementById('progressBar');
            const timeDisplay = document.getElementById('timeDisplay');
            const volumeSlider = document.getElementById('volumeSlider');
            const fullscreenBtn = document.getElementById('fullscreenBtn');
            
            // 生成测试视频(彩色渐变动画)
            generateTestVideo(video);
            
            // 播放/暂停
            playPauseBtn.addEventListener('click', function() {
                if (video.paused) {
                    video.play().then(() => {
                        playPauseBtn.textContent = '⏸️';
                    }).catch(e => console.log('播放失败:', e));
                } else {
                    video.pause();
                    playPauseBtn.textContent = '▶️';
                }
            });
            
            // 停止
            stopBtn.addEventListener('click', function() {
                video.pause();
                video.currentTime = 0;
                playPauseBtn.textContent = '▶️';
            });
            
            // 进度条点击
            progressContainer.addEventListener('click', function(e) {
                const rect = this.getBoundingClientRect();
                const pos = (e.clientX - rect.left) / rect.width;
                video.currentTime = pos * video.duration;
            });
            
            // 时间更新
            video.addEventListener('timeupdate', function() {
                if (video.duration) {
                    const progress = (video.currentTime / video.duration) * 100;
                    progressBar.style.width = progress + '%';
                    
                    const current = formatTime(video.currentTime);
                    const total = formatTime(video.duration);
                    timeDisplay.textContent = `${current} / ${total}`;
                }
            });
            
            // 音量控制
            volumeSlider.addEventListener('input', function() {
                video.volume = this.value;
            });
            
            // 全屏
            fullscreenBtn.addEventListener('click', function() {
                if (video.requestFullscreen) {
                    video.requestFullscreen();
                } else if (video.webkitRequestFullscreen) {
                    video.webkitRequestFullscreen();
                } else if (video.msRequestFullscreen) {
                    video.msRequestFullscreen();
                }
            });
            
            // 视频事件
            video.addEventListener('play', () => console.log('视频开始播放'));
            video.addEventListener('pause', () => console.log('视频暂停'));
            video.addEventListener('ended', () => {
                playPauseBtn.textContent = '▶️';
                console.log('视频播放结束');
            });
        }

        // 生成测试视频(Canvas动画)
        function generateTestVideo(videoElement) {
            const canvas = document.createElement('canvas');
            canvas.width = 640;
            canvas.height = 360;
            const ctx = canvas.getContext('2d');
            
            let frame = 0;
            const fps = 30;
            const duration = 10; // 10秒
            const totalFrames = fps * duration;
            
            function drawFrame() {
                // 清除画布
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                // 绘制彩色渐变背景
                const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
                const hue1 = (frame * 2) % 360;
                const hue2 = (frame * 3) % 360;
                gradient.addColorStop(0, `hsl(${hue1}, 70%, 50%)`);
                gradient.addColorStop(1, `hsl(${hue2}, 70%, 50%)`);
                
                ctx.fillStyle = gradient;
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                
                // 绘制动画圆圈
                const centerX = canvas.width / 2;
                const centerY = canvas.height / 2;
                const radius = 50 + Math.sin(frame * 0.1) * 30;
                
                ctx.beginPath();
                ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.fill();
                
                // 绘制文字
                ctx.fillStyle = '#333';
                ctx.font = 'bold 24px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('HTML5 Video Demo', centerX, centerY - 10);
                ctx.font = '16px Arial';
                ctx.fillText(`Frame: ${frame}/${totalFrames}`, centerX, centerY + 20);
                
                frame++;
                if (frame <= totalFrames) {
                    setTimeout(drawFrame, 1000 / fps);
                }
            }
            
            // 开始绘制
            drawFrame();
            
            // 将canvas转换为视频流(如果支持)
            if (canvas.captureStream) {
                const stream = canvas.captureStream(fps);
                videoElement.srcObject = stream;
            }
        }

        // 设置媒体事件监听
        function setupMediaEventListeners() {
            const events = [
                'loadstart', 'durationchange', 'loadedmetadata', 'loadeddata',
                'progress', 'canplay', 'canplaythrough', 'play', 'pause',
                'seeking', 'seeked', 'ended', 'error', 'volumechange'
            ];
            
            const basicVideo = document.getElementById('basicVideo');
            const basicAudio = document.getElementById('basicAudio');
            
            events.forEach(eventType => {
                [basicVideo, basicAudio].forEach((element, index) => {
                    const mediaType = index === 0 ? 'Video' : 'Audio';
                    element.addEventListener(eventType, function() {
                        console.log(`${mediaType} ${eventType}:`, this.currentTime);
                    });
                });
            });
        }

        // 时间格式化函数
        function formatTime(seconds) {
            const mins = Math.floor(seconds / 60);
            const secs = Math.floor(seconds % 60);
            return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
        }

        // 媒体查询响应
        const mediaQuery = window.matchMedia('(max-width: 768px)');
        function handleMobileView(e) {
            if (e.matches) {
                console.log('切换到移动端视图');
                // 可以在这里调整媒体元素的行为
            } else {
                console.log('切换到桌面端视图');
            }
        }
        
        mediaQuery.addListener(handleMobileView);
        handleMobileView(mediaQuery);
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 了解video和audio元素的基本属性
  • 知道如何处理多格式兼容性
  • 理解autoplay策略和用户体验影响

加分项:

  • 提及现代浏览器的autoplay限制
  • 了解preload属性的性能优化作用
  • 知道如何创建自定义媒体控制器

常见失误:

  • 忽略浏览器兼容性问题
  • 不考虑移动端的数据流量
  • 滥用autoplay影响用户体验

延伸阅读:

媒体 API 事件和控制方法有哪些?

答案

核心概念:

HTML5 媒体API提供丰富的事件和控制方法:

  • 控制方法: play(), pause(), load(), canPlayType()
  • 属性控制: currentTime, volume, playbackRate, duration
  • 状态属性: paused, ended, readyState, networkState
  • 关键事件: loadstart, canplay, play, pause, ended, error
  • 进度事件: loadeddata, progress, timeupdate
  • 错误处理: error事件和error属性
  • 支持programmatic控制和用户交互响应

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>媒体 API 控制与事件处理</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
            color: #333;
            background: #f8f9fa;
        }

        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 20px rgba(0,0,0,0.1);
        }

        h1 {
            color: #2c3e50;
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 15px;
            border-bottom: 3px solid #3498db;
        }

        .demo-section {
            margin: 40px 0;
            padding: 25px;
            background: #f8f9fa;
            border-radius: 8px;
            border-left: 4px solid #3498db;
        }

        .demo-section h2 {
            color: #3498db;
            margin-bottom: 20px;
        }

        .api-demo {
            background: white;
            padding: 20px;
            margin: 15px 0;
            border-radius: 6px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            position: relative;
        }

        .element-tag {
            position: absolute;
            top: -10px;
            right: 10px;
            background: #3498db;
            color: white;
            padding: 2px 8px;
            font-size: 11px;
            border-radius: 3px;
            font-weight: bold;
        }

        /* 媒体播放器样式 */
        .media-player {
            background: #2c3e50;
            border-radius: 10px;
            padding: 20px;
            color: white;
            margin: 20px 0;
        }

        .media-player video {
            width: 100%;
            border-radius: 6px;
            background: black;
        }

        /* 控制面板 */
        .control-panel {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin-top: 20px;
            padding: 15px;
            background: #34495e;
            border-radius: 6px;
        }

        .control-group {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .control-group label {
            font-size: 12px;
            opacity: 0.8;
            text-transform: uppercase;
        }

        .control-btn {
            background: #3498db;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
        }

        .control-btn:hover {
            background: #2980b9;
            transform: translateY(-1px);
        }

        .control-btn:active {
            transform: translateY(0);
        }

        .control-btn:disabled {
            background: #7f8c8d;
            cursor: not-allowed;
            transform: none;
        }

        .control-input {
            padding: 8px;
            border: 1px solid #7f8c8d;
            border-radius: 4px;
            background: white;
            color: #333;
        }

        /* 状态显示 */
        .status-panel {
            background: #ecf0f1;
            border-radius: 6px;
            padding: 15px;
            margin: 15px 0;
            font-family: 'Courier New', monospace;
            font-size: 13px;
        }

        .status-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
        }

        .status-item {
            background: white;
            padding: 10px;
            border-radius: 4px;
            border-left: 3px solid #3498db;
        }

        .status-label {
            font-weight: bold;
            color: #2c3e50;
            font-size: 11px;
            text-transform: uppercase;
        }

        .status-value {
            color: #e74c3c;
            font-weight: bold;
            margin-top: 2px;
        }

        /* 事件日志 */
        .event-log {
            background: #2c3e50;
            color: #ecf0f1;
            border-radius: 6px;
            padding: 15px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            max-height: 200px;
            overflow-y: auto;
            margin: 15px 0;
        }

        .event-log .event-item {
            padding: 2px 0;
            border-bottom: 1px solid #34495e;
        }

        .event-log .event-item:last-child {
            border-bottom: none;
        }

        .event-time {
            color: #95a5a6;
        }

        .event-type {
            color: #3498db;
            font-weight: bold;
        }

        .event-data {
            color: #e74c3c;
        }

        /* 进度条 */
        .progress-section {
            margin: 20px 0;
        }

        .progress-bar-container {
            position: relative;
            height: 30px;
            background: #ecf0f1;
            border-radius: 15px;
            overflow: hidden;
            cursor: pointer;
        }

        .progress-bar {
            height: 100%;
            background: linear-gradient(90deg, #3498db, #2ecc71);
            width: 0%;
            transition: width 0.1s;
            border-radius: 15px;
        }

        .progress-text {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 12px;
            font-weight: bold;
            color: white;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
        }

        /* 音量可视化 */
        .volume-visualizer {
            display: flex;
            align-items: flex-end;
            height: 40px;
            gap: 2px;
            margin: 10px 0;
        }

        .volume-bar {
            background: #3498db;
            width: 4px;
            transition: height 0.1s;
            border-radius: 2px;
        }

        /* 播放速度控制 */
        .speed-control {
            display: flex;
            gap: 5px;
            margin: 10px 0;
        }

        .speed-btn {
            background: #95a5a6;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
        }

        .speed-btn.active {
            background: #e74c3c;
        }

        /* 错误显示 */
        .error-display {
            background: #e74c3c;
            color: white;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
            display: none;
        }

        /* 响应式 */
        @media (max-width: 768px) {
            .control-panel {
                grid-template-columns: 1fr;
            }
            
            .status-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>媒体 API 控制与事件处理</h1>

        <!-- 主要演示区域 -->
        <div class="demo-section">
            <h2>1. 完整的媒体控制演示</h2>

            <div class="api-demo">
                <span class="element-tag">API DEMO</span>
                <div class="media-player">
                    <video id="mainVideo" muted>
                        <source src="#" type="video/mp4">
                        您的浏览器不支持视频标签。
                    </video>
                    
                    <!-- 自定义进度条 -->
                    <div class="progress-section">
                        <div class="progress-bar-container" id="progressContainer">
                            <div class="progress-bar" id="progressBar"></div>
                            <div class="progress-text" id="progressText">00:00 / 00:00</div>
                        </div>
                    </div>
                    
                    <!-- 控制面板 -->
                    <div class="control-panel">
                        <div class="control-group">
                            <label>播放控制</label>
                            <button class="control-btn" id="playBtn">播放</button>
                            <button class="control-btn" id="pauseBtn">暂停</button>
                            <button class="control-btn" id="stopBtn">停止</button>
                        </div>
                        
                        <div class="control-group">
                            <label>跳转控制</label>
                            <button class="control-btn" id="rewindBtn">后退 10s</button>
                            <button class="control-btn" id="forwardBtn">前进 10s</button>
                            <input type="number" class="control-input" id="seekInput" placeholder="跳转到(秒)" min="0">
                        </div>
                        
                        <div class="control-group">
                            <label>音量控制</label>
                            <input type="range" class="control-input" id="volumeSlider" min="0" max="1" step="0.1" value="0.5">
                            <button class="control-btn" id="muteBtn">静音</button>
                        </div>
                        
                        <div class="control-group">
                            <label>其他控制</label>
                            <button class="control-btn" id="fullscreenBtn">全屏</button>
                            <button class="control-btn" id="pipBtn">画中画</button>
                            <button class="control-btn" id="reloadBtn">重新加载</button>
                        </div>
                    </div>
                    
                    <!-- 播放速度控制 -->
                    <div class="control-group">
                        <label>播放速度</label>
                        <div class="speed-control">
                            <button class="speed-btn" data-speed="0.5">0.5x</button>
                            <button class="speed-btn" data-speed="0.75">0.75x</button>
                            <button class="speed-btn active" data-speed="1">1x</button>
                            <button class="speed-btn" data-speed="1.25">1.25x</button>
                            <button class="speed-btn" data-speed="1.5">1.5x</button>
                            <button class="speed-btn" data-speed="2">2x</button>
                        </div>
                    </div>
                </div>
                
                <!-- 错误显示 -->
                <div class="error-display" id="errorDisplay"></div>
            </div>
        </div>

        <!-- 状态监控 -->
        <div class="demo-section">
            <h2>2. 媒体状态实时监控</h2>

            <div class="api-demo">
                <span class="element-tag">STATUS</span>
                <div class="status-panel">
                    <div class="status-grid">
                        <div class="status-item">
                            <div class="status-label">播放状态</div>
                            <div class="status-value" id="playState">暂停</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">当前时间</div>
                            <div class="status-value" id="currentTime">0</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">总时长</div>
                            <div class="status-value" id="duration">0</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">缓冲时间</div>
                            <div class="status-value" id="buffered">0</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">音量</div>
                            <div class="status-value" id="volume">50%</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">播放速度</div>
                            <div class="status-value" id="playbackRate">1x</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">就绪状态</div>
                            <div class="status-value" id="readyState">0</div>
                        </div>
                        <div class="status-item">
                            <div class="status-label">网络状态</div>
                            <div class="status-value" id="networkState">0</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- 事件监听 -->
        <div class="demo-section">
            <h2>3. 媒体事件实时监听</h2>

            <div class="api-demo">
                <span class="element-tag">EVENTS</span>
                <div style="display: flex; gap: 10px; margin-bottom: 10px;">
                    <button class="control-btn" id="clearLogBtn">清空日志</button>
                    <button class="control-btn" id="pauseLogBtn">暂停日志</button>
                    <button class="control-btn" id="exportLogBtn">导出日志</button>
                </div>
                <div class="event-log" id="eventLog">
                    <div class="event-item">
                        <span class="event-time">[00:00:00]</span> 
                        <span class="event-type">INIT</span> 
                        <span class="event-data">事件监听器已启动</span>
                    </div>
                </div>
            </div>
        </div>

        <!-- API 方法测试 -->
        <div class="demo-section">
            <h2>4. 媒体 API 方法测试</h2>

            <div class="api-demo">
                <span class="element-tag">API TEST</span>
                <div class="control-panel">
                    <div class="control-group">
                        <label>canPlayType() 测试</label>
                        <button class="control-btn" onclick="testCanPlayType()">测试格式支持</button>
                        <div id="canPlayResult" style="margin-top: 10px; font-size: 12px;"></div>
                    </div>
                    
                    <div class="control-group">
                        <label>load() 方法</label>
                        <button class="control-btn" onclick="testLoad()">重新加载媒体</button>
                    </div>
                    
                    <div class="control-group">
                        <label>fastSeek() 方法</label>
                        <input type="number" id="fastSeekInput" class="control-input" placeholder="快速跳转(秒)" min="0">
                        <button class="control-btn" onclick="testFastSeek()">快速跳转</button>
                    </div>
                    
                    <div class="control-group">
                        <label>媒体源切换</label>
                        <select id="sourceSelect" class="control-input">
                            <option value="test1">测试视频 1</option>
                            <option value="test2">测试视频 2</option>
                            <option value="test3">测试视频 3</option>
                        </select>
                        <button class="control-btn" onclick="switchSource()">切换源</button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        class MediaController {
            constructor(videoElement) {
                this.video = videoElement;
                this.eventLog = document.getElementById('eventLog');
                this.logPaused = false;
                this.eventCount = 0;
                
                this.initializeVideo();
                this.setupControls();
                this.setupEventListeners();
                this.startStatusUpdate();
            }
            
            initializeVideo() {
                // 生成测试视频
                this.generateTestVideo();
            }
            
            generateTestVideo() {
                const canvas = document.createElement('canvas');
                canvas.width = 800;
                canvas.height = 450;
                const ctx = canvas.getContext('2d');
                
                let frame = 0;
                const fps = 30;
                const duration = 30; // 30秒测试视频
                
                const animate = () => {
                    // 清除画布
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    
                    // 绘制动态背景
                    const time = frame / fps;
                    const gradient = ctx.createRadialGradient(
                        canvas.width/2, canvas.height/2, 0,
                        canvas.width/2, canvas.height/2, Math.max(canvas.width, canvas.height)/2
                    );
                    
                    const hue1 = (time * 30) % 360;
                    const hue2 = (time * 50) % 360;
                    gradient.addColorStop(0, `hsl(${hue1}, 70%, 60%)`);
                    gradient.addColorStop(1, `hsl(${hue2}, 70%, 30%)`);
                    
                    ctx.fillStyle = gradient;
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    
                    // 绘制动画元素
                    const centerX = canvas.width / 2;
                    const centerY = canvas.height / 2;
                    
                    for (let i = 0; i < 5; i++) {
                        const angle = (time + i * 0.5) * 2;
                        const radius = 100 + i * 30;
                        const x = centerX + Math.cos(angle) * radius;
                        const y = centerY + Math.sin(angle) * radius;
                        
                        ctx.beginPath();
                        ctx.arc(x, y, 20, 0, Math.PI * 2);
                        ctx.fillStyle = `hsla(${(time * 100 + i * 60) % 360}, 80%, 70%, 0.8)`;
                        ctx.fill();
                    }
                    
                    // 绘制时间信息
                    ctx.fillStyle = 'white';
                    ctx.font = 'bold 32px Arial';
                    ctx.textAlign = 'center';
                    ctx.strokeStyle = 'black';
                    ctx.lineWidth = 2;
                    
                    const timeText = `${Math.floor(time / 60)}:${Math.floor(time % 60).toString().padStart(2, '0')}`;
                    ctx.strokeText(timeText, centerX, centerY - 20);
                    ctx.fillText(timeText, centerX, centerY - 20);
                    
                    ctx.font = '18px Arial';
                    const frameText = `Frame: ${frame}`;
                    ctx.strokeText(frameText, centerX, centerY + 20);
                    ctx.fillText(frameText, centerX, centerY + 20);
                    
                    frame++;
                    
                    if (time < duration) {
                        setTimeout(animate, 1000 / fps);
                    }
                };
                
                animate();
                
                // 将canvas设置为视频源
                if (canvas.captureStream) {
                    const stream = canvas.captureStream(fps);
                    this.video.srcObject = stream;
                }
            }
            
            setupControls() {
                // 播放控制
                document.getElementById('playBtn').addEventListener('click', () => {
                    this.video.play().catch(e => this.logEvent('ERROR', `播放失败: ${e.message}`));
                });
                
                document.getElementById('pauseBtn').addEventListener('click', () => {
                    this.video.pause();
                });
                
                document.getElementById('stopBtn').addEventListener('click', () => {
                    this.video.pause();
                    this.video.currentTime = 0;
                });
                
                // 跳转控制
                document.getElementById('rewindBtn').addEventListener('click', () => {
                    this.video.currentTime = Math.max(0, this.video.currentTime - 10);
                });
                
                document.getElementById('forwardBtn').addEventListener('click', () => {
                    this.video.currentTime = Math.min(this.video.duration, this.video.currentTime + 10);
                });
                
                document.getElementById('seekInput').addEventListener('change', (e) => {
                    const time = parseFloat(e.target.value);
                    if (!isNaN(time)) {
                        this.video.currentTime = time;
                    }
                });
                
                // 音量控制
                document.getElementById('volumeSlider').addEventListener('input', (e) => {
                    this.video.volume = e.target.value;
                });
                
                document.getElementById('muteBtn').addEventListener('click', () => {
                    this.video.muted = !this.video.muted;
                    document.getElementById('muteBtn').textContent = this.video.muted ? '取消静音' : '静音';
                });
                
                // 全屏和画中画
                document.getElementById('fullscreenBtn').addEventListener('click', () => {
                    if (this.video.requestFullscreen) {
                        this.video.requestFullscreen();
                    }
                });
                
                document.getElementById('pipBtn').addEventListener('click', () => {
                    if (this.video.requestPictureInPicture) {
                        this.video.requestPictureInPicture().catch(e => 
                            this.logEvent('ERROR', `画中画失败: ${e.message}`)
                        );
                    }
                });
                
                document.getElementById('reloadBtn').addEventListener('click', () => {
                    this.video.load();
                });
                
                // 进度条点击
                document.getElementById('progressContainer').addEventListener('click', (e) => {
                    const rect = e.currentTarget.getBoundingClientRect();
                    const pos = (e.clientX - rect.left) / rect.width;
                    this.video.currentTime = pos * this.video.duration;
                });
                
                // 播放速度控制
                document.querySelectorAll('.speed-btn').forEach(btn => {
                    btn.addEventListener('click', () => {
                        const speed = parseFloat(btn.dataset.speed);
                        this.video.playbackRate = speed;
                        
                        document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
                        btn.classList.add('active');
                    });
                });
                
                // 日志控制
                document.getElementById('clearLogBtn').addEventListener('click', () => {
                    this.clearLog();
                });
                
                document.getElementById('pauseLogBtn').addEventListener('click', () => {
                    this.logPaused = !this.logPaused;
                    document.getElementById('pauseLogBtn').textContent = this.logPaused ? '恢复日志' : '暂停日志';
                });
                
                document.getElementById('exportLogBtn').addEventListener('click', () => {
                    this.exportLog();
                });
            }
            
            setupEventListeners() {
                const events = [
                    'loadstart', 'durationchange', 'loadedmetadata', 'loadeddata',
                    'progress', 'canplay', 'canplaythrough', 'play', 'playing',
                    'pause', 'seeking', 'seeked', 'ended', 'timeupdate',
                    'volumechange', 'ratechange', 'resize', 'enterpictureinpicture',
                    'leavepictureinpicture', 'error', 'abort', 'emptied',
                    'stalled', 'suspend', 'waiting'
                ];
                
                events.forEach(eventType => {
                    this.video.addEventListener(eventType, (e) => {
                        this.logEvent(eventType.toUpperCase(), this.getEventData(eventType));
                    });
                });
            }
            
            getEventData(eventType) {
                switch(eventType) {
                    case 'timeupdate':
                        return `时间: ${this.video.currentTime.toFixed(2)}s`;
                    case 'durationchange':
                        return `时长: ${this.video.duration.toFixed(2)}s`;
                    case 'volumechange':
                        return `音量: ${(this.video.volume * 100).toFixed(0)}%, 静音: ${this.video.muted}`;
                    case 'ratechange':
                        return `速度: ${this.video.playbackRate}x`;
                    case 'progress':
                        const buffered = this.video.buffered;
                        if (buffered.length > 0) {
                            return `缓冲: ${buffered.end(buffered.length - 1).toFixed(2)}s`;
                        }
                        return '缓冲: 0s';
                    case 'error':
                        const error = this.video.error;
                        return error ? `错误码: ${error.code}, 消息: ${error.message}` : '未知错误';
                    default:
                        return `就绪状态: ${this.video.readyState}, 网络状态: ${this.video.networkState}`;
                }
            }
            
            logEvent(type, data) {
                if (this.logPaused) return;
                
                this.eventCount++;
                const now = new Date();
                const timeStr = now.toLocaleTimeString();
                
                const eventItem = document.createElement('div');
                eventItem.className = 'event-item';
                eventItem.innerHTML = `
                    <span class="event-time">[${timeStr}]</span>
                    <span class="event-type">${type}</span>
                    <span class="event-data">${data}</span>
                `;
                
                this.eventLog.insertBefore(eventItem, this.eventLog.firstChild);
                
                // 限制日志条数
                if (this.eventLog.children.length > 100) {
                    this.eventLog.removeChild(this.eventLog.lastChild);
                }
            }
            
            clearLog() {
                this.eventLog.innerHTML = `
                    <div class="event-item">
                        <span class="event-time">[${new Date().toLocaleTimeString()}]</span>
                        <span class="event-type">CLEAR</span>
                        <span class="event-data">日志已清空</span>
                    </div>
                `;
                this.eventCount = 0;
            }
            
            exportLog() {
                const events = Array.from(this.eventLog.children).map(item => item.textContent).reverse();
                const content = events.join('\n');
                
                const blob = new Blob([content], { type: 'text/plain' });
                const url = URL.createObjectURL(blob);
                
                const a = document.createElement('a');
                a.href = url;
                a.download = `media-events-${Date.now()}.log`;
                a.click();
                
                URL.revokeObjectURL(url);
            }
            
            startStatusUpdate() {
                setInterval(() => {
                    this.updateStatus();
                }, 100);
            }
            
            updateStatus() {
                // 更新状态显示
                document.getElementById('playState').textContent = this.video.paused ? '暂停' : '播放';
                document.getElementById('currentTime').textContent = this.video.currentTime.toFixed(2) + 's';
                document.getElementById('duration').textContent = (this.video.duration || 0).toFixed(2) + 's';
                document.getElementById('volume').textContent = Math.round(this.video.volume * 100) + '%';
                document.getElementById('playbackRate').textContent = this.video.playbackRate + 'x';
                document.getElementById('readyState').textContent = this.video.readyState;
                document.getElementById('networkState').textContent = this.video.networkState;
                
                // 更新缓冲显示
                const buffered = this.video.buffered;
                let bufferedTime = 0;
                if (buffered.length > 0) {
                    bufferedTime = buffered.end(buffered.length - 1);
                }
                document.getElementById('buffered').textContent = bufferedTime.toFixed(2) + 's';
                
                // 更新进度条
                if (this.video.duration) {
                    const progress = (this.video.currentTime / this.video.duration) * 100;
                    document.getElementById('progressBar').style.width = progress + '%';
                    
                    const current = this.formatTime(this.video.currentTime);
                    const total = this.formatTime(this.video.duration);
                    document.getElementById('progressText').textContent = `${current} / ${total}`;
                }
            }
            
            formatTime(seconds) {
                const mins = Math.floor(seconds / 60);
                const secs = Math.floor(seconds % 60);
                return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
            }
        }

        // API测试函数
        function testCanPlayType() {
            const video = document.getElementById('mainVideo');
            const formats = [
                'video/mp4',
                'video/webm',
                'video/ogg',
                'video/mp4; codecs="avc1.42E01E"',
                'video/webm; codecs="vp8"',
                'video/webm; codecs="vp9"',
                'video/ogg; codecs="theora"'
            ];
            
            let result = '<strong>格式支持测试结果:</strong><br>';
            formats.forEach(format => {
                const support = video.canPlayType(format);
                const status = support === 'probably' ? '✅ 完全支持' :
                             support === 'maybe' ? '⚠️ 可能支持' : '❌ 不支持';
                result += `${format}: ${status}<br>`;
            });
            
            document.getElementById('canPlayResult').innerHTML = result;
        }
        
        function testLoad() {
            const video = document.getElementById('mainVideo');
            video.load();
        }
        
        function testFastSeek() {
            const video = document.getElementById('mainVideo');
            const time = parseFloat(document.getElementById('fastSeekInput').value);
            
            if (!isNaN(time)) {
                if (video.fastSeek) {
                    video.fastSeek(time);
                } else {
                    video.currentTime = time;
                }
            }
        }
        
        function switchSource() {
            const video = document.getElementById('mainVideo');
            const select = document.getElementById('sourceSelect');
            
            // 这里可以切换到不同的测试视频源
            // 为了演示,我们重新生成视频
            mediaController.generateTestVideo();
        }

        // 初始化
        let mediaController;
        document.addEventListener('DOMContentLoaded', function() {
            const video = document.getElementById('mainVideo');
            mediaController = new MediaController(video);
            
            console.log('媒体 API 控制演示已启动');
            console.log('使用控制面板测试各种媒体 API 功能');
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 掌握核心的播放控制方法
  • 了解重要的媒体事件生命周期
  • 知道如何检测播放状态和错误

加分项:

  • 理解readyState不同状态的含义
  • 了解如何实现播放进度条
  • 知道如何处理网络错误和格式错误

常见失误:

  • 不等待canplay事件就调用play()
  • 忽略Promise返回值的错误处理
  • 没有适当的错误处理机制

延伸阅读:

现代Web媒体技术有哪些?

答案

核心概念:

现代Web平台提供多种先进的媒体技术:

  • Media Source Extensions (MSE) - 动态构建媒体流
  • WebRTC - 实时音视频通信
  • Web Audio API - 高级音频处理
  • Canvas + Video - 视频帧操作和特效
  • Picture-in-Picture API - 画中画模式
  • Media Session API - 媒体会话控制
  • Screen Capture API - 屏幕录制和共享
  • Media Recorder API - 录制音视频流

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>现代Web媒体技术演示</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
            color: #333;
            background: #f8f9fa;
        }

        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 20px rgba(0,0,0,0.1);
        }

        h1 {
            color: #2c3e50;
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 15px;
            border-bottom: 3px solid #9b59b6;
        }

        .demo-section {
            margin: 40px 0;
            padding: 25px;
            background: #f8f9fa;
            border-radius: 8px;
            border-left: 4px solid #9b59b6;
        }

        .demo-section h2 {
            color: #9b59b6;
            margin-bottom: 20px;
        }

        .tech-demo {
            background: white;
            padding: 20px;
            margin: 15px 0;
            border-radius: 6px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            position: relative;
        }

        .element-tag {
            position: absolute;
            top: -10px;
            right: 10px;
            background: #9b59b6;
            color: white;
            padding: 2px 8px;
            font-size: 11px;
            border-radius: 3px;
            font-weight: bold;
        }

        /* 画中画演示 */
        .pip-demo {
            text-align: center;
            margin: 20px 0;
        }

        .pip-demo video {
            width: 100%;
            max-width: 400px;
            border-radius: 8px;
        }

        /* Web Audio 可视化 */
        .audio-viz {
            background: #2c3e50;
            border-radius: 8px;
            padding: 20px;
            color: white;
            text-align: center;
        }

        .visualizer-canvas {
            border: 1px solid #34495e;
            border-radius: 4px;
            background: black;
            margin: 15px 0;
        }

        /* MediaRecorder 演示 */
        .recorder-demo {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 8px;
            padding: 20px;
            color: white;
        }

        .recorder-controls {
            display: flex;
            gap: 10px;
            justify-content: center;
            margin: 15px 0;
            flex-wrap: wrap;
        }

        .recorder-status {
            text-align: center;
            margin: 15px 0;
            font-weight: bold;
        }

        /* Screen Capture 演示 */
        .screen-demo {
            background: #34495e;
            border-radius: 8px;
            padding: 20px;
            color: white;
        }

        .screen-preview {
            text-align: center;
            margin: 15px 0;
        }

        .screen-preview video {
            max-width: 100%;
            border-radius: 4px;
            background: black;
        }

        /* 按钮样式 */
        .tech-btn {
            background: #3498db;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
            margin: 5px;
        }

        .tech-btn:hover {
            background: #2980b9;
            transform: translateY(-1px);
        }

        .tech-btn:disabled {
            background: #95a5a6;
            cursor: not-allowed;
            transform: none;
        }

        .tech-btn.danger {
            background: #e74c3c;
        }

        .tech-btn.danger:hover {
            background: #c0392b;
        }

        .tech-btn.success {
            background: #27ae60;
        }

        .tech-btn.success:hover {
            background: #229954;
        }

        /* 状态指示器 */
        .status-indicator {
            display: inline-block;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            margin-right: 8px;
        }

        .status-inactive { background: #95a5a6; }
        .status-active { background: #27ae60; }
        .status-recording { background: #e74c3c; animation: pulse 1s infinite; }
        .status-error { background: #e74c3c; }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        /* 信息框 */
        .info-box {
            background: #d1ecf1;
            border: 1px solid #bee5eb;
            border-radius: 6px;
            padding: 15px;
            margin: 15px 0;
        }

        .info-box.warning {
            background: #fff3cd;
            border-color: #ffeaa7;
        }

        .info-box.error {
            background: #f8d7da;
            border-color: #f5c6cb;
        }

        .info-box strong {
            color: #0c5460;
        }

        /* Canvas effects */
        .canvas-demo {
            text-align: center;
            margin: 20px 0;
        }

        .canvas-demo canvas {
            border: 1px solid #ddd;
            border-radius: 4px;
            max-width: 100%;
        }

        /* 媒体会话 */
        .media-session {
            background: #f39c12;
            border-radius: 8px;
            padding: 20px;
            color: white;
            text-align: center;
        }

        .media-info {
            display: grid;
            grid-template-columns: auto 1fr;
            gap: 15px;
            align-items: center;
            margin: 15px 0;
            text-align: left;
        }

        .album-art {
            width: 80px;
            height: 80px;
            background: #e67e22;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
        }

        /* 响应式 */
        @media (max-width: 768px) {
            .recorder-controls {
                flex-direction: column;
                align-items: center;
            }
            
            .media-info {
                grid-template-columns: 1fr;
                text-align: center;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>现代Web媒体技术演示</h1>

        <!-- Picture-in-Picture API -->
        <div class="demo-section">
            <h2>1. Picture-in-Picture API (画中画)</h2>

            <div class="tech-demo">
                <span class="element-tag">PIP</span>
                <h3>画中画模式演示</h3>
                <div class="pip-demo">
                    <video id="pipVideo" controls muted autoplay>
                        <source src="#" type="video/mp4">
                        您的浏览器不支持视频标签。
                    </video>
                    <div>
                        <button class="tech-btn" id="pipBtn">
                            <span class="status-indicator status-inactive" id="pipStatus"></span>
                            启用画中画
                        </button>
                        <button class="tech-btn" id="exitPipBtn" disabled>退出画中画</button>
                    </div>
                </div>
                
                <div class="info-box">
                    <strong>💡 画中画功能:</strong>
                    允许用户在小窗口中观看视频,同时继续浏览其他页面内容。适用于视频会议、教学视频等场景。
                </div>
            </div>
        </div>

        <!-- Web Audio API -->
        <div class="demo-section">
            <h2>2. Web Audio API (音频处理)</h2>

            <div class="tech-demo">
                <span class="element-tag">AUDIO</span>
                <h3>音频可视化和处理</h3>
                <div class="audio-viz">
                    <canvas id="audioCanvas" class="visualizer-canvas" width="600" height="200"></canvas>
                    <div>
                        <button class="tech-btn" id="audioStartBtn">开始音频</button>
                        <button class="tech-btn" id="audioStopBtn" disabled>停止音频</button>
                        <button class="tech-btn" id="micBtn">使用麦克风</button>
                    </div>
                    <div>
                        <label for="gainSlider">增益: </label>
                        <input type="range" id="gainSlider" min="0" max="2" step="0.1" value="1">
                        <span id="gainValue">1.0</span>
                    </div>
                </div>
                
                <div class="info-box">
                    <strong>🎵 Web Audio API特性:</strong>
                    提供强大的音频处理能力,包括音频合成、效果处理、3D空间音频、实时分析等。
                </div>
            </div>
        </div>

        <!-- MediaRecorder API -->
        <div class="demo-section">
            <h2>3. MediaRecorder API (录制)</h2>

            <div class="tech-demo">
                <span class="element-tag">RECORD</span>
                <h3>媒体录制功能</h3>
                <div class="recorder-demo">
                    <div class="recorder-status">
                        <span class="status-indicator status-inactive" id="recordStatus"></span>
                        <span id="recordStatusText">就绪</span>
                    </div>
                    
                    <div class="recorder-controls">
                        <button class="tech-btn" id="startRecordBtn">开始录制</button>
                        <button class="tech-btn danger" id="stopRecordBtn" disabled>停止录制</button>
                        <button class="tech-btn" id="playRecordBtn" disabled>播放录制</button>
                        <button class="tech-btn" id="downloadRecordBtn" disabled>下载录制</button>
                    </div>
                    
                    <video id="recordedVideo" controls style="width: 100%; max-width: 400px; margin-top: 15px; display: none;"></video>
                </div>
                
                <div class="info-box">
                    <strong>📹 MediaRecorder功能:</strong>
                    录制音频、视频或屏幕内容,支持多种格式输出,适用于会议录制、教学视频制作等。
                </div>
            </div>
        </div>

        <!-- Screen Capture API -->
        <div class="demo-section">
            <h2>4. Screen Capture API (屏幕录制)</h2>

            <div class="tech-demo">
                <span class="element-tag">SCREEN</span>
                <h3>屏幕捕获和共享</h3>
                <div class="screen-demo">
                    <div style="text-align: center; margin: 15px 0;">
                        <button class="tech-btn" id="startScreenBtn">开始屏幕捕获</button>
                        <button class="tech-btn danger" id="stopScreenBtn" disabled>停止捕获</button>
                    </div>
                    
                    <div class="screen-preview">
                        <video id="screenVideo" autoplay muted style="max-width: 500px; display: none;"></video>
                    </div>
                    
                    <div id="screenStatus" style="text-align: center; margin: 15px 0;">
                        点击"开始屏幕捕获"选择要共享的屏幕或窗口
                    </div>
                </div>
                
                <div class="info-box">
                    <strong>🖥️ 屏幕捕获应用:</strong>
                    用于视频会议、远程协作、教学演示、客服支持等场景。现代浏览器会询问用户权限。
                </div>
            </div>
        </div>

        <!-- Canvas + Video -->
        <div class="demo-section">
            <h2>5. Canvas + Video (视频特效)</h2>

            <div class="tech-demo">
                <span class="element-tag">CANVAS</span>
                <h3>实时视频处理</h3>
                <div class="canvas-demo">
                    <video id="sourceVideo" width="300" height="200" muted autoplay style="display: none;"></video>
                    <canvas id="effectCanvas" width="300" height="200"></canvas>
                    
                    <div style="margin: 15px 0;">
                        <button class="tech-btn" id="startEffectBtn">开始效果</button>
                        <button class="tech-btn" id="stopEffectBtn" disabled>停止效果</button>
                        <select id="effectSelect" style="margin: 0 10px; padding: 5px;">
                            <option value="normal">正常</option>
                            <option value="grayscale">灰度</option>
                            <option value="sepia">褐色</option>
                            <option value="invert">反色</option>
                            <option value="blur">模糊</option>
                            <option value="edge">边缘检测</option>
                        </select>
                    </div>
                </div>
                
                <div class="info-box">
                    <strong>🎨 视频特效处理:</strong>
                    通过Canvas实时处理视频帧,可以实现滤镜、美颜、背景替换等效果。
                </div>
            </div>
        </div>

        <!-- Media Session API -->
        <div class="demo-section">
            <h2>6. Media Session API (媒体会话)</h2>

            <div class="tech-demo">
                <span class="element-tag">SESSION</span>
                <h3>媒体会话控制</h3>
                <div class="media-session">
                    <div class="media-info">
                        <div class="album-art">🎵</div>
                        <div>
                            <h4 style="margin: 0;">现在播放</h4>
                            <p style="margin: 5px 0;">示例音乐 - Web Audio Demo</p>
                            <p style="margin: 5px 0; opacity: 0.8;">专辑: HTML5 媒体技术</p>
                        </div>
                    </div>
                    
                    <div>
                        <button class="tech-btn" id="sessionPlayBtn">播放</button>
                        <button class="tech-btn" id="sessionPauseBtn">暂停</button>
                        <button class="tech-btn" id="sessionPrevBtn">上一首</button>
                        <button class="tech-btn" id="sessionNextBtn">下一首</button>
                    </div>
                </div>
                
                <div class="info-box">
                    <strong>🎮 媒体会话特性:</strong>
                    整合系统媒体控制(锁屏界面、通知中心、硬件按键),提供统一的媒体控制体验。
                </div>
            </div>
        </div>

        <!-- API 支持检测 -->
        <div class="demo-section">
            <h2>7. 现代媒体API支持检测</h2>

            <div class="tech-demo">
                <span class="element-tag">SUPPORT</span>
                <h3>浏览器兼容性检查</h3>
                <div id="apiSupport" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin: 15px 0;">
                    <!-- API支持状态将在这里显示 -->
                </div>
                
                <div class="info-box warning">
                    <strong>⚠️ 兼容性注意:</strong>
                    不同的现代媒体API在各浏览器中的支持程度不同,使用前应检测支持性并提供降级方案。
                </div>
            </div>
        </div>
    </div>

    <script>
        class ModernMediaTech {
            constructor() {
                this.audioContext = null;
                this.mediaRecorder = null;
                this.recordedChunks = [];
                this.screenStream = null;
                this.effectAnimation = null;
                
                this.init();
            }
            
            init() {
                this.checkAPISupport();
                this.setupPictureInPicture();
                this.setupWebAudio();
                this.setupMediaRecorder();
                this.setupScreenCapture();
                this.setupCanvasEffects();
                this.setupMediaSession();
            }
            
            checkAPISupport() {
                const apis = {
                    'Picture-in-Picture': 'pictureInPictureEnabled' in document,
                    'Web Audio API': 'AudioContext' in window || 'webkitAudioContext' in window,
                    'MediaRecorder': 'MediaRecorder' in window,
                    'Screen Capture': 'getDisplayMedia' in navigator.mediaDevices,
                    'Media Session': 'mediaSession' in navigator,
                    'WebRTC': 'RTCPeerConnection' in window,
                    'MSE': 'MediaSource' in window,
                    'Web Speech': 'speechSynthesis' in window
                };
                
                const container = document.getElementById('apiSupport');
                
                Object.entries(apis).forEach(([name, supported]) => {
                    const item = document.createElement('div');
                    item.style.cssText = 'padding: 10px; background: white; border-radius: 4px; text-align: center;';
                    
                    const status = supported ? 'success' : 'error';
                    const icon = supported ? '✅' : '❌';
                    const text = supported ? '支持' : '不支持';
                    
                    item.innerHTML = `
                        <div style="font-weight: bold; margin-bottom: 5px;">${name}</div>
                        <div style="color: ${supported ? '#27ae60' : '#e74c3c'}">${icon} ${text}</div>
                    `;
                    
                    container.appendChild(item);
                });
            }
            
            setupPictureInPicture() {
                const video = document.getElementById('pipVideo');
                const pipBtn = document.getElementById('pipBtn');
                const exitPipBtn = document.getElementById('exitPipBtn');
                const pipStatus = document.getElementById('pipStatus');
                
                // 生成测试视频
                this.generatePiPVideo(video);
                
                pipBtn.addEventListener('click', async () => {
                    try {
                        if (document.pictureInPictureEnabled && !video.disablePictureInPicture) {
                            await video.requestPictureInPicture();
                            pipStatus.className = 'status-indicator status-active';
                            pipBtn.textContent = '画中画已启用';
                            pipBtn.disabled = true;
                            exitPipBtn.disabled = false;
                        }
                    } catch (error) {
                        console.error('画中画启用失败:', error);
                    }
                });
                
                exitPipBtn.addEventListener('click', async () => {
                    try {
                        await document.exitPictureInPicture();
                    } catch (error) {
                        console.error('退出画中画失败:', error);
                    }
                });
                
                video.addEventListener('enterpictureinpicture', () => {
                    console.log('进入画中画模式');
                });
                
                video.addEventListener('leavepictureinpicture', () => {
                    console.log('退出画中画模式');
                    pipStatus.className = 'status-indicator status-inactive';
                    pipBtn.textContent = '启用画中画';
                    pipBtn.disabled = false;
                    exitPipBtn.disabled = true;
                });
            }
            
            generatePiPVideo(video) {
                const canvas = document.createElement('canvas');
                canvas.width = 640;
                canvas.height = 360;
                const ctx = canvas.getContext('2d');
                
                let frame = 0;
                const animate = () => {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    
                    // 绘制动态背景
                    const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
                    gradient.addColorStop(0, `hsl(${frame % 360}, 70%, 50%)`);
                    gradient.addColorStop(1, `hsl(${(frame + 180) % 360}, 70%, 30%)`);
                    
                    ctx.fillStyle = gradient;
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    
                    // 绘制时钟
                    const centerX = canvas.width / 2;
                    const centerY = canvas.height / 2;
                    const time = Date.now() / 1000;
                    
                    ctx.strokeStyle = 'white';
                    ctx.lineWidth = 3;
                    ctx.beginPath();
                    ctx.arc(centerX, centerY, 80, 0, Math.PI * 2);
                    ctx.stroke();
                    
                    // 时针
                    const hourAngle = (time / 3600) * Math.PI * 2 - Math.PI / 2;
                    ctx.beginPath();
                    ctx.moveTo(centerX, centerY);
                    ctx.lineTo(centerX + Math.cos(hourAngle) * 40, centerY + Math.sin(hourAngle) * 40);
                    ctx.stroke();
                    
                    // 分针
                    const minuteAngle = (time / 60) * Math.PI * 2 - Math.PI / 2;
                    ctx.beginPath();
                    ctx.moveTo(centerX, centerY);
                    ctx.lineTo(centerX + Math.cos(minuteAngle) * 60, centerY + Math.sin(minuteAngle) * 60);
                    ctx.stroke();
                    
                    // 文字
                    ctx.fillStyle = 'white';
                    ctx.font = 'bold 24px Arial';
                    ctx.textAlign = 'center';
                    ctx.fillText('Picture-in-Picture', centerX, centerY + 120);
                    ctx.font = '16px Arial';
                    ctx.fillText(new Date().toLocaleTimeString(), centerX, centerY + 145);
                    
                    frame++;
                    requestAnimationFrame(animate);
                };
                
                animate();
                
                if (canvas.captureStream) {
                    video.srcObject = canvas.captureStream(30);
                }
            }
            
            setupWebAudio() {
                const startBtn = document.getElementById('audioStartBtn');
                const stopBtn = document.getElementById('audioStopBtn');
                const micBtn = document.getElementById('micBtn');
                const canvas = document.getElementById('audioCanvas');
                const ctx = canvas.getContext('2d');
                const gainSlider = document.getElementById('gainSlider');
                const gainValue = document.getElementById('gainValue');
                
                let analyser, dataArray, source, gainNode;
                
                startBtn.addEventListener('click', async () => {
                    try {
                        this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                        
                        // 创建振荡器
                        const oscillator = this.audioContext.createOscillator();
                        gainNode = this.audioContext.createGain();
                        analyser = this.audioContext.createAnalyser();
                        
                        oscillator.connect(gainNode);
                        gainNode.connect(analyser);
                        analyser.connect(this.audioContext.destination);
                        
                        oscillator.frequency.setValueAtTime(440, this.audioContext.currentTime);
                        oscillator.type = 'sine';
                        oscillator.start();
                        
                        source = oscillator;
                        this.setupAnalyser(analyser, ctx);
                        
                        startBtn.disabled = true;
                        stopBtn.disabled = false;
                        
                    } catch (error) {
                        console.error('Web Audio启动失败:', error);
                    }
                });
                
                micBtn.addEventListener('click', async () => {
                    try {
                        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                        this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                        
                        source = this.audioContext.createMediaStreamSource(stream);
                        gainNode = this.audioContext.createGain();
                        analyser = this.audioContext.createAnalyser();
                        
                        source.connect(gainNode);
                        gainNode.connect(analyser);
                        
                        this.setupAnalyser(analyser, ctx);
                        
                        startBtn.disabled = true;
                        stopBtn.disabled = false;
                        
                    } catch (error) {
                        console.error('麦克风访问失败:', error);
                    }
                });
                
                stopBtn.addEventListener('click', () => {
                    if (this.audioContext) {
                        this.audioContext.close();
                        this.audioContext = null;
                    }
                    
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    
                    startBtn.disabled = false;
                    stopBtn.disabled = true;
                });
                
                gainSlider.addEventListener('input', (e) => {
                    if (gainNode) {
                        gainNode.gain.value = e.target.value;
                    }
                    gainValue.textContent = e.target.value;
                });
            }
            
            setupAnalyser(analyser, ctx) {
                analyser.fftSize = 256;
                const bufferLength = analyser.frequencyBinCount;
                const dataArray = new Uint8Array(bufferLength);
                
                const draw = () => {
                    if (!this.audioContext) return;
                    
                    requestAnimationFrame(draw);
                    
                    analyser.getByteFrequencyData(dataArray);
                    
                    ctx.fillStyle = 'rgb(0, 0, 0)';
                    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                    
                    const barWidth = (ctx.canvas.width / bufferLength) * 2.5;
                    let barHeight;
                    let x = 0;
                    
                    for (let i = 0; i < bufferLength; i++) {
                        barHeight = dataArray[i] * 0.8;
                        
                        const r = barHeight + 25 * (i / bufferLength);
                        const g = 250 * (i / bufferLength);
                        const b = 50;
                        
                        ctx.fillStyle = `rgb(${r},${g},${b})`;
                        ctx.fillRect(x, ctx.canvas.height - barHeight, barWidth, barHeight);
                        
                        x += barWidth + 1;
                    }
                };
                
                draw();
            }
            
            setupMediaRecorder() {
                const startBtn = document.getElementById('startRecordBtn');
                const stopBtn = document.getElementById('stopRecordBtn');
                const playBtn = document.getElementById('playRecordBtn');
                const downloadBtn = document.getElementById('downloadRecordBtn');
                const video = document.getElementById('recordedVideo');
                const status = document.getElementById('recordStatus');
                const statusText = document.getElementById('recordStatusText');
                
                startBtn.addEventListener('click', async () => {
                    try {
                        const stream = await navigator.mediaDevices.getUserMedia({ 
                            video: true, 
                            audio: true 
                        });
                        
                        this.mediaRecorder = new MediaRecorder(stream);
                        this.recordedChunks = [];
                        
                        this.mediaRecorder.ondataavailable = (event) => {
                            if (event.data.size > 0) {
                                this.recordedChunks.push(event.data);
                            }
                        };
                        
                        this.mediaRecorder.onstop = () => {
                            const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
                            video.src = URL.createObjectURL(blob);
                            video.style.display = 'block';
                            
                            playBtn.disabled = false;
                            downloadBtn.disabled = false;
                            
                            status.className = 'status-indicator status-inactive';
                            statusText.textContent = '录制完成';
                            
                            // 停止所有轨道
                            stream.getTracks().forEach(track => track.stop());
                        };
                        
                        this.mediaRecorder.start();
                        
                        status.className = 'status-indicator status-recording';
                        statusText.textContent = '正在录制...';
                        startBtn.disabled = true;
                        stopBtn.disabled = false;
                        
                    } catch (error) {
                        console.error('录制启动失败:', error);
                        status.className = 'status-indicator status-error';
                        statusText.textContent = '录制失败';
                    }
                });
                
                stopBtn.addEventListener('click', () => {
                    if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
                        this.mediaRecorder.stop();
                        startBtn.disabled = false;
                        stopBtn.disabled = true;
                    }
                });
                
                playBtn.addEventListener('click', () => {
                    video.play();
                });
                
                downloadBtn.addEventListener('click', () => {
                    const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `recording-${Date.now()}.webm`;
                    a.click();
                    URL.revokeObjectURL(url);
                });
            }
            
            setupScreenCapture() {
                const startBtn = document.getElementById('startScreenBtn');
                const stopBtn = document.getElementById('stopScreenBtn');
                const video = document.getElementById('screenVideo');
                const status = document.getElementById('screenStatus');
                
                startBtn.addEventListener('click', async () => {
                    try {
                        this.screenStream = await navigator.mediaDevices.getDisplayMedia({
                            video: true,
                            audio: true
                        });
                        
                        video.srcObject = this.screenStream;
                        video.style.display = 'block';
                        
                        startBtn.disabled = true;
                        stopBtn.disabled = false;
                        status.textContent = '正在共享屏幕...';
                        
                        // 监听用户停止共享
                        this.screenStream.getVideoTracks()[0].addEventListener('ended', () => {
                            this.stopScreenCapture();
                        });
                        
                    } catch (error) {
                        console.error('屏幕捕获失败:', error);
                        status.textContent = '屏幕捕获失败,请检查浏览器权限';
                    }
                });
                
                stopBtn.addEventListener('click', () => {
                    this.stopScreenCapture();
                });
            }
            
            stopScreenCapture() {
                if (this.screenStream) {
                    this.screenStream.getTracks().forEach(track => track.stop());
                    this.screenStream = null;
                }
                
                const video = document.getElementById('screenVideo');
                const startBtn = document.getElementById('startScreenBtn');
                const stopBtn = document.getElementById('stopScreenBtn');
                const status = document.getElementById('screenStatus');
                
                video.style.display = 'none';
                video.srcObject = null;
                
                startBtn.disabled = false;
                stopBtn.disabled = true;
                status.textContent = '屏幕共享已停止';
            }
            
            setupCanvasEffects() {
                const video = document.getElementById('sourceVideo');
                const canvas = document.getElementById('effectCanvas');
                const ctx = canvas.getContext('2d');
                const startBtn = document.getElementById('startEffectBtn');
                const stopBtn = document.getElementById('stopEffectBtn');
                const effectSelect = document.getElementById('effectSelect');
                
                // 生成测试视频源
                this.generateEffectVideo(video);
                
                startBtn.addEventListener('click', () => {
                    this.startVideoEffects(video, ctx, effectSelect);
                    startBtn.disabled = true;
                    stopBtn.disabled = false;
                });
                
                stopBtn.addEventListener('click', () => {
                    if (this.effectAnimation) {
                        cancelAnimationFrame(this.effectAnimation);
                        this.effectAnimation = null;
                    }
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    startBtn.disabled = false;
                    stopBtn.disabled = true;
                });
            }
            
            generateEffectVideo(video) {
                const canvas = document.createElement('canvas');
                canvas.width = 300;
                canvas.height = 200;
                const ctx = canvas.getContext('2d');
                
                let frame = 0;
                const animate = () => {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    
                    // 绘制彩色方块动画
                    const time = frame * 0.05;
                    for (let i = 0; i < 10; i++) {
                        for (let j = 0; j < 10; j++) {
                            const x = i * 30;
                            const y = j * 20;
                            const hue = (time + i * 10 + j * 5) % 360;
                            
                            ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
                            ctx.fillRect(x, y, 25, 15);
                        }
                    }
                    
                    // 添加文字
                    ctx.fillStyle = 'white';
                    ctx.font = 'bold 16px Arial';
                    ctx.textAlign = 'center';
                    ctx.fillText('Video Effects Demo', canvas.width / 2, canvas.height / 2);
                    
                    frame++;
                    requestAnimationFrame(animate);
                };
                
                animate();
                
                if (canvas.captureStream) {
                    video.srcObject = canvas.captureStream(30);
                }
            }
            
            startVideoEffects(video, ctx, effectSelect) {
                const processFrame = () => {
                    if (!this.effectAnimation) return;
                    
                    ctx.drawImage(video, 0, 0, ctx.canvas.width, ctx.canvas.height);
                    
                    const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
                    const data = imageData.data;
                    
                    const effect = effectSelect.value;
                    
                    switch (effect) {
                        case 'grayscale':
                            for (let i = 0; i < data.length; i += 4) {
                                const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
                                data[i] = data[i + 1] = data[i + 2] = gray;
                            }
                            break;
                        case 'sepia':
                            for (let i = 0; i < data.length; i += 4) {
                                const r = data[i], g = data[i + 1], b = data[i + 2];
                                data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
                                data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
                                data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
                            }
                            break;
                        case 'invert':
                            for (let i = 0; i < data.length; i += 4) {
                                data[i] = 255 - data[i];
                                data[i + 1] = 255 - data[i + 1];
                                data[i + 2] = 255 - data[i + 2];
                            }
                            break;
                    }
                    
                    if (effect !== 'normal') {
                        ctx.putImageData(imageData, 0, 0);
                    }
                    
                    this.effectAnimation = requestAnimationFrame(processFrame);
                };
                
                this.effectAnimation = requestAnimationFrame(processFrame);
            }
            
            setupMediaSession() {
                if ('mediaSession' in navigator) {
                    navigator.mediaSession.metadata = new MediaMetadata({
                        title: '示例音乐',
                        artist: 'Web Audio Demo',
                        album: 'HTML5 媒体技术',
                        artwork: [
                            { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9Ijk2IiBoZWlnaHQ9Ijk2IiBmaWxsPSIjZjM5YzEyIi8+Cjx0ZXh0IHg9IjQ4IiB5PSI1NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjQ4IiBmaWxsPSJ3aGl0ZSI+8J+OtTwvdGV4dD4KPC9zdmc+', sizes: '96x96', type: 'image/svg+xml' }
                        ]
                    });
                    
                    const actionHandlers = [
                        ['play', () => console.log('媒体会话: 播放')],
                        ['pause', () => console.log('媒体会话: 暂停')],
                        ['previoustrack', () => console.log('媒体会话: 上一首')],
                        ['nexttrack', () => console.log('媒体会话: 下一首')],
                        ['seekbackward', () => console.log('媒体会话: 后退')],
                        ['seekforward', () => console.log('媒体会话: 前进')]
                    ];
                    
                    actionHandlers.forEach(([action, handler]) => {
                        try {
                            navigator.mediaSession.setActionHandler(action, handler);
                        } catch (error) {
                            console.log(`${action} 动作不支持`);
                        }
                    });
                }
                
                // 按钮事件
                document.getElementById('sessionPlayBtn').addEventListener('click', () => {
                    console.log('播放按钮点击');
                });
                
                document.getElementById('sessionPauseBtn').addEventListener('click', () => {
                    console.log('暂停按钮点击');
                });
                
                document.getElementById('sessionPrevBtn').addEventListener('click', () => {
                    console.log('上一首按钮点击');
                });
                
                document.getElementById('sessionNextBtn').addEventListener('click', () => {
                    console.log('下一首按钮点击');
                });
            }
        }

        // 初始化
        document.addEventListener('DOMContentLoaded', function() {
            const modernMedia = new ModernMediaTech();
            console.log('现代Web媒体技术演示已启动');
            console.log('请检查控制台查看各种API的使用情况');
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 了解MSE在流媒体中的作用
  • 知道WebRTC的基本应用场景
  • 理解Web Audio API的音频处理能力

加分项:

  • 提及HLS和DASH等流媒体协议
  • 了解WebCodecs等新兴标准
  • 知道如何优化媒体性能和体验

常见失误:

  • 混淆不同API的适用场景
  • 不了解浏览器兼容性限制
  • 忽略媒体相关的安全和隐私问题

延伸阅读:

如何判断用户设备

答案
方法主要原理优点局限典型代码
User-Agent分析解析navigator.userAgent字符串实现简单,兼容性好易被伪造,部分设备难区分/mobile/i.test(ua)
视口尺寸判断检测window.innerWidth无需依赖UA,适合响应式横屏/分屏等场景不准width<768
媒体查询window.matchMedia结合CSS断点与响应式设计一致仅能判断尺寸,非设备类型matchMedia('(max-width:767px)')

核心概念与原理

  • User-Agent(UA)字符串包含浏览器、系统、设备等信息,常用于初步判断设备类型,但易被修改或伪造。
  • 视口尺寸法通过检测窗口宽度推断设备类别,适合响应式布局,但对特殊场景(如横屏、缩放)不够精确。
  • 媒体查询法利用 CSS 断点,结合 JS 的 window.matchMedia,可动态判断当前视口是否符合某类设备标准。

代码示例

// 1. User-Agent 判断
function detectDevice () {
const ua = navigator.userAgent
if (/mobile/i.test(ua)) return 'Mobile'
if (/tablet/i.test(ua)) return 'Tablet'
if (/iPad|iPhone|iPod/.test(ua)) return 'iOS Device'
return 'Desktop'
}

// 2. 视口尺寸判断
function detectDeviceByViewport () {
const w = window.innerWidth
if (w < 768) return 'Mobile'
if (w < 992) return 'Tablet'
return 'Desktop'
}

// 3. 媒体查询判断
function detectDeviceByMediaQuery () {
if (window.matchMedia('(max-width:767px)').matches) return 'Mobile'
if (window.matchMedia('(min-width:768px) and (max-width:991px)').matches) return 'Tablet'
return 'Desktop'
}

常见误区与开发建议

  • UA 检测不可靠,仅适合作为辅助手段,不能作为安全或唯一依据。
  • 视口尺寸和媒体查询更适合响应式布局,建议优先采用“自适应内容”而非“设备定制”。
  • 设备检测应结合多种手段,避免因单一方法导致误判。

延伸阅读

前端如何实现截图?

答案

核心概念:

前端截图主要通过以下技术实现:html2canvas库(DOM转Canvas)、getDisplayMedia API(屏幕截图)、Canvas API(特定元素截图)。核心原理是将DOM元素转换为Canvas或直接获取屏幕内容,然后通过toDataURL()等方法输出图片数据。需要考虑跨域限制、性能优化、兼容性等因素。

实际示例:

<!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>
        body {
            font-family: Arial, sans-serif;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            margin: 0;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
        }

        .demo-section {
            margin-bottom: 30px;
            padding: 20px;
            border: 2px dashed #ccc;
            border-radius: 8px;
            background: #f9f9f9;
        }

        .buttons {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            margin-bottom: 20px;
        }

        button {
            padding: 12px 20px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            transition: all 0.3s ease;
        }

        .btn-primary {
            background: #007bff;
            color: white;
        }

        .btn-primary:hover {
            background: #0056b3;
            transform: translateY(-2px);
        }

        .btn-success {
            background: #28a745;
            color: white;
        }

        .btn-success:hover {
            background: #1e7e34;
            transform: translateY(-2px);
        }

        .btn-warning {
            background: #ffc107;
            color: #212529;
        }

        .btn-warning:hover {
            background: #e0a800;
            transform: translateY(-2px);
        }

        .screenshot-preview {
            max-width: 100%;
            max-height: 300px;
            border: 2px solid #ddd;
            border-radius: 8px;
            margin-top: 15px;
            display: none;
        }

        .status {
            padding: 10px;
            border-radius: 6px;
            margin-top: 10px;
            font-weight: bold;
            display: none;
        }

        .status.success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .status.info {
            background: #cce7ff;
            color: #004085;
            border: 1px solid #b8daff;
        }

        .demo-content {
            background: linear-gradient(45deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
            padding: 20px;
            border-radius: 8px;
            margin: 20px 0;
            text-align: center;
        }

        .demo-content h3 {
            color: #333;
            margin: 0 0 10px 0;
        }

        .demo-content p {
            color: #666;
            margin: 0;
            font-size: 14px;
        }

        .loading {
            display: none;
            text-align: center;
            margin-top: 15px;
        }

        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #3498db;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
            display: inline-block;
            margin-right: 10px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .method-info {
            background: #e3f2fd;
            padding: 15px;
            border-radius: 6px;
            margin-bottom: 15px;
            border-left: 4px solid #2196f3;
        }

        .method-info h4 {
            margin: 0 0 8px 0;
            color: #1976d2;
        }

        .method-info p {
            margin: 0;
            font-size: 14px;
            color: #424242;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🖼️ 前端截图实现 MVP Demo</h1>
        
        <div class="demo-section">
            <h2>📱 方法一:Canvas API 截图</h2>
            <div class="method-info">
                <h4>原理</h4>
                <p>使用Canvas API直接绘制DOM元素,适用于简单的元素截图。局限性:无法截取跨域图片,样式支持有限。</p>
            </div>
            
            <div class="demo-content" id="canvasTarget">
                <h3>Canvas截图目标区域</h3>
                <p>这个区域将被Canvas API截图</p>
                <div style="background: #4caf50; color: white; padding: 10px; border-radius: 4px; margin-top: 10px;">
                    Canvas可以截取这个绿色区域
                </div>
            </div>
            
            <div class="buttons">
                <button class="btn-primary" onclick="screenshotWithCanvas()">Canvas截图</button>
                <button class="btn-warning" onclick="downloadImage('canvas')">下载图片</button>
            </div>
            
            <div class="loading" id="canvasLoading">
                <div class="spinner"></div>
                正在生成截图...
            </div>
            
            <div class="status" id="canvasStatus"></div>
            <img class="screenshot-preview" id="canvasPreview" alt="Canvas截图预览">
        </div>

        <div class="demo-section">
            <h2>📺 方法二:html2canvas 库截图</h2>
            <div class="method-info">
                <h4>原理</h4>
                <p>使用html2canvas库,能够更好地处理CSS样式和复杂DOM结构,是目前最流行的DOM截图解决方案。</p>
            </div>
            
            <div class="demo-content" id="html2canvasTarget">
                <h3>html2canvas截图目标区域</h3>
                <p>这个区域展示了更复杂的样式效果</p>
                <div style="display: flex; gap: 10px; margin-top: 15px;">
                    <div style="flex: 1; background: #ff5722; color: white; padding: 15px; border-radius: 8px; text-align: center;">
                        复杂样式1
                    </div>
                    <div style="flex: 1; background: #9c27b0; color: white; padding: 15px; border-radius: 8px; text-align: center;">
                        复杂样式2
                    </div>
                </div>
            </div>
            
            <div class="buttons">
                <button class="btn-primary" onclick="screenshotWithHtml2Canvas()">html2canvas截图</button>
                <button class="btn-warning" onclick="downloadImage('html2canvas')">下载图片</button>
            </div>
            
            <div class="loading" id="html2canvasLoading">
                <div class="spinner"></div>
                正在生成截图...
            </div>
            
            <div class="status" id="html2canvasStatus"></div>
            <img class="screenshot-preview" id="html2canvasPreview" alt="html2canvas截图预览">
        </div>

        <div class="demo-section">
            <h2>🖥️ 方法三:Screen Capture API 屏幕截图</h2>
            <div class="method-info">
                <h4>原理</h4>
                <p>使用getDisplayMedia API获取屏幕内容,可以截取整个屏幕、窗口或标签页。需要用户授权,安全性更高。</p>
            </div>
            
            <div class="buttons">
                <button class="btn-success" onclick="screenshotWithDisplayMedia()">屏幕截图 (需授权)</button>
                <button class="btn-warning" onclick="downloadImage('screen')">下载图片</button>
            </div>
            
            <div class="loading" id="screenLoading">
                <div class="spinner"></div>
                正在获取屏幕权限...
            </div>
            
            <div class="status" id="screenStatus"></div>
            <img class="screenshot-preview" id="screenPreview" alt="屏幕截图预览">
        </div>
    </div>

    <!-- html2canvas CDN -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>

    <script>
        let screenshots = {};

        // Canvas API 截图
        function screenshotWithCanvas() {
            const target = document.getElementById('canvasTarget');
            const loading = document.getElementById('canvasLoading');
            const status = document.getElementById('canvasStatus');
            const preview = document.getElementById('canvasPreview');
            
            loading.style.display = 'block';
            status.style.display = 'none';
            preview.style.display = 'none';
            
            try {
                // 创建Canvas
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                // 设置Canvas尺寸
                const rect = target.getBoundingClientRect();
                canvas.width = rect.width;
                canvas.height = rect.height;
                
                // 设置背景色
                ctx.fillStyle = getComputedStyle(target).background || '#ffffff';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                
                // 简单的文本绘制示例(实际应用中需要遍历DOM元素)
                ctx.fillStyle = '#333';
                ctx.font = '20px Arial';
                ctx.fillText('Canvas API 截图示例', 20, 40);
                
                ctx.fillStyle = '#666';
                ctx.font = '14px Arial';
                ctx.fillText('这是Canvas直接绘制的示例', 20, 70);
                
                // 绘制绿色区域
                ctx.fillStyle = '#4caf50';
                ctx.fillRect(20, 90, canvas.width - 40, 50);
                
                ctx.fillStyle = 'white';
                ctx.font = '14px Arial';
                ctx.fillText('Canvas可以截取这个绿色区域', 30, 120);
                
                const dataURL = canvas.toDataURL('image/png');
                screenshots.canvas = dataURL;
                
                preview.src = dataURL;
                preview.style.display = 'block';
                
                loading.style.display = 'none';
                showStatus('canvasStatus', 'Canvas截图成功!', 'success');
                
            } catch (error) {
                loading.style.display = 'none';
                showStatus('canvasStatus', `Canvas截图失败: ${error.message}`, 'error');
            }
        }

        // html2canvas 截图
        function screenshotWithHtml2Canvas() {
            const target = document.getElementById('html2canvasTarget');
            const loading = document.getElementById('html2canvasLoading');
            const status = document.getElementById('html2canvasStatus');
            const preview = document.getElementById('html2canvasPreview');
            
            loading.style.display = 'block';
            status.style.display = 'none';
            preview.style.display = 'none';
            
            if (typeof html2canvas === 'undefined') {
                loading.style.display = 'none';
                showStatus('html2canvasStatus', 'html2canvas库未加载,请检查网络连接', 'error');
                return;
            }
            
            html2canvas(target, {
                useCORS: true,
                scale: 1,
                logging: false,
                width: target.offsetWidth,
                height: target.offsetHeight
            }).then(canvas => {
                const dataURL = canvas.toDataURL('image/png');
                screenshots.html2canvas = dataURL;
                
                preview.src = dataURL;
                preview.style.display = 'block';
                
                loading.style.display = 'none';
                showStatus('html2canvasStatus', 'html2canvas截图成功!', 'success');
            }).catch(error => {
                loading.style.display = 'none';
                showStatus('html2canvasStatus', `html2canvas截图失败: ${error.message}`, 'error');
            });
        }

        // Screen Capture API 截图
        async function screenshotWithDisplayMedia() {
            const loading = document.getElementById('screenLoading');
            const status = document.getElementById('screenStatus');
            const preview = document.getElementById('screenPreview');
            
            loading.style.display = 'block';
            status.style.display = 'none';
            preview.style.display = 'none';
            
            try {
                if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
                    throw new Error('浏览器不支持Screen Capture API');
                }
                
                // 获取屏幕媒体流
                const stream = await navigator.mediaDevices.getDisplayMedia({
                    video: { mediaSource: 'screen' }
                });
                
                // 创建video元素来捕获帧
                const video = document.createElement('video');
                video.srcObject = stream;
                video.play();
                
                video.addEventListener('loadedmetadata', () => {
                    // 创建Canvas来截取视频帧
                    const canvas = document.createElement('canvas');
                    const ctx = canvas.getContext('2d');
                    
                    canvas.width = video.videoWidth;
                    canvas.height = video.videoHeight;
                    
                    // 绘制当前帧到Canvas
                    ctx.drawImage(video, 0, 0);
                    
                    const dataURL = canvas.toDataURL('image/png');
                    screenshots.screen = dataURL;
                    
                    preview.src = dataURL;
                    preview.style.display = 'block';
                    
                    // 停止媒体流
                    stream.getTracks().forEach(track => track.stop());
                    
                    loading.style.display = 'none';
                    showStatus('screenStatus', '屏幕截图成功!', 'success');
                });
                
            } catch (error) {
                loading.style.display = 'none';
                if (error.name === 'NotAllowedError') {
                    showStatus('screenStatus', '用户拒绝了屏幕共享权限', 'error');
                } else {
                    showStatus('screenStatus', `屏幕截图失败: ${error.message}`, 'error');
                }
            }
        }

        // 下载图片
        function downloadImage(type) {
            if (!screenshots[type]) {
                alert('请先生成截图!');
                return;
            }
            
            const link = document.createElement('a');
            link.download = `screenshot-${type}-${Date.now()}.png`;
            link.href = screenshots[type];
            link.click();
        }

        // 显示状态信息
        function showStatus(elementId, message, type) {
            const element = document.getElementById(elementId);
            element.textContent = message;
            element.className = `status ${type}`;
            element.style.display = 'block';
            
            // 3秒后自动隐藏成功信息
            if (type === 'success') {
                setTimeout(() => {
                    element.style.display = 'none';
                }, 3000);
            }
        }

        // 页面加载完成后的提示
        window.addEventListener('load', () => {
            console.log('📸 截图功能已就绪!');
            console.log('支持的截图方式:');
            console.log('1. Canvas API - 适用于简单元素');
            console.log('2. html2canvas - 适用于复杂DOM结构');
            console.log('3. Screen Capture API - 适用于屏幕截图(需要HTTPS)');
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 理解不同截图方式的原理和适用场景
  • 掌握html2canvas等主流截图库的使用
  • 了解Canvas API的基本操作和限制

加分项:

  • 提及getDisplayMedia API的屏幕截图能力
  • 了解跨域图片的处理方法(CORS)
  • 知道如何优化大图截图的性能

常见失误:

  • 忽略跨域限制导致的安全错误
  • 不了解不同截图方式的浏览器兼容性
  • 截图质量和性能平衡处理不当

延伸阅读:

web 网页如何禁止别人移除水印

答案

核心概念:

Web水印防护是通过技术手段增加水印移除难度的防护策略。主要包括:明水印(可见水印)和暗水印(隐形水印)两种类型。防护手段涉及MutationObserver监听DOM变化、CSS保护措施、JavaScript动态重建、样式混淆等技术。完全阻止移除是不可能的,但可以显著提高移除成本和技术门槛。

实际示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web水印防护技术</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Arial', sans-serif;
            line-height: 1.6;
            color: #333;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 15px;
            overflow: hidden;
            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
        }

        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }

        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
        }

        .header p {
            font-size: 1.1em;
            opacity: 0.9;
        }

        .content {
            padding: 30px;
        }

        .demo-section {
            margin-bottom: 40px;
            padding: 25px;
            border: 2px solid #e0e0e0;
            border-radius: 10px;
            position: relative;
            background: #fafafa;
        }

        .demo-section h2 {
            color: #444;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid #eee;
        }

        .controls {
            display: flex;
            gap: 15px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }

        button {
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s ease;
            font-size: 14px;
        }

        .btn-primary {
            background: #007bff;
            color: white;
        }

        .btn-primary:hover {
            background: #0056b3;
            transform: translateY(-2px);
        }

        .btn-danger {
            background: #dc3545;
            color: white;
        }

        .btn-danger:hover {
            background: #c82333;
            transform: translateY(-2px);
        }

        .btn-success {
            background: #28a745;
            color: white;
        }

        .btn-success:hover {
            background: #1e7e34;
            transform: translateY(-2px);
        }

        .btn-warning {
            background: #ffc107;
            color: #212529;
        }

        .btn-warning:hover {
            background: #e0a800;
            transform: translateY(-2px);
        }

        .demo-area {
            min-height: 200px;
            background: linear-gradient(45deg, #fff 0%, #f8f9fa 100%);
            border: 2px dashed #ccc;
            border-radius: 8px;
            padding: 30px;
            text-align: center;
            position: relative;
            overflow: hidden;
        }

        .demo-content {
            font-size: 18px;
            color: #666;
        }

        .status {
            margin-top: 15px;
            padding: 10px;
            border-radius: 6px;
            font-weight: bold;
            display: none;
        }

        .status.success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .status.info {
            background: #cce7ff;
            color: #004085;
            border: 1px solid #b8daff;
        }

        .watermark-info {
            background: #e3f2fd;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
            border-left: 4px solid #2196f3;
        }

        .watermark-info h4 {
            color: #1976d2;
            margin-bottom: 8px;
        }

        .protection-log {
            background: #1e1e1e;
            color: #00ff00;
            padding: 15px;
            border-radius: 8px;
            font-family: 'Courier New', monospace;
            height: 150px;
            overflow-y: auto;
            margin-top: 15px;
            font-size: 12px;
            line-height: 1.4;
        }

        /* 水印样式 */
        .watermark-dom {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 1000;
            background: transparent;
        }

        .watermark-item {
            position: absolute;
            color: rgba(0, 0, 0, 0.1);
            font-size: 14px;
            transform: rotate(-20deg);
            user-select: none;
            font-weight: bold;
        }

        .stealth-watermark {
            position: absolute;
            width: 1px;
            height: 1px;
            opacity: 0.01;
            overflow: hidden;
            z-index: -1;
        }

        .attack-simulation {
            background: #ffe6e6;
            border: 2px solid #ff9999;
            padding: 15px;
            border-radius: 8px;
            margin-top: 20px;
        }

        .attack-simulation h4 {
            color: #cc0000;
            margin-bottom: 10px;
        }

        /* 响应式设计 */
        @media (max-width: 768px) {
            .controls {
                flex-direction: column;
            }
            
            button {
                width: 100%;
            }
            
            .header h1 {
                font-size: 2em;
            }
            
            .content {
                padding: 20px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🛡️ Web水印防护技术</h1>
            <p>探索水印生成、检测与防护的技术实现</p>
        </div>

        <div class="content">
            <!-- DOM水印防护 -->
            <div class="demo-section">
                <h2>🏗️ DOM元素水印防护</h2>
                <div class="watermark-info">
                    <h4>技术原理</h4>
                    <p>通过MutationObserver监听DOM变化,当水印元素被删除时自动重新生成。使用多层防护和动态位置变化增加移除难度。</p>
                </div>

                <div class="controls">
                    <button class="btn-primary" onclick="createDOMWatermark()">生成DOM水印</button>
                    <button class="btn-danger" onclick="simulateRemoveWatermark()">模拟删除水印</button>
                    <button class="btn-warning" onclick="clearDOMWatermark()">清除所有水印</button>
                </div>

                <div class="demo-area" id="domWatermarkArea">
                    <div class="demo-content">
                        <h3>DOM水印保护区域</h3>
                        <p>尝试在开发者工具中删除水印元素,观察自动重建效果</p>
                        <p>当前时间: <span id="currentTime"></span></p>
                    </div>
                </div>

                <div class="status" id="domStatus"></div>
                <div class="protection-log" id="domLog"></div>
            </div>

            <!-- Canvas水印防护 -->
            <div class="demo-section">
                <h2>🎨 Canvas背景水印</h2>
                <div class="watermark-info">
                    <h4>技术原理</h4>
                    <p>使用Canvas绘制水印图案,转换为base64图片作为背景,通过CSS repeat实现全屏覆盖。难以直接删除,但可以被CSS覆盖。</p>
                </div>

                <div class="controls">
                    <button class="btn-primary" onclick="createCanvasWatermark()">生成Canvas水印</button>
                    <button class="btn-success" onclick="regenerateCanvasWatermark()">重新生成</button>
                    <button class="btn-warning" onclick="clearCanvasWatermark()">清除水印</button>
                </div>

                <div class="demo-area" id="canvasWatermarkArea">
                    <div class="demo-content">
                        <h3>Canvas背景水印区域</h3>
                        <p>此区域使用Canvas生成的背景水印</p>
                        <p>水印ID: <span id="canvasWatermarkId"></span></p>
                    </div>
                </div>

                <div class="status" id="canvasStatus"></div>
            </div>

            <!-- SVG水印防护 -->
            <div class="demo-section">
                <h2>🔷 SVG矢量水印</h2>
                <div class="watermark-info">
                    <h4>技术原理</h4>
                    <p>使用SVG创建矢量水印,支持复杂图形和动画效果。可以嵌入到background-image中,保持高清晰度。</p>
                </div>

                <div class="controls">
                    <button class="btn-primary" onclick="createSVGWatermark()">生成SVG水印</button>
                    <button class="btn-success" onclick="animateSVGWatermark()">添加动画</button>
                    <button class="btn-warning" onclick="clearSVGWatermark()">清除水印</button>
                </div>

                <div class="demo-area" id="svgWatermarkArea">
                    <div class="demo-content">
                        <h3>SVG矢量水印区域</h3>
                        <p>矢量图形保证缩放时的清晰度</p>
                        <p>支持复杂图形和动画效果</p>
                    </div>
                </div>

                <div class="status" id="svgStatus"></div>
            </div>

            <!-- 隐形水印技术 -->
            <div class="demo-section">
                <h2>👻 隐形水印技术</h2>
                <div class="watermark-info">
                    <h4>技术原理</h4>
                    <p>通过微调像素值、使用零宽字符或隐藏元素等方式嵌入信息。肉眼不可见,但可通过特定算法检测和提取。</p>
                </div>

                <div class="controls">
                    <button class="btn-primary" onclick="createStealthWatermark()">嵌入隐形水印</button>
                    <button class="btn-success" onclick="detectStealthWatermark()">检测隐形水印</button>
                    <button class="btn-warning" onclick="clearStealthWatermark()">清除水印</button>
                </div>

                <div class="demo-area" id="stealthWatermarkArea">
                    <div class="demo-content">
                        <h3>隐形水印区域</h3>
                        <p>此区域包含隐藏的水印信息</p>
                        <p>用户ID: USER_12345 | 时间戳: <span id="stealthTimestamp"></span></p>
                    </div>
                </div>

                <div class="status" id="stealthStatus"></div>
            </div>

            <!-- 攻击模拟 -->
            <div class="demo-section">
                <div class="attack-simulation">
                    <h4>🎯 攻击模拟测试</h4>
                    <p>模拟各种移除水印的攻击手段,测试防护效果:</p>
                    <div class="controls" style="margin-top: 15px;">
                        <button class="btn-danger" onclick="simulateCSSSOverride()">CSS样式覆盖攻击</button>
                        <button class="btn-danger" onclick="simulateDOMRemoval()">DOM删除攻击</button>
                        <button class="btn-danger" onclick="simulateEventBlocking()">事件阻断攻击</button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 全局变量
        let watermarkConfig = {
            text: 'CONFIDENTIAL',
            userId: 'USER_12345',
            timestamp: new Date().getTime()
        };

        let observers = [];
        let watermarkElements = [];
        let protectionEnabled = true;

        // 日志系统
        function addLog(containerId, message, type = 'info') {
            const logContainer = document.getElementById(containerId);
            const timestamp = new Date().toLocaleTimeString();
            const logEntry = `[${timestamp}] ${message}\n`;
            
            if (logContainer) {
                logContainer.textContent += logEntry;
                logContainer.scrollTop = logContainer.scrollHeight;
            }
            
            console.log(`[Watermark Protection] ${message}`);
        }

        // 状态显示
        function showStatus(elementId, message, type) {
            const element = document.getElementById(elementId);
            if (element) {
                element.textContent = message;
                element.className = `status ${type}`;
                element.style.display = 'block';
                
                setTimeout(() => {
                    element.style.display = 'none';
                }, 3000);
            }
        }

        // DOM水印相关功能
        function createDOMWatermark() {
            const container = document.getElementById('domWatermarkArea');
            
            // 清除现有水印
            clearDOMWatermark();
            
            // 创建水印容器
            const watermarkLayer = document.createElement('div');
            watermarkLayer.className = 'watermark-dom';
            watermarkLayer.id = `watermark-${Date.now()}`;
            
            // 生成多个水印元素
            for (let i = 0; i < 20; i++) {
                const watermarkItem = document.createElement('div');
                watermarkItem.className = 'watermark-item';
                watermarkItem.textContent = `${watermarkConfig.text} - ${watermarkConfig.userId}`;
                
                // 随机位置
                watermarkItem.style.left = Math.random() * 80 + '%';
                watermarkItem.style.top = Math.random() * 80 + '%';
                
                watermarkLayer.appendChild(watermarkItem);
            }
            
            container.appendChild(watermarkLayer);
            watermarkElements.push(watermarkLayer);
            
            // 设置MutationObserver监听
            setupDOMProtection(container);
            
            addLog('domLog', 'DOM水印已生成,开始监听保护...');
            showStatus('domStatus', 'DOM水印生成成功,保护机制已启动', 'success');
        }

        function setupDOMProtection(container) {
            const observer = new MutationObserver((mutations) => {
                if (!protectionEnabled) return;
                
                mutations.forEach(mutation => {
                    // 检查删除的节点
                    mutation.removedNodes.forEach(node => {
                        if (node.className && node.className.includes('watermark-dom')) {
                            addLog('domLog', '⚠️ 检测到水印被删除,自动重建中...');
                            
                            // 延迟重建,避免被立即删除
                            setTimeout(() => {
                                if (protectionEnabled) {
                                    createDOMWatermark();
                                    addLog('domLog', '✅ 水印已重建完成');
                                }
                            }, 100);
                        }
                    });
                    
                    // 检查属性变化
                    if (mutation.type === 'attributes' && 
                        mutation.target.className && 
                        mutation.target.className.includes('watermark-dom')) {
                        addLog('domLog', '⚠️ 检测到水印属性被修改');
                        
                        // 恢复原始属性
                        mutation.target.style.display = '';
                        mutation.target.style.visibility = 'visible';
                        mutation.target.style.opacity = '1';
                    }
                });
            });
            
            observer.observe(container, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['style', 'class']
            });
            
            observers.push(observer);
        }

        function simulateRemoveWatermark() {
            const watermarks = document.querySelectorAll('.watermark-dom');
            if (watermarks.length > 0) {
                watermarks[0].remove();
                addLog('domLog', '🎯 模拟删除水印元素...');
                showStatus('domStatus', '模拟攻击:水印元素被删除', 'error');
            } else {
                showStatus('domStatus', '没有找到可删除的水印元素', 'info');
            }
        }

        function clearDOMWatermark() {
            // 停止保护
            protectionEnabled = false;
            
            // 断开所有观察者
            observers.forEach(observer => observer.disconnect());
            observers = [];
            
            // 删除所有水印元素
            const watermarks = document.querySelectorAll('.watermark-dom');
            watermarks.forEach(watermark => watermark.remove());
            watermarkElements = [];
            
            addLog('domLog', '🚫 所有DOM水印已清除,保护机制已停止');
            showStatus('domStatus', 'DOM水印已清除', 'info');
            
            // 重新启用保护
            setTimeout(() => {
                protectionEnabled = true;
            }, 1000);
        }

        // Canvas水印相关功能
        function createCanvasWatermark() {
            const container = document.getElementById('canvasWatermarkArea');
            const watermarkId = 'watermark-' + Date.now();
            
            // 创建Canvas
            const canvas = document.createElement('canvas');
            canvas.width = 200;
            canvas.height = 100;
            const ctx = canvas.getContext('2d');
            
            // 绘制水印
            ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
            ctx.font = '16px Arial';
            ctx.rotate(-20 * Math.PI / 180);
            ctx.fillText(watermarkConfig.text, 10, 50);
            ctx.fillText(watermarkConfig.userId, 10, 70);
            
            // 设置为背景
            const dataURL = canvas.toDataURL();
            container.style.backgroundImage = `url(${dataURL})`;
            container.style.backgroundRepeat = 'repeat';
            container.style.backgroundSize = '200px 100px';
            
            document.getElementById('canvasWatermarkId').textContent = watermarkId;
            showStatus('canvasStatus', 'Canvas水印生成成功', 'success');
        }

        function regenerateCanvasWatermark() {
            watermarkConfig.timestamp = Date.now();
            createCanvasWatermark();
            showStatus('canvasStatus', 'Canvas水印已重新生成', 'success');
        }

        function clearCanvasWatermark() {
            const container = document.getElementById('canvasWatermarkArea');
            container.style.backgroundImage = '';
            document.getElementById('canvasWatermarkId').textContent = '';
            showStatus('canvasStatus', 'Canvas水印已清除', 'info');
        }

        // SVG水印相关功能
        function createSVGWatermark() {
            const container = document.getElementById('svgWatermarkArea');
            
            const svgString = `
                <svg width="200" height="100" xmlns="http://www.w3.org/2000/svg">
                    <defs>
                        <pattern id="watermarkPattern" patternUnits="userSpaceOnUse" width="200" height="100">
                            <text x="10" y="30" fill="rgba(0,0,0,0.1)" font-size="14" transform="rotate(-20)">
                                ${watermarkConfig.text}
                            </text>
                            <text x="10" y="50" fill="rgba(0,0,0,0.1)" font-size="12" transform="rotate(-20)">
                                ${watermarkConfig.userId}
                            </text>
                        </pattern>
                    </defs>
                    <rect width="100%" height="100%" fill="url(#watermarkPattern)"/>
                </svg>
            `;
            
            const svgBlob = new Blob([svgString], { type: 'image/svg+xml' });
            const svgURL = URL.createObjectURL(svgBlob);
            
            container.style.backgroundImage = `url(${svgURL})`;
            container.style.backgroundRepeat = 'repeat';
            container.style.backgroundSize = '200px 100px';
            
            showStatus('svgStatus', 'SVG水印生成成功', 'success');
        }

        function animateSVGWatermark() {
            const container = document.getElementById('svgWatermarkArea');
            
            const svgString = `
                <svg width="200" height="100" xmlns="http://www.w3.org/2000/svg">
                    <defs>
                        <pattern id="animatedPattern" patternUnits="userSpaceOnUse" width="200" height="100">
                            <text x="10" y="30" fill="rgba(0,0,0,0.1)" font-size="14" transform="rotate(-20)">
                                <animateTransform attributeName="transform" type="rotate" 
                                    values="-20;-15;-20" dur="3s" repeatCount="indefinite"/>
                                ${watermarkConfig.text}
                            </text>
                            <text x="10" y="50" fill="rgba(0,0,0,0.1)" font-size="12" transform="rotate(-20)">
                                ${watermarkConfig.userId}
                            </text>
                        </pattern>
                    </defs>
                    <rect width="100%" height="100%" fill="url(#animatedPattern)"/>
                </svg>
            `;
            
            const svgBlob = new Blob([svgString], { type: 'image/svg+xml' });
            const svgURL = URL.createObjectURL(svgBlob);
            
            container.style.backgroundImage = `url(${svgURL})`;
            
            showStatus('svgStatus', 'SVG动画水印生成成功', 'success');
        }

        function clearSVGWatermark() {
            const container = document.getElementById('svgWatermarkArea');
            container.style.backgroundImage = '';
            showStatus('svgStatus', 'SVG水印已清除', 'info');
        }

        // 隐形水印相关功能
        function createStealthWatermark() {
            const container = document.getElementById('stealthWatermarkArea');
            
            // 方法1:零宽字符
            const zeroWidthChars = '\u200B\u200C\u200D\uFEFF';
            const hiddenText = watermarkConfig.userId.split('').join(zeroWidthChars);
            
            // 方法2:隐藏元素
            const hiddenElement = document.createElement('div');
            hiddenElement.className = 'stealth-watermark';
            hiddenElement.textContent = `WATERMARK:${watermarkConfig.userId}:${watermarkConfig.timestamp}`;
            hiddenElement.setAttribute('data-watermark', 'true');
            
            container.appendChild(hiddenElement);
            
            // 方法3:在现有文本中插入零宽字符
            const textElement = container.querySelector('.demo-content p:last-child');
            if (textElement) {
                textElement.innerHTML = textElement.innerHTML + hiddenText;
            }
            
            document.getElementById('stealthTimestamp').textContent = new Date().toLocaleTimeString();
            showStatus('stealthStatus', '隐形水印已嵌入(零宽字符+隐藏元素)', 'success');
        }

        function detectStealthWatermark() {
            const container = document.getElementById('stealthWatermarkArea');
            let detectedInfo = [];
            
            // 检测隐藏元素
            const hiddenElements = container.querySelectorAll('.stealth-watermark');
            if (hiddenElements.length > 0) {
                hiddenElements.forEach(el => {
                    detectedInfo.push(`隐藏元素: ${el.textContent}`);
                });
            }
            
            // 检测零宽字符(简化版)
            const textContent = container.textContent;
            const hasZeroWidth = /[\u200B\u200C\u200D\uFEFF]/.test(textContent);
            if (hasZeroWidth) {
                detectedInfo.push('检测到零宽字符水印');
            }
            
            if (detectedInfo.length > 0) {
                showStatus('stealthStatus', `检测到隐形水印: ${detectedInfo.join(', ')}`, 'info');
            } else {
                showStatus('stealthStatus', '未检测到隐形水印', 'error');
            }
        }

        function clearStealthWatermark() {
            const container = document.getElementById('stealthWatermarkArea');
            const hiddenElements = container.querySelectorAll('.stealth-watermark');
            hiddenElements.forEach(el => el.remove());
            
            // 清理零宽字符(重新设置文本)
            const textElement = container.querySelector('.demo-content p:last-child');
            if (textElement) {
                textElement.innerHTML = `用户ID: USER_12345 | 时间戳: <span id="stealthTimestamp"></span>`;
            }
            
            showStatus('stealthStatus', '隐形水印已清除', 'info');
        }

        // 攻击模拟
        function simulateCSSSOverride() {
            // 尝试用CSS隐藏水印
            const style = document.createElement('style');
            style.textContent = `
                .watermark-dom { display: none !important; }
                .watermark-item { visibility: hidden !important; }
            `;
            document.head.appendChild(style);
            
            setTimeout(() => {
                document.head.removeChild(style);
            }, 3000);
            
            showStatus('domStatus', '模拟CSS覆盖攻击(3秒后恢复)', 'error');
        }

        function simulateDOMRemoval() {
            const watermarks = document.querySelectorAll('.watermark-dom, .stealth-watermark');
            watermarks.forEach(el => el.remove());
            showStatus('domStatus', '模拟DOM删除攻击', 'error');
        }

        function simulateEventBlocking() {
            // 临时禁用保护
            const originalEnabled = protectionEnabled;
            protectionEnabled = false;
            
            setTimeout(() => {
                protectionEnabled = originalEnabled;
            }, 5000);
            
            showStatus('domStatus', '模拟事件阻断攻击(5秒后恢复保护)', 'error');
        }

        // 初始化
        function init() {
            // 更新时间显示
            function updateTime() {
                const timeElement = document.getElementById('currentTime');
                if (timeElement) {
                    timeElement.textContent = new Date().toLocaleTimeString();
                }
            }
            updateTime();
            setInterval(updateTime, 1000);
            
            // 添加初始日志
            addLog('domLog', '水印防护系统已初始化');
            addLog('domLog', '支持的防护技术: DOM监听、Canvas背景、SVG矢量、隐形水印');
            addLog('domLog', '请点击按钮开始测试...');
            
            console.log('🛡️ 水印防护系统已就绪');
        }

        // 页面加载后初始化
        window.addEventListener('load', init);
        
        // 页面卸载前清理
        window.addEventListener('beforeunload', () => {
            observers.forEach(observer => observer.disconnect());
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 理解明水印和暗水印的区别和实现原理
  • 掌握MutationObserver API的监听和防护机制
  • 了解多种水印生成方式(DOM、Canvas、SVG)

加分项:

  • 提及CSS样式保护和混淆技术
  • 了解服务端验证和数字指纹技术
  • 知道水印防护的局限性和攻防平衡

常见失误:

  • 认为前端技术可以完全防止水印移除
  • 忽略性能影响和用户体验
  • 不了解暗水印的技术原理

延伸阅读:

富文本里面, 是如何做到划词的(鼠标滑动选择一组字符, 对组字符进行操作)

答案

核心概念:

富文本划词功能基于浏览器的Selection API和Range API实现。核心流程包括:监听鼠标事件、获取文本选择范围、计算选择位置、处理用户操作。主要涉及window.getSelection()Range对象、contextmenu事件等API。广泛应用于富文本编辑器、文档阅读器、在线协作工具等场景,需要考虑跨浏览器兼容性、性能优化和用户体验。

实际示例:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

富文本划词功能的实现包含以下关键技术点:

  1. Selection API: 使用window.getSelection()获取用户选择的文本范围
  2. Range API: 通过Range对象精确控制文本选择的起始和结束位置
  3. 事件监听: 监听mouseupselectionchangecontextmenu等事件
  4. 位置计算: 使用getBoundingClientRect()计算选择区域的屏幕位置
  5. 动态菜单: 根据选择状态动态显示工具栏或右键菜单

面试官视角:

主要考察 DOM 操作方法,特别是 getSelection API
属于较为冷门的知识点,通常在富文本编辑器开发经验的候选人面试中会涉及

这个问题考查候选人对浏览器原生文本选择API的理解程度。优秀的回答应该包含:Selection和Range API的使用、事件处理机制、位置计算方法、性能优化策略。进阶讨论可能涉及:跨浏览器兼容性处理、移动端适配、与现代框架的集成方案。

延伸阅读:

如何在划词选择的文本上添加右键菜单(划词:鼠标滑动选择一组字符, 对组字符进行操作)

答案

核心概念:

划词右键菜单功能通过监听contextmenu事件和getSelection API实现。核心流程包括:监听文本选择、捕获右键事件、获取选中文本范围、创建自定义菜单、处理菜单操作。常用于富文本编辑器、文档阅读器等场景,需要考虑位置计算、事件冒泡、菜单隐藏等细节。

实际示例:

主要考察 dom 方法, getSelection 属于很冷门知识, 只会在做过富文本的同学面试过程中可能会问得到。

要在划词选择的文本上添加右键菜单,可以按照以下步骤进行操作:

  1. 监听鼠标右键事件 在文档或富文本区域上添加 contextmenu 事件的监听。
document.addEventListener('contextmenu', function (event) {
// 阻止默认的浏览器右键菜单
event.preventDefault()

// 在此处显示自定义右键菜单
showCustomMenu(event)
})
  1. 显示自定义右键菜单 创建一个自定义的菜单元素,并根据选择的文本设置菜单选项。
function showCustomMenu (event) {
const customMenu = document.createElement('div')
customMenu.style.position = 'absolute'
customMenu.style.left = event.clientX + 'px'
customMenu.style.top = event.clientY + 'px'

// 添加菜单选项
const menuItem1 = document.createElement('div')
menuItem1.textContent = '复制'
menuItem1.addEventListener('click', function () {
// 处理复制操作
copySelectedText()
})
customMenu.appendChild(menuItem1)

// 可以添加更多的菜单选项

document.body.appendChild(customMenu)
}
  1. 处理菜单选项的操作 例如,实现复制选中文本的功能。
function copySelectedText () {
const selection = window.getSelection()
if (selection) {
const range = selection.getRangeAt(0)
const clipboardData = new ClipboardEvent('copy', {
clipboardData: { text: range.toString() },
bubbles: true
}).clipboardData
document.execCommand('copy', false, clipboardData)
}
}
  1. 隐藏右键菜单 当用户点击菜单之外的区域时,隐藏自定义右键菜单。
document.addEventListener('click', function (event) {
const customMenu = document.querySelector('.custom-menu')
if (customMenu && !customMenu.contains(event.target)) {
customMenu.remove()
}
})

面试官视角:

要点清单:

  • 理解getSelection API和文本选择原理
  • 掌握contextmenu事件处理和默认行为阻止
  • 了解自定义菜单的创建、定位和管理

加分项:

  • 实现复杂的菜单功能(复制、搜索、翻译等)
  • 处理跨元素选择和Rich Text情况
  • 提供良好的用户体验和错误处理

常见失误:

  • 忘记阻止默认右键菜单
  • 菜单定位计算不准确
  • 没有处理菜单的隐藏和清理

延伸阅读: