H5全局播放器实现

102 阅读9分钟

H5全局全功能播放器实现 此播放器功能支持播放区间,支持淡入淡出、播放控制, seek控制、支持个性化配置。

H5Audio方法介绍

// HTML <audio> 元素支持一系列事件,这些事件可以帮助您管理音频的播放、暂停、加载等状态
// onloadstart: 当浏览器开始加载音频时触发。
// onloadedmetadata: 当浏览器已加载音频的元数据时触发。
// onloadeddata: 当浏览器已加载音频的全部数据时触发。
// oncanplay: 当浏览器可以开始播放音频时触发。
// oncanplaythrough: 当浏览器预计可以在不停顿的情况下播放音频时触发。
// onplay: 当音频开始播放时触发。
// onplaying: 当音频正在播放时触发。
// onpause: 当音频暂停时触发。
// onended: 当音频播放结束时触发。
// onerror: 当音频加载出错时触发。
// onprogress: 当音频正在下载时触发,以便显示下载进度。
// ontimeupdate: 当音频播放位置发生变化时触发,以便更新播放进度条等。


// HTML <audio> 元素本身没有方法,但可以通过JavaScript来操作它。以下是一些常用的通过 JavaScript 操作 <audio> 元素的方法:

// play(): 开始播放音频。
// pause(): 暂停音频播放。
// load(): 重新加载音频。
// canPlayType(type): 返回一个字符串,指示浏览器是否能够播放指定类型的音频文件。
// currentTime: 属性,用于获取或设置音频的当前播放位置。
// volume: 属性,用于获取或设置音频的音量。
// muted: 属性,用于获取或设置音频是否静音。
// duration: 属性,返回音频的总时长。
// seekable: 属性,返回一个 TimeRanges 对象,表示音频可寻址的时间范围。
// ended: 属性,返回一个布尔值,指示音频是否已经播放结束。

实现

 /*
 * @author: Ronin lee
 * @LastEditTime: 2024-05-12 18:00:48
 * @Description: 全局播放器单例
 * @FilePath:
 */

// HTML <audio> 元素支持一系列事件,这些事件可以帮助您管理音频的播放、暂停、加载等状态
// onloadstart: 当浏览器开始加载音频时触发。
// onloadedmetadata: 当浏览器已加载音频的元数据时触发。
// onloadeddata: 当浏览器已加载音频的全部数据时触发。
// oncanplay: 当浏览器可以开始播放音频时触发。
// oncanplaythrough: 当浏览器预计可以在不停顿的情况下播放音频时触发。
// onplay: 当音频开始播放时触发。
// onplaying: 当音频正在播放时触发。
// onpause: 当音频暂停时触发。
// onended: 当音频播放结束时触发。
// onerror: 当音频加载出错时触发。
// onprogress: 当音频正在下载时触发,以便显示下载进度。
// ontimeupdate: 当音频播放位置发生变化时触发,以便更新播放进度条等。

// HTML <audio> 元素本身没有方法,但可以通过JavaScript来操作它。以下是一些常用的通过 JavaScript 操作 <audio> 元素的方法:

// play(): 开始播放音频。
// pause(): 暂停音频播放。
// load(): 重新加载音频。
// canPlayType(type): 返回一个字符串,指示浏览器是否能够播放指定类型的音频文件。
// currentTime: 属性,用于获取或设置音频的当前播放位置。
// volume: 属性,用于获取或设置音频的音量。
// muted: 属性,用于获取或设置音频是否静音。
// duration: 属性,返回音频的总时长。
// seekable: 属性,返回一个 TimeRanges 对象,表示音频可寻址的时间范围。
// ended: 属性,返回一个布尔值,指示音频是否已经播放结束。
const globleConfig = {
  volume: 1,
  analyserFftSize: 2048,
  mapType: "url", // 当存在重复url时推荐给url添加模块后缀的方式避免渲染重复
};
export const defaultPersonalizedConfig = {
  endedToStart: true, // 播放结束后是否回归零位置
  loop: true, // 是否循环播放
  fadeInTime: 0, // 淡入时间
  fadeOutTime: 0, // 淡出时间
  volumeGain: 0, // 音量偏移,当前歌曲音量降低或增加0-1,音量最大为1
  analyser: false, // 创建频谱直方图
  canSeekNotCurrent: false, // 当不是当前播放时是否可以调整下次播放位置
  autoToStartIfNotCurrent: true, // 当不是当前时是否自动归零
  isGenerating: false, // 是否正在生成音频
};

