Files
video-player-demo/index.html.v1
2025-06-06 16:59:09 +08:00

565 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#trackSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" controls crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
<span>音轨:</span>
<select id="trackSelect" disabled></select>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackSelect = document.getElementById('trackSelect');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
const VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 连接音频节点链:视频 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
trackSelect.disabled = true;
trackSelect.innerHTML = '';
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
trackSelect.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
trackSelect.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const option = document.createElement('option');
option.value = i;
option.text = track.label || track.language || `音轨 ${i + 1}`;
option.selected = track.enabled;
trackSelect.appendChild(option);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
}
// 音轨切换事件
trackSelect.addEventListener('change', (e) => {
const selectedIndex = parseInt(e.target.value);
console.log('切换到音轨:', selectedIndex);
// 禁用所有音轨
for (let i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = false;
}
// 启用选中的音轨
if (audioTracks[selectedIndex]) {
audioTracks[selectedIndex].enabled = true;
console.log('已启用音轨:', selectedIndex);
}
});
}
// 如果没有多个音轨,显示默认选项
else {
const option = document.createElement('option');
option.value = 0;
option.text = '默认音轨';
trackSelect.appendChild(option);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>