Files
AlgerMusicPlayer/src/renderer/services/audioService.ts

785 lines
24 KiB
TypeScript
Raw Normal View History

import { Howl, Howler } from 'howler';
import type { SongResult } from '@/type/music';
import { isElectron } from '@/utils'; // 导入isElectron常量
class AudioService {
private currentSound: Howl | null = null;
private currentTrack: SongResult | null = null;
private context: AudioContext | null = null;
private filters: BiquadFilterNode[] = [];
private source: MediaElementAudioSourceNode | null = null;
private gainNode: GainNode | null = null;
private bypass = false;
private playbackRate = 1.0; // 添加播放速度属性
// 预设的 EQ 频段
private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
// 默认的 EQ 设置
private defaultEQSettings: { [key: string]: number } = {
'31': 0,
'62': 0,
'125': 0,
'250': 0,
'500': 0,
'1000': 0,
'2000': 0,
'4000': 0,
'8000': 0,
'16000': 0
};
private retryCount = 0;
private seekLock = false;
private seekDebounceTimer: NodeJS.Timeout | null = null;
// 添加操作锁防止并发操作
private operationLock = false;
private operationLockTimer: NodeJS.Timeout | null = null;
private operationLockTimeout = 5000; // 5秒超时
private operationLockStartTime: number = 0;
private operationLockId: string = '';
constructor() {
if ('mediaSession' in navigator) {
this.initMediaSession();
}
// 从本地存储加载 EQ 开关状态
const bypassState = localStorage.getItem('eqBypass');
this.bypass = bypassState ? JSON.parse(bypassState) : false;
// 页面加载时立即强制重置操作锁
this.forceResetOperationLock();
// 添加页面卸载事件,确保离开页面时清除锁
window.addEventListener('beforeunload', () => {
this.forceResetOperationLock();
});
}
private initMediaSession() {
navigator.mediaSession.setActionHandler('play', () => {
this.currentSound?.play();
});
navigator.mediaSession.setActionHandler('pause', () => {
this.currentSound?.pause();
});
navigator.mediaSession.setActionHandler('stop', () => {
this.stop();
});
navigator.mediaSession.setActionHandler('seekto', (event) => {
if (event.seekTime && this.currentSound) {
// this.currentSound.seek(event.seekTime);
this.seek(event.seekTime);
}
});
navigator.mediaSession.setActionHandler('seekbackward', (event) => {
if (this.currentSound) {
const currentTime = this.currentSound.seek() as number;
this.seek(currentTime - (event.seekOffset || 10));
}
});
navigator.mediaSession.setActionHandler('seekforward', (event) => {
if (this.currentSound) {
const currentTime = this.currentSound.seek() as number;
this.seek(currentTime + (event.seekOffset || 10));
}
});
navigator.mediaSession.setActionHandler('previoustrack', () => {
// 这里需要通过回调通知外部
this.emit('previoustrack');
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
// 这里需要通过回调通知外部
this.emit('nexttrack');
});
}
private updateMediaSessionMetadata(track: SongResult) {
if (!('mediaSession' in navigator)) return;
const artists = track.ar ? track.ar.map((a) => a.name) : track.song.artists?.map((a) => a.name);
const album = track.al ? track.al.name : track.song.album.name;
const artwork = ['96', '128', '192', '256', '384', '512'].map((size) => ({
src: `${track.picUrl}?param=${size}y${size}`,
type: 'image/jpg',
sizes: `${size}x${size}`
}));
const metadata = {
title: track.name || '',
artist: artists ? artists.join(',') : '',
album: album || '',
artwork
};
navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
}
private updateMediaSessionState(isPlaying: boolean) {
if (!('mediaSession' in navigator)) return;
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
this.updateMediaSessionPositionState();
}
private updateMediaSessionPositionState() {
if (!this.currentSound || !('mediaSession' in navigator)) return;
if ('setPositionState' in navigator.mediaSession) {
navigator.mediaSession.setPositionState({
duration: this.currentSound.duration(),
playbackRate: this.playbackRate,
position: this.currentSound.seek() as number
});
}
}
// 事件处理相关
private callbacks: { [key: string]: Function[] } = {};
private emit(event: string, ...args: any[]) {
const eventCallbacks = this.callbacks[event];
if (eventCallbacks) {
eventCallbacks.forEach((callback) => callback(...args));
}
}
on(event: string, callback: Function) {
if (!this.callbacks[event]) {
this.callbacks[event] = [];
}
this.callbacks[event].push(callback);
}
off(event: string, callback: Function) {
const eventCallbacks = this.callbacks[event];
if (eventCallbacks) {
this.callbacks[event] = eventCallbacks.filter((cb) => cb !== callback);
}
}
// EQ 相关方法
public isEQEnabled(): boolean {
return !this.bypass;
}
public setEQEnabled(enabled: boolean) {
this.bypass = !enabled;
localStorage.setItem('eqBypass', JSON.stringify(this.bypass));
if (this.source && this.gainNode && this.context) {
this.applyBypassState();
}
}
public setEQFrequencyGain(frequency: string, gain: number) {
const filterIndex = this.frequencies.findIndex((f) => f.toString() === frequency);
if (filterIndex !== -1 && this.filters[filterIndex]) {
this.filters[filterIndex].gain.setValueAtTime(gain, this.context?.currentTime || 0);
this.saveEQSettings(frequency, gain);
}
}
public resetEQ() {
this.filters.forEach((filter) => {
filter.gain.setValueAtTime(0, this.context?.currentTime || 0);
});
localStorage.removeItem('eqSettings');
}
public getAllEQSettings(): { [key: string]: number } {
return this.loadEQSettings();
}
private saveEQSettings(frequency: string, gain: number) {
const settings = this.loadEQSettings();
settings[frequency] = gain;
localStorage.setItem('eqSettings', JSON.stringify(settings));
}
private loadEQSettings(): { [key: string]: number } {
const savedSettings = localStorage.getItem('eqSettings');
return savedSettings ? JSON.parse(savedSettings) : { ...this.defaultEQSettings };
}
private async disposeEQ(keepContext = false) {
try {
// 清理音频节点连接
if (this.source) {
this.source.disconnect();
this.source = null;
}
// 清理滤波器
this.filters.forEach((filter) => {
try {
filter.disconnect();
} catch (e) {
console.warn('清理滤波器时出错:', e);
}
});
this.filters = [];
// 清理增益节点
if (this.gainNode) {
this.gainNode.disconnect();
this.gainNode = null;
}
// 如果不需要保持上下文,则关闭它
if (!keepContext && this.context) {
try {
await this.context.close();
this.context = null;
} catch (e) {
console.warn('关闭音频上下文时出错:', e);
}
}
} catch (error) {
console.error('清理EQ资源时出错:', error);
}
}
private async setupEQ(sound: Howl) {
try {
if (!isElectron) {
console.log('Web环境中跳过EQ设置避免CORS问题');
this.bypass = true;
return;
}
const howl = sound as any;
// eslint-disable-next-line no-underscore-dangle
const audioNode = howl._sounds?.[0]?._node;
if (!audioNode || !(audioNode instanceof HTMLMediaElement)) {
if (this.retryCount < 3) {
console.warn('等待音频节点初始化,重试次数:', this.retryCount + 1);
await new Promise((resolve) => setTimeout(resolve, 100));
this.retryCount++;
return await this.setupEQ(sound);
}
throw new Error('无法获取音频节点,请重试');
}
this.retryCount = 0;
// 确保使用 Howler 的音频上下文
this.context = Howler.ctx as AudioContext;
if (!this.context || this.context.state === 'closed') {
Howler.ctx = new AudioContext();
this.context = Howler.ctx;
Howler.masterGain = this.context.createGain();
Howler.masterGain.connect(this.context.destination);
}
if (this.context.state === 'suspended') {
await this.context.resume();
}
// 清理现有连接
await this.disposeEQ(true);
try {
// 检查节点是否已经有源
const existingSource = (audioNode as any).source as MediaElementAudioSourceNode;
if (existingSource?.context === this.context) {
console.log('复用现有音频源节点');
this.source = existingSource;
} else {
// 创建新的源节点
console.log('创建新的音频源节点');
this.source = this.context.createMediaElementSource(audioNode);
(audioNode as any).source = this.source;
}
} catch (e) {
console.error('创建音频源节点失败:', e);
throw e;
}
// 创建增益节点
this.gainNode = this.context.createGain();
// 创建滤波器
this.filters = this.frequencies.map((freq) => {
const filter = this.context!.createBiquadFilter();
filter.type = 'peaking';
filter.frequency.value = freq;
filter.Q.value = 1;
filter.gain.value = this.loadEQSettings()[freq.toString()] || 0;
return filter;
});
// 应用EQ状态
this.applyBypassState();
// 设置音量
const volume = localStorage.getItem('volume');
if (this.gainNode) {
this.gainNode.gain.value = volume ? parseFloat(volume) : 1;
}
console.log('EQ初始化成功');
} catch (error) {
console.error('EQ初始化失败:', error);
await this.disposeEQ();
throw error;
}
}
private applyBypassState() {
if (!this.source || !this.gainNode || !this.context) return;
try {
// 断开所有现有连接
this.source.disconnect();
this.filters.forEach((filter) => filter.disconnect());
this.gainNode.disconnect();
if (this.bypass) {
// EQ被禁用时直接连接到输出
this.source.connect(this.gainNode);
this.gainNode.connect(this.context.destination);
} else {
// EQ启用时通过滤波器链连接
this.source.connect(this.filters[0]);
this.filters.forEach((filter, index) => {
if (index < this.filters.length - 1) {
filter.connect(this.filters[index + 1]);
}
});
this.filters[this.filters.length - 1].connect(this.gainNode);
this.gainNode.connect(this.context.destination);
}
} catch (error) {
console.error('应用EQ状态时出错:', error);
}
}
// 设置操作锁,带超时自动释放
private setOperationLock(): boolean {
// 生成唯一的锁ID
const lockId = Date.now().toString() + Math.random().toString(36).substring(2, 9);
// 如果锁已经存在,检查是否超时
if (this.operationLock) {
const currentTime = Date.now();
const lockDuration = currentTime - this.operationLockStartTime;
// 如果锁持续时间超过2秒直接强制重置
if (lockDuration > 2000) {
console.warn(`操作锁已激活 ${lockDuration}ms超过安全阈值强制重置`);
this.forceResetOperationLock();
} else {
console.log(`操作锁激活中,持续时间 ${lockDuration}ms`);
return false;
}
}
this.operationLock = true;
this.operationLockStartTime = Date.now();
this.operationLockId = lockId;
// 将锁信息存储到 localStorage仅用于调试实际不依赖此值
try {
localStorage.setItem('audioOperationLock', JSON.stringify({
id: this.operationLockId,
startTime: this.operationLockStartTime
}));
} catch (error) {
console.error('存储操作锁信息失败:', error);
}
// 清除之前的定时器
if (this.operationLockTimer) {
clearTimeout(this.operationLockTimer);
}
// 设置超时自动释放锁
this.operationLockTimer = setTimeout(() => {
console.warn('操作锁超时自动释放');
this.releaseOperationLock();
}, this.operationLockTimeout);
return true;
}
// 释放操作锁
public releaseOperationLock(): void {
this.operationLock = false;
this.operationLockStartTime = 0;
// 从 localStorage 中移除锁信息
try {
localStorage.removeItem('audioOperationLock');
} catch (error) {
console.error('清除存储的操作锁信息失败:', error);
}
if (this.operationLockTimer) {
clearTimeout(this.operationLockTimer);
this.operationLockTimer = null;
}
}
// 强制重置操作锁,用于特殊情况
public forceResetOperationLock(): void {
console.log('强制重置操作锁');
this.operationLock = false;
this.operationLockStartTime = 0;
this.operationLockId = '';
if (this.operationLockTimer) {
clearTimeout(this.operationLockTimer);
this.operationLockTimer = null;
}
// 清除存储的锁
localStorage.removeItem('audioOperationLock');
}
// 播放控制相关
play(url?: string, track?: SongResult, isPlay: boolean = true): Promise<Howl> {
// 每次调用play方法时尝试强制重置锁注意仅在页面刷新后的第一次播放时应用
if (!this.currentSound) {
console.log('首次播放请求,强制重置操作锁');
this.forceResetOperationLock();
}
// 如果操作锁已激活,但持续时间超过安全阈值,强制重置
if (this.operationLock) {
const currentTime = Date.now();
const lockDuration = currentTime - this.operationLockStartTime;
if (lockDuration > 2000) {
console.warn(`操作锁已激活 ${lockDuration}ms超过安全阈值强制重置`);
this.forceResetOperationLock();
}
}
// 获取锁
if (!this.setOperationLock()) {
console.log('audioService: 操作锁激活,强制执行当前播放请求');
// 如果只是要继续播放当前音频,直接执行
if (this.currentSound && !url && !track) {
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.play();
return Promise.resolve(this.currentSound);
}
// 强制释放锁并继续执行
this.forceResetOperationLock();
// 这里不再返回错误,而是继续执行播放逻辑
}
// 如果没有提供新的 URL 和 track且当前有音频实例则继续播放
2025-01-17 22:34:49 +08:00
if (this.currentSound && !url && !track) {
// 如果有进行中的seek操作等待其完成
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
2025-01-17 22:34:49 +08:00
this.currentSound.play();
this.releaseOperationLock();
return Promise.resolve(this.currentSound);
2025-01-17 22:34:49 +08:00
}
// 如果没有提供必要的参数,返回错误
if (!url || !track) {
this.releaseOperationLock();
return Promise.reject(new Error('缺少必要参数: url和track'));
}
return new Promise<Howl>((resolve, reject) => {
let retryCount = 0;
const maxRetries = 1;
const tryPlay = async () => {
try {
console.log('audioService: 开始创建音频对象');
// 确保 Howler 上下文已初始化
if (!Howler.ctx) {
console.log('audioService: 初始化 Howler 上下文');
Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
}
// 确保使用同一个音频上下文
if (Howler.ctx.state === 'closed') {
console.log('audioService: 重新创建音频上下文');
Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
this.context = Howler.ctx;
Howler.masterGain = this.context.createGain();
Howler.masterGain.connect(this.context.destination);
}
// 恢复上下文状态
if (Howler.ctx.state === 'suspended') {
console.log('audioService: 恢复暂停的音频上下文');
await Howler.ctx.resume();
}
// 先停止并清理现有的音频实例
if (this.currentSound) {
console.log('audioService: 停止并清理现有的音频实例');
// 确保任何进行中的seek操作被取消
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.stop();
this.currentSound.unload();
this.currentSound = null;
}
// 清理 EQ 但保持上下文
console.log('audioService: 清理 EQ');
await this.disposeEQ(true);
this.currentTrack = track;
console.log('audioService: 创建新的 Howl 对象');
this.currentSound = new Howl({
src: [url],
html5: true,
autoplay: false, // 修改为 false不自动播放等待完全初始化后手动播放
volume: localStorage.getItem('volume')
? parseFloat(localStorage.getItem('volume') as string)
: 1,
rate: this.playbackRate, // 设置初始播放速度
format: ['mp3', 'aac'],
2025-01-22 23:43:17 +08:00
onloaderror: (_, error) => {
console.error('Audio load error:', error);
if (retryCount < maxRetries) {
retryCount++;
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
setTimeout(tryPlay, 1000 * retryCount);
} else {
// 发送URL过期事件通知外部需要重新获取URL
this.emit('url_expired', this.currentTrack);
this.releaseOperationLock();
reject(new Error('音频加载失败,请尝试切换其他歌曲'));
}
},
2025-01-22 23:43:17 +08:00
onplayerror: (_, error) => {
console.error('Audio play error:', error);
if (retryCount < maxRetries) {
retryCount++;
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
setTimeout(tryPlay, 1000 * retryCount);
} else {
// 发送URL过期事件通知外部需要重新获取URL
this.emit('url_expired', this.currentTrack);
this.releaseOperationLock();
reject(new Error('音频播放失败,请尝试切换其他歌曲'));
}
},
onload: async () => {
// 音频加载成功后设置 EQ 和更新媒体会话
if (this.currentSound) {
try {
console.log('audioService: 音频加载成功,设置 EQ');
await this.setupEQ(this.currentSound);
this.updateMediaSessionMetadata(track);
this.updateMediaSessionPositionState();
this.emit('load');
// 此时音频已完全初始化,根据 isPlay 参数决定是否播放
console.log('audioService: 音频完全初始化isPlay =', isPlay);
if (isPlay) {
console.log('audioService: 开始播放');
this.currentSound.play();
}
resolve(this.currentSound);
} catch (error) {
console.error('设置 EQ 失败:', error);
// 即使 EQ 设置失败,也继续播放(如果需要)
if (isPlay) {
this.currentSound.play();
}
resolve(this.currentSound);
}
}
}
});
// 设置音频事件监听
if (this.currentSound) {
this.currentSound.on('play', () => {
this.updateMediaSessionState(true);
this.emit('play');
});
this.currentSound.on('pause', () => {
this.updateMediaSessionState(false);
this.emit('pause');
});
this.currentSound.on('end', () => {
this.emit('end');
});
this.currentSound.on('seek', () => {
this.updateMediaSessionPositionState();
this.emit('seek');
});
}
} catch (error) {
console.error('Error creating audio instance:', error);
this.releaseOperationLock();
reject(error);
}
};
tryPlay();
}).finally(() => {
// 无论成功或失败都解除操作锁
this.releaseOperationLock();
});
}
getCurrentSound() {
return this.currentSound;
}
getCurrentTrack() {
return this.currentTrack;
}
stop() {
// 强制重置操作锁并继续执行
this.forceResetOperationLock();
try {
if (this.currentSound) {
try {
// 确保任何进行中的seek操作被取消
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.stop();
this.currentSound.unload();
} catch (error) {
console.error('停止音频失败:', error);
}
this.currentSound = null;
}
this.currentTrack = null;
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = 'none';
}
this.disposeEQ();
} catch (error) {
console.error('停止音频时发生错误:', error);
}
}
setVolume(volume: number) {
if (this.currentSound) {
this.currentSound.volume(volume);
localStorage.setItem('volume', volume.toString());
}
}
seek(time: number) {
// 直接强制重置操作锁
this.forceResetOperationLock();
if (this.currentSound) {
try {
// 直接执行seek操作
this.currentSound.seek(time);
// 触发seek事件
this.updateMediaSessionPositionState();
this.emit('seek', time);
} catch (error) {
console.error('Seek操作失败:', error);
}
}
}
2025-01-17 22:34:49 +08:00
pause() {
this.forceResetOperationLock();
2025-01-17 22:34:49 +08:00
if (this.currentSound) {
try {
// 确保任何进行中的seek操作被取消
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.pause();
} catch (error) {
console.error('暂停音频失败:', error);
}
2025-01-17 22:34:49 +08:00
}
}
clearAllListeners() {
this.callbacks = {};
}
public getCurrentPreset(): string | null {
return localStorage.getItem('currentPreset');
}
public setCurrentPreset(preset: string): void {
localStorage.setItem('currentPreset', preset);
}
public setPlaybackRate(rate: number) {
if (!this.currentSound) return;
this.playbackRate = rate;
// Howler 的 rate() 在 html5 模式下不生效
this.currentSound.rate(rate);
// 取出底层 HTMLAudioElement改原生 playbackRate
const sounds = (this.currentSound as any)._sounds as any[];
sounds.forEach(({ _node }) => {
if (_node instanceof HTMLAudioElement) {
_node.playbackRate = rate;
}
});
// 同步给 Media Session UI
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
navigator.mediaSession.setPositionState({
duration: this.currentSound.duration(),
playbackRate: rate,
position: this.currentSound.seek() as number
});
}
}
public getPlaybackRate(): number {
return this.playbackRate;
}
}
export const audioService = new AudioService();