export class GlobalAudioPlayer {
  constructor() {
    if (GlobalAudioPlayer.instance) {
      return GlobalAudioPlayer.instance;
    }

    this.initAudio();

    GlobalAudioPlayer.instance = this;
  }

  setGlobalPersonalizedConfig(config) {
    Object.assign(defaultPersonalizedConfig, config);
  }

  getAudioDurationFromStreamWithUpdates(audioUrl, updateCallback) {
    return new Promise((resolve, reject) => {
      const audioContext = new (window.AudioContext ||
        window.webkitAudioContext)();
      const audioData = []; // 用来存储音频数据
      let lastUpdateTime = 0; // 记录上次更新的时刻

      fetch(audioUrl)
        .then((response) => {
          const reader = response.body.getReader();

          function readStream() {
            reader
              .read()
              .then(({ done, value }) => {
                if (done) {
                  // 流读取完毕后,解码音频数据
                  const audioBuffer = new Uint8Array(audioData).buffer;
                  audioContext
                    .decodeAudioData(audioBuffer)
                    .then((decodedData) => {
                      resolve(decodedData.duration); // 返回音频的总时长
                    })
                    .catch((error) => {
                      reject("音频解码失败: " + error);
                    });
                  return;
                }

                // 将当前数据块保存到 audioData 数组中
                audioData.push(...value);

                // 每读取一定数量的音频数据后,调用回调更新时长
                if (
                  updateCallback &&
                  audioContext.currentTime - lastUpdateTime >= 1
                ) {
                  lastUpdateTime = audioContext.currentTime;
                  // 计算并触发时长更新
                  updateCallback(audioContext.currentTime);
                }

                // 继续读取流
                readStream();
              })
              .catch((error) => {
                reject("读取音频流失败: " + error);
              });
          }

          // 开始读取音频流
          readStream();
        })
        .catch((error) => {
          reject("请求音频文件失败: " + error);
        });
    });
  }

