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);