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

1022 lines
38 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-js {
width: 100%;
border-radius: 8px;
}
.video-js .vjs-tech {
border-radius: 8px;
}
/* Video.js 自定义样式 */
.video-js .vjs-big-play-button {
font-size: 3em;
line-height: 1.5em;
height: 1.5em;
width: 3em;
border-radius: 0.3em;
background-color: rgba(43, 51, 63, 0.7);
border: none;
margin-top: -0.75em;
margin-left: -1.5em;
}
.video-js .vjs-control-bar {
background: rgba(43, 51, 63, 0.7);
color: #fff;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.control-group h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
margin: 0;
}
.radio-item label {
font-size: 13px;
cursor: pointer;
user-select: none;
}
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;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
.progress-container {
margin: 15px 0;
position: relative;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
border-radius: 4px;
width: 0%;
transition: width 0.1s ease;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 5px;
font-size: 12px;
color: #666;
}
.time-display {
font-family: monospace;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/video.js@8.22.0/dist/video.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/video.js@8.22.0/dist/video-js.min.css">
<script>
const videos = [
{ src: 'hls/master.m3u8', type: 'application/x-mpegURL' },
{ src: 'demo01.mp4', type: 'video/mp4' },
{ src: 'demo02.mp4', type: 'video/mp4' },
{ src: 'demo03.mp4', type: 'video/mp4' }
];
// const VIDEO_URL = 'hls/master.m3u8'; // 使用支持CORS的HLS视频源
// const VIDEO_URL = 'output.mp4';
// const VIDEO_URL = 'merged_output.mp4';
// const VIDEO_URL = 'merged_dual_track.mp4';
// const VIDEO_URL = 'demo01.mp4';
// const VIDEO_URL = 'demo02.mp4';
// const VIDEO_URL = 'demo03.mp4';
</script>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" class="video-js vjs-default-skin" crossorigin="anonymous" preload="auto" data-setup='{}'>
<p class="vjs-no-js">
要查看此视频,请启用 JavaScript并考虑升级到
<a href="https://videojs.com/html5-video-support/" target="_blank">
支持HTML5视频的网络浏览器
</a>。
</p>
</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">
</div>
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-info">
<span class="time-display" id="currentTime">00:00</span>
<span class="time-display" id="duration">00:00</span>
</div>
</div>
<div class="control-group">
<h4>音轨选择</h4>
<div class="radio-group" id="trackRadioGroup">
<div class="radio-item">
<input type="radio" id="track0" name="audioTrack" value="0" checked>
<label for="track0">默认音轨</label>
</div>
</div>
</div>
<div class="control-group">
<h4>播放速度</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="rate05" name="playbackRate" value="0.5">
<label for="rate05">0.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate075" name="playbackRate" value="0.75">
<label for="rate075">0.75x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate1" name="playbackRate" value="1" checked>
<label for="rate1">1x (正常)</label>
</div>
<div class="radio-item">
<input type="radio" id="rate125" name="playbackRate" value="1.25">
<label for="rate125">1.25x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate15" name="playbackRate" value="1.5">
<label for="rate15">1.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate2" name="playbackRate" value="2">
<label for="rate2">2x</label>
</div>
</div>
</div>
<div class="control-group">
<h4>音调变换</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="pitch-12" name="pitchShift" value="-12">
<label for="pitch-12">降一个八度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-7" name="pitchShift" value="-7">
<label for="pitch-7">降五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-5" name="pitchShift" value="-5">
<label for="pitch-5">降四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-3" name="pitchShift" value="-3">
<label for="pitch-3">降小三度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-2" name="pitchShift" value="-2">
<label for="pitch-2">降大二度 (C→B♭)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-1" name="pitchShift" value="-1">
<label for="pitch-1">降小二度 (C→B)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch0" name="pitchShift" value="0" checked>
<label for="pitch0">原调</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch1" name="pitchShift" value="1">
<label for="pitch1">升小二度 (C→C#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch2" name="pitchShift" value="2">
<label for="pitch2">升大二度 (C→D)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch3" name="pitchShift" value="3">
<label for="pitch3">升小三度 (C→D#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch4" name="pitchShift" value="4">
<label for="pitch4">升大三度 (C→E)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch5" name="pitchShift" value="5">
<label for="pitch5">升四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch7" name="pitchShift" value="7">
<label for="pitch7">升五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch12" name="pitchShift" value="12">
<label for="pitch12">升一个八度</label>
</div>
</div>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
// Video.js 播放器实例
let player;
let video; // 将在 Video.js 初始化后设置
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 trackRadioGroup = document.getElementById('trackRadioGroup');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
const currentTimeDisplay = document.getElementById('currentTime');
const durationDisplay = document.getElementById('duration');
// 固定视频地址 - 使用支持CORS的视频源
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let soundTouchNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
let isWorkletRegistered = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 格式化时间显示(秒转换为 mm:ss 格式)
function formatTime(seconds) {
if (isNaN(seconds) || !isFinite(seconds)) {
return '00:00';
}
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 更新进度条
function updateProgress() {
if (player && player.duration() && isFinite(player.duration())) {
const progress = (player.currentTime() / player.duration()) * 100;
progressFill.style.width = `${progress}%`;
currentTimeDisplay.textContent = formatTime(player.currentTime());
durationDisplay.textContent = formatTime(player.duration());
}
}
// 处理进度条点击跳转
function handleProgressBarClick(event) {
const rect = progressBar.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const progressBarWidth = rect.width;
const clickRatio = clickX / progressBarWidth;
if (player && player.duration() && isFinite(player.duration())) {
const newTime = clickRatio * player.duration();
player.currentTime(Math.max(0, Math.min(newTime, player.duration())));
console.log(`跳转到: ${formatTime(newTime)}`);
}
}
// 创建 SoundTouch AudioWorkletNode (现代化音调变换)
async function createSoundTouchNode(audioContext) {
try {
// 首先注册 AudioWorklet 处理器
if (!isWorkletRegistered) {
console.log('注册 SoundTouch AudioWorklet...');
await audioContext.audioWorklet.addModule('https://cdn.jsdelivr.net/npm/@soundtouchjs/audio-worklet@0.2.1/dist/soundtouch-worklet.js');
isWorkletRegistered = true;
console.log('SoundTouch AudioWorklet 注册成功');
}
// 创建 AudioWorkletNode
const workletNode = new AudioWorkletNode(audioContext, 'soundtouch-processor');
console.log('SoundTouch AudioWorkletNode 创建成功');
return workletNode;
} catch (error) {
console.error('创建 SoundTouch 节点失败:', error);
// 如果失败,返回一个直通节点
return createPassThroughNode(audioContext);
}
}
// 创建直通节点作为后备方案
function createPassThroughNode(audioContext) {
console.log('创建直通节点作为后备方案');
const gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
return gainNode;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 如果有 SoundTouch 节点,设置音调参数
if (soundTouchNode && soundTouchNode.parameters) {
try {
// 使用 pitchSemitones 参数来设置半音变换
const pitchSemitonesParam = soundTouchNode.parameters.get('pitchSemitones');
if (pitchSemitonesParam) {
pitchSemitonesParam.value = semitones;
console.log('SoundTouch 音调半音设置为:', semitones);
} else {
console.warn('pitchSemitones 参数不可用,尝试使用 pitch 参数');
// 备选方案:使用 pitch 参数(比率)
const pitchParam = soundTouchNode.parameters.get('pitch');
if (pitchParam) {
const pitchRatio = Math.pow(2, semitones / 12);
pitchParam.value = pitchRatio;
console.log('SoundTouch 音调比率设置为:', pitchRatio);
}
}
} catch (error) {
console.warn('设置 SoundTouch 参数时出错:', error);
}
}
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = player ? player.playbackRate() : 1;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
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('增益节点已创建');
// 创建 SoundTouch 音调变换节点
soundTouchNode = await createSoundTouchNode(audioContext);
console.log('SoundTouch 节点已创建');
// 连接音频节点链:视频 -> SoundTouch -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(soundTouchNode);
soundTouchNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(使用 SoundTouch AudioWorklet');
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 (player && !player.paused()) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (player && player.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;
}
}
// 初始化 Video.js 播放器
function initVideoPlayer() {
console.log('初始化 Video.js 播放器...');
player = videojs('video', {
controls: true,
fluid: true,
responsive: true,
playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2],
plugins: {
// 可以在这里添加插件配置
}
});
// 获取底层的 HTML5 video 元素
video = player.tech(true).el();
player.ready(() => {
console.log('Video.js 播放器已准备就绪');
// 设置视频源
player.src({
src: VIDEO_URL,
type: 'video/mp4'
});
console.log('视频源已设置:', VIDEO_URL);
statusText.textContent = 'Video.js 播放器已初始化,视频源已加载';
statusText.style.color = 'green';
// 启用控制按钮
playBtn.disabled = false;
pauseBtn.disabled = false;
});
// Video.js 事件监听
player.on('loadedmetadata', () => {
console.log('Video.js: 视频元数据已加载');
console.log('视频时长:', player.duration());
console.log('视频尺寸:', player.videoWidth(), 'x', player.videoHeight());
// 初始化进度条
updateProgress();
// 处理音轨信息
handleAudioTracks();
});
player.on('play', () => {
console.log('Video.js: 播放开始');
handlePlayEvent();
});
player.on('pause', () => {
console.log('Video.js: 播放暂停');
handlePauseEvent();
});
player.on('ended', () => {
console.log('Video.js: 播放结束');
handleEndedEvent();
});
player.on('timeupdate', () => {
updateProgress();
});
player.on('volumechange', () => {
console.log('Video.js: 音量变化:', player.volume(), '静音:', player.muted());
// 同步到自定义音量控件
volumeInput.value = player.volume();
muteBtn.textContent = player.muted() ? '取消静音' : '静音';
});
player.on('error', (error) => {
console.error('Video.js 错误:', error);
statusText.textContent = `播放器错误: ${error.message || '未知错误'}`;
statusText.style.color = 'red';
});
return player;
}
// 处理音轨信息
function handleAudioTracks() {
console.log('处理音轨信息...');
// 清空现有的音轨选项
trackRadioGroup.innerHTML = '';
// 获取音轨信息
const audioTracks = video.audioTracks;
if (audioTracks && audioTracks.length > 0) {
console.log('发现', audioTracks.length, '个音轨');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = `track${i}`;
radio.value = i;
radio.checked = track.enabled;
const label = document.createElement('label');
label.htmlFor = `track${i}`;
label.textContent = track.label || track.language || `音轨 ${i + 1}`;
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
// 添加音轨切换事件
radio.addEventListener('change', () => {
if (radio.checked) {
console.log('切换到音轨:', i);
// 禁用所有音轨
for (let j = 0; j < audioTracks.length; j++) {
audioTracks[j].enabled = false;
}
// 启用选中的音轨
audioTracks[i].enabled = true;
}
});
}
} else {
// 默认音轨
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = 'track0';
radio.value = '0';
radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'track0';
label.textContent = '默认音轨';
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log('使用默认音轨配置');
}
}
// 处理播放事件
function handlePlayEvent() {
playBtn.disabled = true;
pauseBtn.disabled = false;
const currentRate = player.playbackRate();
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 启动音频可视化
if (isAudioContextInitialized && analyser) {
startVisualization();
}
}
// 处理暂停事件
function handlePauseEvent() {
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
stopVisualization();
}
// 处理结束事件
function handleEndedEvent() {
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '播放结束';
statusText.style.color = '#666';
stopVisualization();
}
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 使用 Video.js 播放
if (player) {
await player.play();
console.log('Video.js 开始播放');
} else {
console.error('Video.js 播放器未初始化');
}
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
if (player) {
player.pause();
console.log('Video.js 暂停播放');
}
});
muteBtn.addEventListener('click', () => {
if (player) {
player.muted(!player.muted());
muteBtn.textContent = player.muted() ? '取消静音' : '静音';
console.log('Video.js 静音状态:', player.muted());
}
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('Video.js 播放器状态:');
if (player) {
console.log('- 是否暂停:', player.paused());
console.log('- 是否静音:', player.muted());
console.log('- 音量:', player.volume());
console.log('- 当前时间:', player.currentTime());
console.log('- 总时长:', player.duration());
console.log('- 播放速度:', player.playbackRate());
} else {
console.log('- Video.js 播放器未初始化');
}
console.log('底层视频元素状态:');
if (video) {
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
} else {
console.log('- 底层视频元素未获取');
}
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 (player && !player.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.js
if (player) {
player.volume(volume);
}
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
}
console.log('音量已更新:', volume);
});
// 播放速度控制
document.querySelectorAll('input[name="playbackRate"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const playbackRate = parseFloat(e.target.value);
// 使用 Video.js 设置播放速度
if (player) {
player.playbackRate(playbackRate);
}
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (player && !player.paused()) {
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
}
});
});
// 音调变换控制
document.querySelectorAll('input[name="pitchShift"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
}
});
});
// 监听原生视频事件(通过 Video.js 已经处理,这里移除重复的事件监听)
// 所有事件现在通过 Video.js 的事件系统处理
// 进度条点击跳转
progressBar.addEventListener('click', handleProgressBarClick);
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 初始化 Video.js 播放器
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 加载完成,初始化 Video.js...');
initVideoPlayer();
});
</script>
</body>
</html>