  initAudio() {
    this.audio = new Audio();
    this.globleAudioAnalyser = null;
    this.players = {}; // 存储每个URL对应的播放数据
    this.lastUrl = null; // 记录上一个播放的URL
    this.currentUrl = null; // 记录当前正在播放的URL
    this.currentPlayer = null; // 记录当前播放器
    this.changeUrlListener = [];
    this.globalListenerMap = {
      playStateChange: [],
      curPlayEnded: [],
      curPlayUrlChange: [],
    };
    this.audio.crossOrigin = "anonymous";
    this.checkNetworkStatus();
    this.audio.addEventListener("loadstart", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      // console.log('loadstart', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onloadstart",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        true
      );
    });
    this.audio.addEventListener("loadedmetadata", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      // console.log('🍑🍑🍑音频持续时间:', this.audio.duration); // 音频时长(秒)
      // console.log('🍑🍑🍑音频采样率:', this.audio.sampleRate); // 采样率(如果支持)
      // console.log('🍑🍑🍑音频格式:', this.audio.src); // 文件路径
      // const metadata = this.audio.mediaMetadata;
      // if (metadata) {
      //   console.log('🍑🍑🍑音频标题:', metadata.title);
      //   console.log('🍑🍑🍑艺术家:', metadata.artist);
      //   console.log('🍑🍑🍑专辑:', metadata.album);
      // } else {
      //   console.log('没有找到音频的元数据');
      // }
      // console.log('loadedmetadata', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onloadedmetadata",
        ...args
      );
    });
    this.audio.addEventListener("loadeddata", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      // console.log('loadeddata', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onloadeddata",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        false
      );
    });
    this.audio.addEventListener("canplay", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      // console.log('canplay', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "oncanplay",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        false
      );
    });
    this.audio.addEventListener("canplaythrough", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      if (!this.currentPlayer.isPlaying) {
        this.pause(this.currentUrl);
      }
      // console.log('canplaythrough', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "oncanplaythrough",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        false
      );
    });

    this.audio.addEventListener("play", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      // console.log('play', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onplay",
        ...args
      );
      // this.triggerListenerBroadcast(this.players[this.currentUrl], 'onPlayStateChange', true)
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        false
      );
    });
    this.audio.addEventListener("playing", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      this.currentPlayer.isPlaying = true;
      // console.log('playing', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "oncanplaythrough",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onPlayStateChange",
        true
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        false
      );
    });
    this.audio.addEventListener("pause", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      this.currentPlayer.isPlaying = false;
      // console.log('pause', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onpause",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onPlayStateChange",
        false
      );
    });
    this.audio.addEventListener("durationchange", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      console.log('🍎🍎🍎🍎durationchange', ...args);
      if (this.audio.duration != this.players[this.currentUrl].duration && this.audio.duration != Infinity) {
        this.players[this.currentUrl].duration = this.audio.duration;
        this.triggerListenerBroadcast(
          this.players[this.currentUrl],
          "onDurationChange",
          this.getDuration(this.players[this.currentUrl]),
        );
      } else if (this.audio.duration == Infinity) {
        // this.getAudioDurationFromStreamWithUpdates('path/to/audio/file.mp3', (currentTime) => {
        //   console.log('🍑🍑🍑当前音频时长:', currentTime.toFixed(2), '秒');
        // })
        // .then(totalDuration => {
        //   console.log('🍑🍑🍑音频总时长:', totalDuration, '秒');
        // })
        // .catch(error => {
        //   console.error('🍑🍑🍑错误:', error);
        // });
        console.log("🍑🍑🍑错误:音频时长为Infinity");
      } else {
        console.log("🍑🍑🍑错误:音频时长为Infinity");
      }
    })
    this.audio.addEventListener("ended", (...args) => {
      if (!this.currentUrl) {
        return;
      }

      // console.log('ended', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onended",
        ...args
      );
      const { endedToStart, loop } =
        this.players[this.currentUrl].personalizedConfig;

      this.triggerGlobleListenerBroadcast("curPlayEnded", {
        currentUrl: this.currentUrl,
        player: this.players[this.currentUrl],
      });
      if (loop) {
        this.audio.play();
      } else {
        this.audio.pause();
      }
      if (endedToStart) {
        this.audio.currentTime = 0;
      }
    });
    this.audio.addEventListener("error", (...args) => {
      // console.error('addEventListener__error', ...args);
      if (!this.currentUrl) {
        return;
      }
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onerror",
        ...args
      );
    });
    this.audio.addEventListener("progress", (...args) => {
      if (!this.currentUrl) {
        return;
      }
      // console.log('progress', ...args);
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onprogress"
      );
      if (this.audio.readyState >= 2) {
        if (
          this.audio.buffered.end(0) / this.audio.duration ==
          this.audio.currentTime / this.audio.duration
        ) {
          this.triggerListenerBroadcast(
            this.players[this.currentUrl],
            "onLoadingStateChange",
            true
          );
        }
      }
    });
    this.audio.addEventListener("timeupdate", (...args) => {
      // console.log('timeupdate', ...args);
      if (!this.currentUrl) {
        return;
      }
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "ontimeupdate",
        ...args
      );
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onLoadingStateChange",
        false
      );

      if (this.audio.duration == Infinity) {
        if (this.players[this.currentUrl].duration < 60) {
          this.players[this.currentUrl].duration = 60;
        }
        if (this.players[this.currentUrl].duration < this.audio.currentTime + 10) {
          this.players[this.currentUrl].duration = this.audio.currentTime + 30;
        }
        this.triggerListenerBroadcast(
          this.players[this.currentUrl],
          "onDurationChange",
          this.getDuration(this.players[this.currentUrl])
        );
      }

      const { endedToStart, loop, fadeInTime, fadeOutTime, volumeGain } =
        this.players[this.currentUrl].personalizedConfig;
      let { clipStart, clipEnd } = this.checkClipTime(
        this.players[this.currentUrl]
      );
      if (clipStart >= 0 && clipEnd > clipStart) {
        let currentTime = this.audio.currentTime;
        if (currentTime < clipStart || currentTime >= clipEnd) {
          if (currentTime < clipStart) {
            this.audio.currentTime = clipStart;
          } else {
            this.triggerGlobleListenerBroadcast("curPlayEnded", {
              currentUrl: this.currentUrl,
              player: this.players[this.currentUrl],
            });
            if (loop) {
              this.audio.play();
            } else {
              this.audio.pause();
            }
            if (endedToStart) {
              this.audio.currentTime = clipStart;
            } else {
              this.audio.currentTime = clipEnd;
            }
          }
        }
      } else {
        clipEnd = this.audio.duration;
      }
      let volume = globleConfig.volume + volumeGain;
      if (volume < 0) {
        volume = 0;
      } else if (volume > 1) {
        volume = 1;
      }
      if (fadeInTime > 0 && this.audio.currentTime <= clipStart + fadeInTime) {
        this.audio.volume =
          ((this.audio.currentTime - clipStart) / fadeInTime) * volume;
      } else if (
        fadeOutTime > 0 &&
        this.audio.currentTime >= clipEnd - fadeOutTime
      ) {
        this.audio.volume =
          ((clipEnd - this.audio.currentTime) / fadeOutTime) * volume;
      } else {
        this.audio.volume = volume;
      }
      this.players[this.currentUrl].currentTime = this.audio.currentTime;
      this.triggerListenerBroadcast(
        this.players[this.currentUrl],
        "onPositionChange",
        this.getPositionData(this.players[this.currentUrl])
      );
      if (this.players[this.currentUrl].personalizedConfig.analyser) {
        this.triggerListenerBroadcast(
          this.players[this.currentUrl],
          "onAudioAnalyserChange",
          this.getAnalyserData(this.players[this.currentUrl])
        );
      }
      if (!this.currentPlayer.isPlaying) {
        this.pause(this.currentUrl);
      }
    });
  }

  getUuid() {
    return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
      (
        c ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
      ).toString(16)
    );
  }

  isAudioReadableStreamSupported() {
    // 检查 ReadableStream 是否存在
    const supportsReadableStream = typeof ReadableStream !== 'undefined';
  
    // 检查 Fetch API 和 response.body 是否支持 ReadableStream
    const supportsFetchStream = (() => {
      if (!supportsReadableStream || !self.fetch) return false;
      try {
        const testResponse = new Response(new ReadableStream());
        return testResponse.body instanceof ReadableStream;
      } catch (e) {
        return false;
      }
    })();
  
    // 检查 AudioContext 是否可用(音频处理相关)
    const supportsAudioContext = typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined';
  
    return supportsReadableStream && supportsFetchStream && supportsAudioContext;
  }


  splitArray(arr, count) {
    const size = Math.ceil(arr.length ?? 0) / count;
    return arr.length
      ? arr.reduce(
        (res, cur) => (
          res[res.length - 1].length < size
            ? res[res.length - 1].push(cur)
            : res.push([cur]),
          res
        ),
        [[]]
      )
      : [];
  }

  formatTime(secs) {
    if (isNaN(Number(secs))) {
      return "00:00";
    }
    secs = Math.ceil(secs);
    const minutes = Math.floor(secs / 60) || 0;
    const seconds = Math.floor(secs - minutes * 60) || 0;
    return `${String(minutes).padStart(2, 0)}:${String(seconds).padStart(
      2,
      0
    )}`;
  }

  canPlayType(type) {
    return this.audio.canPlayType(type);
  }

  setVolume(volume) {
    globleConfig.volume = volume;
    this.audio.volume = volume;
  }

  checkClipTime(player) {
    let { clipStart, clipEnd, duration } = player;
    clipStart = clipStart > 0 ? (clipStart > duration ? 0 : clipStart) : 0;
    clipEnd = clipEnd > 0 ? (clipEnd > duration ? duration : clipEnd) : 0;
    return {
      clipStart,
      clipEnd,
    };
  }

  getDuration(player) {
    const { clipStart, clipEnd } = this.checkClipTime(player);
    const { duration } = player;
    if (clipStart >= 0 && clipEnd > clipStart) {
      return {
        duration: clipEnd - clipStart,
        durationTime: this.formatTime(clipEnd - clipStart),
        sourceDuration: duration,
        sourceDurationTime: this.formatTime(duration),
      };
    } else {
      return {
        duration: duration,
        durationTime: this.formatTime(duration),
        sourceDuration: duration,
        sourceDurationTime: this.formatTime(duration),
      };
    }
  }

  getPositionData(player) {
    const { clipStart, clipEnd } = this.checkClipTime(player);
    const { currentTime, duration } = player;
    if (clipStart >= 0 && clipEnd > clipStart) {
      return {
        start: 0,
        startTime: this.formatTime(0),
        end: clipEnd - clipStart,
        endTime: this.formatTime(clipEnd - clipStart),
        current: currentTime - clipStart,
        currentTime: this.formatTime(currentTime - clipStart),
        progress: (currentTime - clipStart) / (clipEnd - clipStart),

        sourceStart: 0,
        sourceStartTime: this.formatTime(0),
        sourceEnd: duration,
        sourceEndTime: this.formatTime(duration),
        sourceCurrent: currentTime,
        sourceCurrentTime: this.formatTime(currentTime),
        sourceProgress: currentTime / duration,
      };
    } else {
      return {
        start: 0,
        startTime: this.formatTime(0),
        end: duration,
        endTime: this.formatTime(duration),
        current: currentTime,
        currentTime: this.formatTime(currentTime),
        progress: currentTime / duration,

        sourceStart: 0,
        sourceStartTime: this.formatTime(0),
        sourceEnd: duration,
        sourceEndTime: this.formatTime(duration),
        sourceCurrent: currentTime,
        sourceCurrent: this.formatTime(currentTime),
        sourceProgress: currentTime / duration,
      };
    }
  }

  registGlobleListener(eventName, listener) {
    const listeners = this.globalListenerMap[eventName];
    if (listeners && !listeners.includes(listener)) {
      listeners.push(listener);
    }
  }
  releaseGlobleListener(eventName, listener) {
    const listeners = this.globalListenerMap[eventName];
    const index = listeners?.indexOf(listener);
    if (index > -1) {
      listeners.splice(index, 1);
    }
  }

  triggerGlobleListenerBroadcast(eventName, ...args) {
    const listeners = this.globalListenerMap[eventName];
    if (listeners) {
      listeners.forEach((listener) => {
        listener(...args);
      });
    }
  }

  triggerListenerBroadcast(player, listenerName, ...args) {
    player &&
      player.listeners.forEach((listener) => {
        listener[listenerName] && listener[listenerName](...args);
      });
    if (listenerName == "onPlayStateChange") {
      this.triggerGlobleListenerBroadcast("playStateChange", {
        isPlaying: args[0],
        lastUrl: this.lastUrl,
        player: player,
        currentUrl: this.currentUrl,
      });
    }
  }

  setupAudioContext = () => {
    try {
      if (typeof AudioContext !== "undefined") {
        return new AudioContext();
      } else if (typeof webkitAudioContext !== "undefined") {
        return new webkitAudioContext();
      } else {
        return null;
      }
    } catch (e) {
      return null;
    }
  };

  openAudioAnalyser = (player) => {
    let isAnalyser = player.personalizedConfig?.analyser;
    if (isAnalyser) {
      if (!this.globleAudioAnalyser) {
        let audioContext = this.setupAudioContext();
        try {
          audioContext?.resume().then(() => {
            let analyser = audioContext.createAnalyser();
            analyser.fftSize = globleConfig.analyserFftSize;
            let analyserMediaElementSource =
              audioContext.createMediaElementSource(this.audio);
            analyserMediaElementSource.connect(analyser);
            analyserMediaElementSource.connect(audioContext.destination);
            this.globleAudioAnalyser = {
              audioContext,
              analyser,
              analyserMediaElementSource,
            };
            player.audioAnalyser = {
              ...player.audioAnalyser,
              ...this.globleAudioAnalyser,
              active: true,
            };
          });
        } catch (e) {
          console.log("initAudioAnalyser__error", e);
        }
      } else {
        // this.globleAudioAnalyser.audioContext.resume()
        player.audioAnalyser = {
          ...player.audioAnalyser,
          ...this.globleAudioAnalyser,
          active: true,
        };
      }
    }
  };

  closeAudioAnalyser = async (player, isDestroy = false) => {
    let isAnalyser = player.personalizedConfig?.analyser;
    let { audioContext, analyser, analyserMediaElementSource } =
      player.audioAnalyser;
    if (isAnalyser && audioContext) {
      player.audioAnalyser.active = false;
      // this.globleAudioAnalyser.audioContext.suspend();
      if (!isDestroy) {
        this.triggerListenerBroadcast(
          player,
          "onAudioAnalyserChange",
          this.getAnalyserData(player, true)
        );
      }
    }
  };

  getAnalyserData(player) {
    let isAnalyser = player.personalizedConfig?.analyser;
    if (!isAnalyser) {
      return null;
    }
    let { audioContext, analyser, analyserMediaElementSource, active } =
      player.audioAnalyser;
    if (this.globleAudioAnalyser && active) {
      return {
        player,
        getAnalyserData: (count, averageCount = 2) => {
          let length =
            ((analyser.frequencyBinCount * 44100) / audioContext.sampleRate) |
            0;
          let arr = new Uint8Array(length);
          analyser.getByteFrequencyData(arr);
          let l0 = 0;
          let r0 = 0;
          for (let i = 0; i < arr.length; i++) {
            if (arr[i] > 0 && l0 == 0) {
              l0 = i;
            }
            if (arr[length - i - 1] > 0 && r0 == 0) {
              r0 = length - i - 1;
            }
            if (l0 != 0 && r0 != 0) {
              break;
            }
          }
          arr = arr.slice(l0, r0);
          return this.splitArray(arr, count).map((chuck, index) => {
            const step = Math.floor((chuck.length ?? 0) / averageCount);
            let sum = 0;
            for (let i = 0; i < chuck.length; i += step) {
              sum += Math.abs(chuck[i]);
            }
            const average = sum / (chuck.length / step);
            return Math.round(average);
          });
        },
      };
    } else {
      return {
        player,
        getAnalyserData: (count) => {
          return new Uint8Array(count);
        },
      };
    }
  }

  getOrCreatePlayer(url, options) {
    if (!options && this.players[url]) {
      return {
        url,
        player: this.players[url],
        id: this.players[url].id,
      };
    }
    const {
      duration = 0,
      clipStart = 0,
      clipEnd = 0,
      listeners = {},
      personalizedConfig = {},
    } = options ?? {};
    const {
      // 推荐
      // onStateChange = () => {},
      onDurationChange = () => { },
      onBuffer = () => { },
      onLoadingStateChange = () => { },
      onPlayStateChange = () => { },
      onPositionChange = () => { },
      onAudioAnalyserChange = () => { },

      // 非必要 原生透传
      onloadstart = () => { },
      onloadedmetadata = () => { },
      onloadeddata = () => { },
      oncanplay = () => { },
      oncanplaythrough = () => { },
      onplay = () => { },
      onplaying = () => { },
      onpause = () => { },
      onended = () => { },
      onerror = () => { },
      onprogress = () => { },
      ontimeupdate = () => { },
    } = listeners;
    const id = this.getUuid();
    const listenersCache = {
      id,
      // onStateChange,
      onDurationChange,
      onBuffer,
      onLoadingStateChange,
      onPlayStateChange,
      onPositionChange,
      onAudioAnalyserChange,

      onloadstart,
      onloadedmetadata,
      onloadeddata,
      oncanplay,
      oncanplaythrough,
      onplay,
      onplaying,
      onpause,
      onended,
      onerror,
      onprogress,
      ontimeupdate,
    };

    if (!this.players[url]) {
      this.players[url] = {
        id: id,
        url: url,
        isPlaying: false,
        bufferedPosition: 0,
        duration: duration,
        currentTime: 0,
        listeners: [listenersCache],
        clipStart: clipStart,
        clipEnd: clipEnd,
        audioAnalyser: {
          audioContext: null,
          analyser: null,
          fftSize: 2048,
          analyserMediaElementSource: null,
        },
        personalizedConfig: {
          ...defaultPersonalizedConfig,
          ...personalizedConfig,
        },
      };
      const _clipStart = this.checkClipTime(this.players[url]).clipStart;
      this.players[url].currentTime = _clipStart;
      this.triggerListenerBroadcast(
        this.players[url],
        "onPositionChange",
        this.getPositionData(this.players[url])
      );
    } else {
      this.players[url].listeners.push(listenersCache);
      this.players[url].personalizedConfig = {
        ...defaultPersonalizedConfig,
        ...this.players[url].personalizedConfig,
        ...personalizedConfig,
      };
      this.triggerListenerBroadcast(
        this.players[url],
        "onPositionChange",
        this.getPositionData(this.players[url])
      );
    }
    return {
      player: this.players[url],
      id,
      listenerId: id,
      url,
    };
  }

  registerPlayUrl(url, options) {
    // 注册播放器
    const res = this.getOrCreatePlayer(url, options);
    this.triggerListenerBroadcast(
      res.player,
      "onPlayStateChange",
      res.player.isPlaying
    );
    this.triggerListenerBroadcast(
      res.player,
      "onPositionChange",
      this.getPositionData(res.player)
    );
    this.triggerListenerBroadcast(
      res.player,
      "onDurationChange",
      this.getDuration(res.player)
    );
    return res;
  }

  releaseListener(url, listenerId) {
    const player = this.players[url];
    if (player) {
      const index = player.listeners.findIndex((item) => item.id == listenerId);
      if (index !== -1) {
        player.listeners.splice(index, 1);
      }
    }
  }
  releasePlayer(obj) {
    // 组件销毁时调用释放
    const { player, id, url, listenerId } = obj;
    this.releaseListener(url, listenerId);
    if (this.players[url] && this.players[url].listeners.length === 0) {
      if (this.currentUrl == url) {
        this.audio.pause();
        this.audio.url = null;
        this.currentUrl = null;
        this.closeAudioAnalyser(this.players[url], true);
        this.currentPlayer = null;
      }
      if (this.lastUrl == url) {
        this.lastUrl = null;
      }
      delete this.players[url];
    }
  }

  setLoop(url, loop) {
    const player = this.players[url];
    if (player) {
      player.personalizedConfig.loop = loop;
    }
  }

  setClip(url, from, to) {
    const player = this.players[url];
    if (from >= 0 && to > from) {
      player.clipStart = from;
      player.clipEnd = to;
    }
    const { clipStart } = this.checkClipTime(player);
    player.currentTime = clipStart;
    if (this.currentUrl === url) {
      this.audio.currentTime = clipStart;
    } else {
      this.triggerListenerBroadcast(
        player,
        "onPositionChange",
        this.getPositionData(player)
      );
    }
  }

  checkNetworkStatus() {
    window.addEventListener("online", () => {
      if (this.audio.src) {
        this.audio.load();
        if (this.players[this.audio.src]?.isPlaying) {
          this.audio.play();
        }
      }
    });
  }

  async play(url, isInitSyncState = true) {
    console.log("play 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎", url);
    // console.log('play', url)
    if (this.currentUrl !== url) {
      this.lastUrl = this.currentUrl;
      this.pause(this.currentUrl);
      this.currentUrl = url;
      this.audio.src = url;
      if (navigator.onLine) {
        this.audio.load();
      }
      this.audio.currentTime = this.players[url].currentTime;
      if (this.currentPlayer) {
        const { clipStart } = this.checkClipTime(this.currentPlayer);
        const { autoToStartIfNotCurrent } =
          this.currentPlayer.personalizedConfig;
        if (autoToStartIfNotCurrent) {
          this.currentPlayer.currentTime = clipStart;
          this.triggerListenerBroadcast(
            this.players[this.lastUrl],
            "onPositionChange",
            this.getPositionData(this.players[this.lastUrl])
          );
        }
        await this.closeAudioAnalyser(this.currentPlayer);
      }

      this.currentPlayer = this.players[url];
    }
    this.audio.currentTime = this.currentPlayer.currentTime;
    this.triggerGlobleListenerBroadcast("curPlayUrlChange", url, this.lastUrl);
    try {
      await this.audio.play();
      this.currentPlayer.isPlaying = true;
      this.openAudioAnalyser(this.currentPlayer);
      if (isInitSyncState) {
        this.triggerListenerBroadcast(this.currentPlayer, "onplay");
        this.triggerListenerBroadcast(
          this.currentPlayer,
          "onPlayStateChange",
          true
        );
      }
    } catch (e) {
      console.log("player Error", e);
      this.audio.pause();
      this.currentPlayer.isPlaying = false;
      if (isInitSyncState) {
        this.triggerListenerBroadcast(this.currentPlayer, "onpause");
        this.triggerListenerBroadcast(
          this.currentPlayer,
          "onPlayStateChange",
          false
        );
      }
    }
    return this.currentPlayer.isPlaying;
  }

  pause(url, toStart) {
    if (this.currentUrl == url && url != "") {
      if (this.currentPlayer && this.currentPlayer.isPlaying) {
        this.currentPlayer.isPlaying = false;
        this.audio.pause();
        if (toStart) {
          this.seek(url, 0);
        }

        this.triggerListenerBroadcast(this.currentPlayer, "onpause");
        this.triggerListenerBroadcast(
          this.currentPlayer,
          "onPlayStateChange",
          false
        );
      }
    } else {
      if (toStart) {
        this.seek(url, 0);
      }
    }
  }

  tempToggle(isPause) {
    if (isPause) {
      this.beforeTemp = this.isPlaying;
      this.audio.pause();
    } else {
      if (this.beforeTemp) {
        this.audio.play();
      }
    }
  }

  seek(url, progress) {
    if (!url) {
      return;
    }
    if (this.currentUrl == url) {
      const player = this.players[this.currentUrl];
      const { clipStart, clipEnd } = this.checkClipTime(player);
      const { duration } = player;
      if (clipStart >= 0 && clipEnd > clipStart) {
        const time = clipStart + progress * (clipEnd - clipStart);
        this.audio.currentTime = time;
      } else {
        this.audio.currentTime = progress * duration;
      }
    } else {
      const player = this.players[url];
      const { canSeekNotCurrent } = player.personalizedConfig;
      const { clipStart, clipEnd } = this.checkClipTime(player);
      const { duration } = player;
      let time = duration * progress;
      if (clipStart >= 0 && clipEnd > clipStart) {
        time = clipStart + progress * (clipEnd - clipStart);
      }
      if (canSeekNotCurrent) {
        player.currentTime = time;
      } else {
        player.currentTime = clipStart;
      }
      this.triggerListenerBroadcast(
        player,
        "onPositionChange",
        this.getPositionData(player)
      );
    }
  }
}

