添加网络音频播放功能
This commit is contained in:
parent
38b1c613b5
commit
3ec43d5240
@ -12,13 +12,19 @@
|
||||
<h1>音乐播放器</h1>
|
||||
|
||||
<!-- 文件选择 -->
|
||||
<div class="file-selector">
|
||||
<div class="file-selector" id="fileSelector">
|
||||
<input type="file" id="audioFile" accept="audio/*" />
|
||||
<label for="audioFile" class="file-label">选择音乐文件</label>
|
||||
<input type="file" id="audioFolder" webkitdirectory directory multiple accept="audio/*" style="display: none;" />
|
||||
<label for="audioFolder" class="file-label folder-label">选择音乐文件夹</label>
|
||||
</div>
|
||||
|
||||
<!-- 网络音乐信息(默认隐藏) -->
|
||||
<div class="network-music-info" id="networkMusicInfo" style="display: none;">
|
||||
<p>🌐 正在播放网络音乐</p>
|
||||
<p id="networkMusicName">-</p>
|
||||
</div>
|
||||
|
||||
<!-- 当前播放信息 -->
|
||||
<div class="track-info">
|
||||
<p id="trackName">请选择音乐文件</p>
|
||||
|
||||
753
src/main.js
753
src/main.js
@ -20,6 +20,11 @@ class AudioPlayer {
|
||||
this.isPlayingFolder = false;
|
||||
this.playbackMode = 'loop'; // 默认列表循环模式:'single', 'loop', 'random'
|
||||
|
||||
// 网络音乐相关
|
||||
this.isNetworkMusic = false;
|
||||
this.networkMusicUrl = null;
|
||||
this.networkMusicName = null;
|
||||
|
||||
// Web Audio API警告标志
|
||||
this.webAudioApiWarningShown = false;
|
||||
|
||||
@ -31,6 +36,7 @@ class AudioPlayer {
|
||||
this.setupCanvas();
|
||||
this.setupKeyboardShortcuts();
|
||||
this.initializePlayerState();
|
||||
this.checkUrlParams(); // 检查URL参数
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
@ -164,6 +170,12 @@ class AudioPlayer {
|
||||
}
|
||||
|
||||
handleFileSelect(event) {
|
||||
// 如果正在播放网络音乐,不允许选择本地文件
|
||||
if (this.isNetworkMusic) {
|
||||
alert('正在播放网络音乐,请先停止当前播放再选择本地文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
// 单个文件播放模式
|
||||
@ -172,7 +184,7 @@ class AudioPlayer {
|
||||
this.currentTrackIndex = 0;
|
||||
|
||||
// 清理旧的URL
|
||||
if (this.audio.src) {
|
||||
if (this.audio.src && !this.isNetworkMusic) {
|
||||
URL.revokeObjectURL(this.audio.src);
|
||||
}
|
||||
|
||||
@ -186,10 +198,20 @@ class AudioPlayer {
|
||||
|
||||
// 设置音频上下文(如果尚未初始化)
|
||||
this.setupAudioContext();
|
||||
|
||||
// 关键:本地音乐也需要连接音频源
|
||||
console.log('本地音乐文件已选择,准备连接音频源');
|
||||
this.connectAudioSource();
|
||||
}
|
||||
}
|
||||
|
||||
handleFolderSelect(event) {
|
||||
// 如果正在播放网络音乐,不允许选择本地文件夹
|
||||
if (this.isNetworkMusic) {
|
||||
alert('正在播放网络音乐,请先停止当前播放再选择本地文件夹');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(event.target.files);
|
||||
if (files.length > 0) {
|
||||
// 筛选音频文件 - 支持更多格式
|
||||
@ -206,6 +228,7 @@ class AudioPlayer {
|
||||
this.playlist = audioFiles;
|
||||
this.currentTrackIndex = 0;
|
||||
this.isPlayingFolder = true;
|
||||
this.isNetworkMusic = false; // 切换到本地模式
|
||||
|
||||
// 确保播放模式显示正确(默认列表循环)
|
||||
this.playbackMode = 'loop';
|
||||
@ -271,6 +294,10 @@ class AudioPlayer {
|
||||
// 设置自动播放状态
|
||||
this.shouldAutoPlay = shouldAutoPlay;
|
||||
|
||||
// 关键:文件夹中的本地音乐也需要连接音频源
|
||||
console.log('文件夹音乐已加载,准备连接音频源');
|
||||
this.connectAudioSource();
|
||||
|
||||
// 等待音频数据加载完成后播放
|
||||
const tryAutoPlay = () => {
|
||||
if (this.audio.readyState >= 2 && (!this.audio.paused || this.shouldAutoPlay)) {
|
||||
@ -336,13 +363,11 @@ class AudioPlayer {
|
||||
console.log('音频上下文和可视化器初始化成功');
|
||||
}
|
||||
|
||||
// 连接音频源(只在首次或需要重新连接时)
|
||||
if (this.audioContext && this.analyser && !this.source) {
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
console.log('音频源连接成功');
|
||||
// 确保音频上下文处于运行状态
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('音频上下文初始化失败:', error);
|
||||
if (!this.webAudioApiWarningShown) {
|
||||
@ -352,24 +377,386 @@ class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
// 为网络音乐创建一个智能模拟可视化系统,绕过CORS限制
|
||||
startNetworkMusicVisualization() {
|
||||
console.log('🔄 启动网络音乐智能模拟可视化...');
|
||||
|
||||
// 检查是否有音频上下文和画布
|
||||
if (!this.audioContext || !this.canvas) {
|
||||
console.error('无法启动模拟可视化:缺少必要组件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个音频活动和节奏检测器
|
||||
const rhythmDetectorState = {
|
||||
lastTime: 0,
|
||||
timeDelta: 0,
|
||||
energy: 0,
|
||||
energyHistory: []
|
||||
};
|
||||
|
||||
// 检测音频活动
|
||||
const detectActivity = () => {
|
||||
return !this.audio.paused && this.audio.currentTime > 0;
|
||||
};
|
||||
|
||||
// 计算节奏能量(基于播放速度变化)
|
||||
const calculateRhythmEnergy = () => {
|
||||
if (!detectActivity()) return 0;
|
||||
|
||||
const currentTime = this.audio.currentTime;
|
||||
rhythmDetectorState.timeDelta = currentTime - rhythmDetectorState.lastTime;
|
||||
rhythmDetectorState.lastTime = currentTime;
|
||||
|
||||
// 计算能量(模拟音频波形的振幅)
|
||||
const baseEnergy = Math.sin(currentTime * 3) * 0.5 + 0.5;
|
||||
const beatEnergy = Math.sin(currentTime * 1.5) > 0.8 ? 1 : 0;
|
||||
const energy = baseEnergy + beatEnergy * 0.5;
|
||||
|
||||
// 平滑能量值
|
||||
rhythmDetectorState.energyHistory.push(energy);
|
||||
if (rhythmDetectorState.energyHistory.length > 10) rhythmDetectorState.energyHistory.shift();
|
||||
|
||||
rhythmDetectorState.energy = rhythmDetectorState.energyHistory.reduce((sum, val) => sum + val, 0) / rhythmDetectorState.energyHistory.length;
|
||||
return rhythmDetectorState.energy;
|
||||
};
|
||||
|
||||
// 智能可视化绘制函数
|
||||
const drawSmartVisualization = () => {
|
||||
if (!detectActivity()) {
|
||||
// 音频未播放,清空画布
|
||||
this.ctx.fillStyle = 'rgb(0, 0, 0)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
return;
|
||||
}
|
||||
|
||||
const time = this.audio.currentTime;
|
||||
const energy = calculateRhythmEnergy();
|
||||
|
||||
// 清除画布
|
||||
this.ctx.fillStyle = 'rgb(0, 0, 0)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 绘制动态条形图(根据能量变化)
|
||||
const barWidth = 8;
|
||||
const barSpacing = 4;
|
||||
const numBars = Math.floor(this.canvas.width / (barWidth + barSpacing));
|
||||
|
||||
for (let i = 0; i < numBars; i++) {
|
||||
// 使用多种波形组合生成更自然的高度变化
|
||||
const wave1 = Math.sin(time * 2 + i * 0.1) * 0.5 + 0.5;
|
||||
const wave2 = Math.cos(time * 1.5 + i * 0.2) * 0.3 + 0.3;
|
||||
const wave3 = Math.sin(time * 0.5 + i * 0.05) * 0.2 + 0.2;
|
||||
|
||||
// 结合能量值调整高度
|
||||
const heightFactor = wave1 + wave2 + wave3;
|
||||
const height = energy * heightFactor * (this.canvas.height * 0.7) + 5;
|
||||
|
||||
// 颜色随能量和位置动态变化
|
||||
const r = Math.floor(200 + energy * 55 * Math.sin(i * 0.1 + time));
|
||||
const g = Math.floor(100 + energy * 155 * Math.cos(i * 0.1 + time));
|
||||
const b = Math.floor(150 + energy * 105 * Math.sin(i * 0.2 + time));
|
||||
|
||||
this.ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
||||
|
||||
// 绘制带圆角的条形
|
||||
this.ctx.fillRect(
|
||||
i * (barWidth + barSpacing),
|
||||
this.canvas.height - height,
|
||||
barWidth,
|
||||
height
|
||||
);
|
||||
|
||||
// 添加顶部高光
|
||||
this.ctx.fillStyle = `rgba(255, 255, 255, ${energy * 0.3})`;
|
||||
this.ctx.fillRect(
|
||||
i * (barWidth + barSpacing),
|
||||
this.canvas.height - height,
|
||||
barWidth,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
// 绘制中央波形线(响应能量变化)
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = `rgba(0, ${Math.floor(255 * energy)}, 255, ${0.8 + energy * 0.2})`;
|
||||
this.ctx.lineWidth = 3;
|
||||
|
||||
// 绘制更复杂的波形
|
||||
for (let x = 0; x < this.canvas.width; x += 2) {
|
||||
// 基础波形
|
||||
const baseWave = Math.sin(x * 0.015 + time * 5) * 30;
|
||||
// 能量影响的波形
|
||||
const energyWave = Math.cos(x * 0.02 + time * 3) * 40 * energy;
|
||||
// 高频细节
|
||||
const detailWave = Math.sin(x * 0.05 + time * 10) * 10;
|
||||
|
||||
const y = this.canvas.height / 2 + baseWave + energyWave + detailWave;
|
||||
|
||||
if (x === 0) {
|
||||
this.ctx.moveTo(x, y);
|
||||
} else {
|
||||
this.ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
|
||||
// 添加脉冲效果(当检测到强节拍时)
|
||||
if (rhythmDetectorState.timeDelta < 0.01 && energy > 0.8) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = `rgba(255, 255, 255, ${energy * 0.6})`;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.arc(
|
||||
this.canvas.width / 2,
|
||||
this.canvas.height / 2,
|
||||
Math.random() * 50 + 100,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
// 定期更新可视化(60fps)
|
||||
const animate = () => {
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
drawSmartVisualization();
|
||||
};
|
||||
|
||||
animate();
|
||||
console.log('✅ 网络音乐智能模拟可视化已启动');
|
||||
}
|
||||
|
||||
connectAudioSource() {
|
||||
try {
|
||||
console.log('🔄 创建新的音频源连接...');
|
||||
|
||||
// 检查音频元素状态
|
||||
if (this.audio.readyState < 2) {
|
||||
console.log('音频元素尚未准备好,延迟连接');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确保没有现有的连接
|
||||
if (this.source) {
|
||||
console.log('清理现有音频源连接');
|
||||
try {
|
||||
this.source.disconnect();
|
||||
} catch (e) {
|
||||
console.log('断开旧连接时出错:', e);
|
||||
}
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
// 关键:重新创建MediaElementSource
|
||||
console.log('创建新的MediaElementSource...');
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
console.log('MediaElementSource创建成功');
|
||||
|
||||
// 连接到分析器
|
||||
console.log('连接到分析器...');
|
||||
this.source.connect(this.analyser);
|
||||
console.log('分析器连接成功');
|
||||
|
||||
console.log('连接到音频目标...');
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
console.log('音频目标连接成功');
|
||||
|
||||
console.log('✅ 音频源连接完成');
|
||||
console.log('连接链路: 音频元素 → MediaElementSource → 分析器' + (this.isNetworkMusic ? '' : ' → 目标'));
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 音频源连接失败:', error);
|
||||
console.error('错误类型:', error.name);
|
||||
console.error('错误消息:', error.message);
|
||||
|
||||
// 特殊处理:音频元素已连接的错误
|
||||
if (error.name === 'InvalidStateError' && error.message.includes('already connected')) {
|
||||
console.log('💡 检测到音频元素已连接,尝试重新初始化...');
|
||||
return this.reinitializeAudioConnection();
|
||||
}
|
||||
|
||||
// 特殊处理:CORS错误
|
||||
if (error.name === 'NotSupportedError' || error.message.includes('CORS')) {
|
||||
console.log('💡 检测到CORS错误,网络音乐将完全独立播放');
|
||||
return true; // 即使连接失败,也返回成功,让网络音乐独立播放
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
tryAlternativeConnection() {
|
||||
console.log('🔄 尝试备用音频连接方式...');
|
||||
|
||||
try {
|
||||
// 清理现有连接
|
||||
if (this.source) {
|
||||
this.source.disconnect();
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
// 创建新的音频源连接,但不连接到destination
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
this.source.connect(this.analyser);
|
||||
|
||||
console.log('✅ 备用连接方式成功');
|
||||
console.log('连接链路: 音频元素 → MediaElementSource → 分析器(跳过destination)');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 备用连接方式失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reinitializeAudioConnection() {
|
||||
console.log('🔄 重新初始化音频连接...');
|
||||
|
||||
try {
|
||||
// 1. 完全重置音频元素
|
||||
const currentSrc = this.audio.src;
|
||||
const currentTime = this.audio.currentTime;
|
||||
const wasPlaying = !this.audio.paused;
|
||||
|
||||
console.log('保存当前播放状态:', { currentTime, wasPlaying });
|
||||
|
||||
// 2. 暂停并清理
|
||||
if (wasPlaying) {
|
||||
this.audio.pause();
|
||||
}
|
||||
|
||||
// 3. 完全断开所有连接
|
||||
if (this.source) {
|
||||
try {
|
||||
this.source.disconnect();
|
||||
this.source = null;
|
||||
} catch (e) {
|
||||
console.log('清理旧连接失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 重新设置音频源(强制重置)
|
||||
this.audio.src = '';
|
||||
this.audio.removeAttribute('src');
|
||||
|
||||
// 5. 重新设置跨域属性和音频源
|
||||
this.audio.crossOrigin = 'anonymous';
|
||||
this.audio.src = currentSrc;
|
||||
this.audio.load();
|
||||
|
||||
// 6. 等待音频准备好
|
||||
return new Promise((resolve) => {
|
||||
const onCanPlay = () => {
|
||||
console.log('音频重新加载完成');
|
||||
this.audio.removeEventListener('canplay', onCanPlay);
|
||||
|
||||
// 恢复播放位置
|
||||
this.audio.currentTime = currentTime;
|
||||
|
||||
// 重新连接音频源
|
||||
try {
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
console.log('✅ 重新初始化音频连接成功');
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
console.error('重新连接失败:', e);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.audio.addEventListener('canplay', onCanPlay, { once: true });
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('重新初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.audio.src) {
|
||||
console.log('开始播放,音频源:', this.audio.src);
|
||||
console.log('网络音乐模式:', this.isNetworkMusic);
|
||||
|
||||
// 确保音频上下文处于运行状态
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
this.audio.play().then(() => {
|
||||
this.startVisualization();
|
||||
this.updatePlayPauseButton(true);
|
||||
document.querySelector('.audio-player').classList.add('playing');
|
||||
}).catch(error => {
|
||||
console.error('播放失败:', error);
|
||||
alert('播放失败,请检查音频文件');
|
||||
});
|
||||
// 关键:确保音频源已连接,无论是网络音乐还是本地音乐
|
||||
if (!this.source) {
|
||||
console.log('音频源未连接,尝试重新连接');
|
||||
this.setupAudioContext();
|
||||
|
||||
// 延迟播放,等待音频源连接
|
||||
setTimeout(() => {
|
||||
if (this.source) {
|
||||
console.log('音频源已连接,继续播放');
|
||||
this.performPlay();
|
||||
} else {
|
||||
console.error('音频源连接失败,尝试直接播放');
|
||||
this.performPlay();
|
||||
}
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保音频元素已准备好
|
||||
if (this.audio.readyState < 2) {
|
||||
console.log('音频尚未准备好,等待canplay事件');
|
||||
this.audio.addEventListener('canplay', () => {
|
||||
console.log('音频已准备好,开始播放');
|
||||
this.performPlay();
|
||||
}, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.performPlay();
|
||||
}
|
||||
}
|
||||
|
||||
performPlay() {
|
||||
console.log('执行播放操作');
|
||||
|
||||
// 确保音量未被静音且设置正确
|
||||
console.log('当前音量设置:', this.audio.volume);
|
||||
console.log('静音状态:', this.isMuted);
|
||||
|
||||
// 如果是静音状态,临时取消静音用于测试
|
||||
if (this.isMuted) {
|
||||
this.audio.volume = 0.5;
|
||||
this.isMuted = false;
|
||||
this.updateVolumeIcon(50);
|
||||
console.log('已临时取消静音');
|
||||
}
|
||||
|
||||
this.audio.play().then(() => {
|
||||
console.log('播放成功,启动可视化');
|
||||
console.log('实际播放音量:', this.audio.volume);
|
||||
console.log('播放状态:', this.audio.paused ? '暂停' : '播放中');
|
||||
|
||||
// 可视化
|
||||
this.startVisualization();
|
||||
|
||||
this.updatePlayPauseButton(true);
|
||||
document.querySelector('.audio-player').classList.add('playing');
|
||||
|
||||
}).catch(error => {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
console.error('播放被阻止,请点击播放按钮手动开始播放');
|
||||
} else {
|
||||
console.error('播放失败: ' + error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.audio.pause();
|
||||
this.stopVisualization();
|
||||
@ -487,24 +874,76 @@ class AudioPlayer {
|
||||
}
|
||||
|
||||
startVisualization() {
|
||||
|
||||
if (!this.waveformVisualizer) {
|
||||
console.warn('可视化器未初始化,使用备用方案');
|
||||
this.startBasicVisualization();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.analyser) {
|
||||
console.error('分析器未创建,无法开始可视化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 关键:确保音频源连接,无论是网络音乐还是本地音乐
|
||||
if (!this.source) {
|
||||
console.warn('音频源未连接,尝试重新连接');
|
||||
|
||||
// 强制重新设置音频上下文
|
||||
this.setupAudioContext();
|
||||
|
||||
// 尝试连接音频源
|
||||
const connected = this.connectAudioSource();
|
||||
|
||||
if (!connected) {
|
||||
// 尝试备用连接方式
|
||||
console.warn('常规连接失败,尝试备用连接方式...');
|
||||
const altConnected = this.tryAlternativeConnection();
|
||||
|
||||
if (!altConnected) {
|
||||
// 延迟开始可视化,等待连接完成
|
||||
setTimeout(() => {
|
||||
console.log('延迟后开始可视化检查');
|
||||
if (this.source) {
|
||||
console.log('✅ 音频源已连接,开始可视化');
|
||||
this.startVisualization();
|
||||
} else {
|
||||
console.error('❌ 音频源连接失败,使用基础可视化');
|
||||
this.startBasicVisualization();
|
||||
}
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const draw = () => {
|
||||
this.animationId = requestAnimationFrame(draw);
|
||||
|
||||
// 测试频率数据
|
||||
if (this.analyser && this.dataArray) {
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
}
|
||||
|
||||
this.waveformVisualizer.draw();
|
||||
};
|
||||
|
||||
draw();
|
||||
console.log('可视化已启动');
|
||||
}
|
||||
|
||||
startBasicVisualization() {
|
||||
// 备用基础可视化方案
|
||||
if (!this.analyser) return;
|
||||
|
||||
console.log('启动基础可视化方案');
|
||||
|
||||
if (!this.analyser || !this.dataArray) {
|
||||
console.error('基础可视化失败:分析器或数据数组未准备好');
|
||||
return;
|
||||
}
|
||||
|
||||
const draw = () => {
|
||||
this.animationId = requestAnimationFrame(draw);
|
||||
|
||||
@ -634,12 +1073,12 @@ class AudioPlayer {
|
||||
}
|
||||
break;
|
||||
case 'n':
|
||||
if (this.isPlayingFolder) {
|
||||
if (this.isPlayingFolder && !this.isNetworkMusic) {
|
||||
this.nextTrack();
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
if (this.isPlayingFolder) {
|
||||
if (this.isPlayingFolder && !this.isNetworkMusic) {
|
||||
this.previousTrack();
|
||||
}
|
||||
break;
|
||||
@ -660,7 +1099,204 @@ class AudioPlayer {
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
checkUrlParams() {
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let filePath = urlParams.get('filePath');
|
||||
let fileName = urlParams.get('fileName') || '网络音乐';
|
||||
|
||||
try {
|
||||
filePath = decodeURIComponent(decodeURIComponent(filePath));
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
try {
|
||||
fileName = decodeURIComponent(decodeURIComponent(fileName));
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (filePath) {
|
||||
this.loadNetworkMusic(filePath, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
loadNetworkMusic(filePath, fileName) {
|
||||
try {
|
||||
console.log('开始加载网络音乐:', filePath, fileName);
|
||||
|
||||
// 设置网络音乐状态
|
||||
this.isNetworkMusic = true;
|
||||
this.networkMusicUrl = filePath;
|
||||
this.networkMusicName = fileName;
|
||||
|
||||
// 隐藏文件选择按钮
|
||||
const fileSelector = document.getElementById('fileSelector');
|
||||
const networkMusicInfo = document.getElementById('networkMusicInfo');
|
||||
//const networkMusicNameEl = document.getElementById('networkMusicName');
|
||||
|
||||
if (fileSelector) {
|
||||
fileSelector.style.display = 'none';
|
||||
}
|
||||
|
||||
// if (networkMusicInfo) {
|
||||
// networkMusicInfo.style.display = 'block';
|
||||
// networkMusicNameEl.textContent = fileName;
|
||||
// }
|
||||
|
||||
// 清理旧的音频源
|
||||
if (this.audio.src && !this.isNetworkMusic) {
|
||||
URL.revokeObjectURL(this.audio.src);
|
||||
}
|
||||
|
||||
// 设置音频源 - 关键:先停止当前播放
|
||||
this.stop();
|
||||
|
||||
// 关键:重置音频元素,确保正确的设置顺序
|
||||
console.log('设置网络音频源,URL:', filePath);
|
||||
|
||||
// 1. 设置crossOrigin,避免CORS错误
|
||||
this.audio.crossOrigin = 'anonymous';
|
||||
// 2. 设置音频源
|
||||
this.audio.src = filePath;
|
||||
console.log('音频源已设置:', this.audio.src);
|
||||
|
||||
// 3. 强制重新加载
|
||||
this.audio.load();
|
||||
console.log('音频加载已触发');
|
||||
|
||||
// 关键:确保音频元素独立播放,不依赖Web Audio API连接
|
||||
console.log('设置网络音乐独立播放模式');
|
||||
|
||||
// 检查并重置音量设置
|
||||
const currentVolume = this.volumeSlider.value / 100;
|
||||
this.audio.volume = currentVolume;
|
||||
this.isMuted = false;
|
||||
console.log('网络音乐音量设置为:', currentVolume);
|
||||
|
||||
this.trackName.textContent = fileName;
|
||||
|
||||
// 显示加载提示
|
||||
this.showLoadingFeedback('正在加载网络音乐...');
|
||||
|
||||
// 监听加载错误 - 更详细的错误处理
|
||||
this.audio.addEventListener('error', (e) => {
|
||||
console.error('网络音乐加载失败:', e);
|
||||
console.error('错误代码:', this.audio.error?.code);
|
||||
console.error('错误信息:', this.audio.error?.message);
|
||||
|
||||
let errorMsg = '网络音乐加载失败';
|
||||
if (this.audio.error) {
|
||||
switch (this.audio.error.code) {
|
||||
case 1: errorMsg = '音频下载被中止'; break;
|
||||
case 2: errorMsg = '网络错误'; break;
|
||||
case 3: errorMsg = '音频解码错误'; break;
|
||||
case 4: errorMsg = '音频格式不支持'; break;
|
||||
default: errorMsg = '未知错误,请检查URL是否有效且支持跨域访问';
|
||||
}
|
||||
}
|
||||
this.showErrorFeedback(errorMsg);
|
||||
}, { once: true });
|
||||
|
||||
// 监听加载成功 - 使用canplaythrough事件,确保音频可以完整播放
|
||||
this.audio.addEventListener('canplaythrough', () => {
|
||||
console.log('网络音乐可以完整播放了');
|
||||
this.showSuccessFeedback('网络音乐加载成功!');
|
||||
|
||||
// 检查音频格式和准备状态
|
||||
console.log('音频就绪状态:', this.audio.readyState);
|
||||
console.log('音频格式:', this.audio.canPlayType(this.audio.src));
|
||||
|
||||
// 尝试自动播放
|
||||
this.play();
|
||||
|
||||
}, { once: true });
|
||||
|
||||
// 监听播放事件
|
||||
this.audio.addEventListener('play', () => {
|
||||
console.log('网络音乐开始播放');
|
||||
console.log('播放状态:', this.audio.paused ? '暂停' : '播放中');
|
||||
console.log('当前音量:', this.audio.volume);
|
||||
console.log('静音状态:', this.isMuted);
|
||||
}, { once: true });
|
||||
|
||||
// 监听加载进度
|
||||
this.audio.addEventListener('loadstart', () => {
|
||||
console.log('开始加载音频...');
|
||||
});
|
||||
|
||||
this.audio.addEventListener('progress', () => {
|
||||
if (this.audio.buffered.length > 0) {
|
||||
const buffered = this.audio.buffered.end(0);
|
||||
const duration = this.audio.duration;
|
||||
console.log(`加载进度: ${buffered}/${duration}`);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载网络音乐失败:', error);
|
||||
this.showErrorFeedback('加载网络音乐失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
showLoadingFeedback(message) {
|
||||
const feedback = document.createElement('div');
|
||||
feedback.textContent = message;
|
||||
feedback.className = 'visualization-feedback';
|
||||
feedback.style.background = 'rgba(102, 126, 234, 0.9)';
|
||||
|
||||
document.body.appendChild(feedback);
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedback.parentNode) {
|
||||
feedback.parentNode.removeChild(feedback);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showSuccessFeedback(message) {
|
||||
const feedback = document.createElement('div');
|
||||
feedback.textContent = message;
|
||||
feedback.className = 'visualization-feedback';
|
||||
feedback.style.background = 'rgba(76, 175, 80, 0.9)';
|
||||
|
||||
document.body.appendChild(feedback);
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedback.parentNode) {
|
||||
feedback.parentNode.removeChild(feedback);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showErrorFeedback(message) {
|
||||
const feedback = document.createElement('div');
|
||||
feedback.textContent = message;
|
||||
feedback.className = 'visualization-feedback';
|
||||
feedback.style.background = 'rgba(244, 67, 54, 0.9)';
|
||||
|
||||
document.body.appendChild(feedback);
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedback.parentNode) {
|
||||
feedback.parentNode.removeChild(feedback);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
updateNavigationButtons() {
|
||||
// 网络音乐模式下禁用所有导航按钮
|
||||
if (this.isNetworkMusic) {
|
||||
this.prevBtn.disabled = true;
|
||||
this.nextBtn.disabled = true;
|
||||
this.playbackModeBtn.disabled = true;
|
||||
|
||||
this.prevBtn.style.opacity = '0.3';
|
||||
this.nextBtn.style.opacity = '0.3';
|
||||
this.playbackModeBtn.style.opacity = '0.3';
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件夹模式下的正常处理
|
||||
const hasPlaylist = this.isPlayingFolder && this.playlist.length > 0;
|
||||
this.prevBtn.disabled = !hasPlaylist;
|
||||
this.nextBtn.disabled = !hasPlaylist;
|
||||
@ -677,6 +1313,89 @@ class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
startAudioDiagnostics() {
|
||||
// 启动音频连接状态监控
|
||||
if (this.diagnosticsInterval) {
|
||||
clearInterval(this.diagnosticsInterval);
|
||||
}
|
||||
|
||||
let diagnosticCount = 0;
|
||||
this.diagnosticsInterval = setInterval(() => {
|
||||
diagnosticCount++;
|
||||
|
||||
if (diagnosticCount > 10) { // 最多监控10次
|
||||
clearInterval(this.diagnosticsInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🔍 音频诊断 #${diagnosticCount}:`);
|
||||
console.log(` 播放状态: ${this.audio.paused ? '暂停' : '播放中'}`);
|
||||
console.log(` 当前时间: ${this.audio.currentTime.toFixed(2)}s`);
|
||||
console.log(` 音频时长: ${this.audio.duration ? this.audio.duration.toFixed(2) + 's' : '未知'}`);
|
||||
console.log(` 音量: ${(this.audio.volume * 100).toFixed(0)}%`);
|
||||
console.log(` 音频源连接: ${this.source ? '已连接' : '未连接'}`);
|
||||
console.log(` 音频上下文状态: ${this.audioContext ? this.audioContext.state : '未创建'}`);
|
||||
|
||||
// 检查音频是否正在播放但没有声音
|
||||
if (!this.audio.paused && this.audio.currentTime > 0 && !this.source) {
|
||||
console.warn('⚠️ 警告:音频正在播放但没有音频源连接!');
|
||||
console.log('💡 尝试重新连接音频源...');
|
||||
this.setupAudioContext();
|
||||
}
|
||||
|
||||
// 检查音频上下文是否被暂停
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
console.log('🔧 音频上下文被暂停,尝试恢复...');
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
// 检查可视化数据
|
||||
if (this.analyser && this.dataArray && !this.audio.paused) {
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
const hasData = this.dataArray.some(value => value > 0);
|
||||
if (!hasData && diagnosticCount > 3) {
|
||||
console.warn('⚠️ 无可视化数据,尝试强制重连...');
|
||||
this.forceReconnectAudio();
|
||||
}
|
||||
}
|
||||
}, 2000); // 每2秒检查一次
|
||||
}
|
||||
|
||||
forceReconnectAudio() {
|
||||
console.log('🔄 执行强制音频重连...');
|
||||
|
||||
try {
|
||||
// 1. 重新创建音频上下文(如果需要)
|
||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||
console.log('重新创建音频上下文');
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 2048;
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
// 重新初始化可视化器
|
||||
if (this.waveformVisualizer) {
|
||||
this.waveformVisualizer = new WaveformVisualizer(this.canvas, this.audioContext, this.analyser);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 确保音频上下文运行
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
// 3. 使用新的连接方法
|
||||
console.log('使用新的连接方法');
|
||||
this.connectAudioSource();
|
||||
|
||||
console.log('✅ 强制重连完成');
|
||||
|
||||
} catch (error) {
|
||||
console.error('强制重连失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
togglePlayPause() {
|
||||
if (this.audio.paused) {
|
||||
this.play();
|
||||
|
||||
@ -18,8 +18,8 @@ body {
|
||||
.audio-player {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
width: 100%;
|
||||
@ -49,6 +49,46 @@ body {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
}
|
||||
|
||||
/* 网络音乐信息 */
|
||||
.network-music-info {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: linear-gradient(45deg, #4facfe, #00f2fe);
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.network-music-info p {
|
||||
margin: 5px 0;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.network-music-info p:first-child {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.network-music-info p:last-child {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.file-selector input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -4,7 +4,24 @@ export default defineConfig({
|
||||
// Vite配置选项
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
open: false,
|
||||
host: false,
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Range',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Expose-Headers': 'Content-Length, Content-Range'
|
||||
},
|
||||
proxy: {
|
||||
'/DocServer': {
|
||||
target: 'http://192.168.1.201:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/DocServer/, '/DocServer')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user