const globalAudioPlayer = new GlobalAudioPlayer();

export default globalAudioPlayer;

调用示例

// Example usage: 单位秒

// const renderData = reactive({
//     url: props.url,
//     startTime: '00:00',
//     endTime: '00:00',
//     bufferTime: '00:00',
//     duration: props.duration,
//     progress: 0,
//     isPlaying: false,
//     isLoading: false,
//   });
// 初始化
// const curPlayerData = globalAudioPlayer.registerPlayUrl(renderData.url, {
//     duration: props.duration ?? 0, 
//     clipStart: props.from, 
//     clipEnd: props.to, 
//     listeners: {
//       onDurationChange({duration, durationTime,sourceDuration,sourceDurationTime}){
//         renderData.duration = duration;
//         renderData.endTime = durationTime;
//       },
//       onPlayStateChange(isPlaying){
//         renderData.isPlaying = isPlaying;
//       },
//       onLoadingStateChange(isLoading){
//         renderData.isLoading = isLoading;
//       },
//       onPositionChange: (obj)=>{
//         const {progress, currentTime, endTime} = obj;
//         if(!stateData.isChangeProgress){
//           renderData.startTime = currentTime;
//           renderData.progress = progress*100;
//         }
//       },
//     }
//   });
// 播放
// globalAudioPlayer.play(renderData.url, true);
// 暂停
// globalAudioPlayer.pause(renderData.url);
// 调整播放位置
// globalAudioPlayer.seek(renderData.url, renderData.progress/100);
// 调整播放区间
// globalAudioPlayer.setClip(renderData.url, startTime, endTime);
// 释放
// globalAudioPlayer.releasePlayer(curPlayerData);