mstf-kit
Version:
一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数
756 lines (755 loc) • 30.3 kB
JavaScript
import { base64ToAudioBlob } from "./audio.js";
function createAudioPlayer(options = {}) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
const config = {
renderUI: (_a = options.renderUI) != null ? _a : false,
container: (_b = options.container) != null ? _b : document.body,
width: (_c = options.width) != null ? _c : "100%",
height: (_d = options.height) != null ? _d : "80px",
theme: (_e = options.theme) != null ? _e : "default",
showVisualization: (_f = options.showVisualization) != null ? _f : true,
visualizationType: (_g = options.visualizationType) != null ? _g : "bars",
autoplay: (_h = options.autoplay) != null ? _h : false,
loop: (_i = options.loop) != null ? _i : false,
playbackRate: (_j = options.playbackRate) != null ? _j : 1,
volume: (_k = options.volume) != null ? _k : 1,
sampleRate: (_l = options.sampleRate) != null ? _l : 44100,
onPlay: (_m = options.onPlay) != null ? _m : () => {
},
onPause: (_n = options.onPause) != null ? _n : () => {
},
onEnded: (_o = options.onEnded) != null ? _o : () => {
},
onTimeUpdate: (_p = options.onTimeUpdate) != null ? _p : () => {
},
onVolumeChange: (_q = options.onVolumeChange) != null ? _q : () => {
},
onError: (_r = options.onError) != null ? _r : (error) => console.error("Audio player error:", error)
};
let state = "idle";
let audioElement = null;
let audioContext = null;
let analyser = null;
let audioSource = null;
let visualizationCanvas = null;
let canvasContext = null;
let animationFrameId = null;
let playerContainer = null;
let playerControls = null;
let currentAudioUrl = null;
function initAudio() {
audioElement = new Audio();
audioElement.volume = config.volume;
audioElement.playbackRate = config.playbackRate;
audioElement.loop = config.loop;
audioElement.addEventListener("play", handlePlay);
audioElement.addEventListener("pause", handlePause);
audioElement.addEventListener("ended", handleEnded);
audioElement.addEventListener("timeupdate", handleTimeUpdate);
audioElement.addEventListener("volumechange", handleVolumeChange);
audioElement.addEventListener("error", handleError);
if (config.renderUI) {
createPlayerUI();
}
}
function getDOMElement(container) {
if (!container) {
return null;
}
if (typeof container === "string") {
return document.querySelector(container);
}
if (container instanceof HTMLElement) {
return container;
}
if (container && "value" in container && container.value instanceof HTMLElement) {
return container.value;
}
return null;
}
function createPlayerUI() {
if (!audioElement) return;
const container = getDOMElement(config.container);
if (!container) {
console.error("无法找到播放器容器元素");
return;
}
playerContainer = document.createElement("div");
playerContainer.className = `mstf-audio-player theme-${config.theme}`;
playerContainer.style.width = config.width;
playerContainer.style.height = config.height;
playerContainer.style.position = "relative";
playerContainer.style.display = "flex";
playerContainer.style.flexDirection = "column";
playerContainer.style.borderRadius = "12px";
playerContainer.style.overflow = "hidden";
playerContainer.style.boxShadow = config.theme === "dark" ? "0 8px 24px rgba(0, 0, 0, 0.3), 0 3px 8px rgba(0, 0, 0, 0.2)" : "0 8px 24px rgba(0, 0, 0, 0.07), 0 3px 8px rgba(0, 0, 0, 0.05)";
playerContainer.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
if (config.theme === "dark") {
playerContainer.style.backgroundColor = "#1a1a1a";
playerContainer.style.color = "#fff";
} else if (config.theme === "minimal") {
playerContainer.style.backgroundColor = "transparent";
playerContainer.style.boxShadow = "none";
} else {
playerContainer.style.backgroundColor = "#ffffff";
playerContainer.style.color = "#333";
}
if (config.showVisualization) {
visualizationCanvas = document.createElement("canvas");
visualizationCanvas.style.width = "100%";
visualizationCanvas.style.height = config.theme === "minimal" ? "40px" : "60%";
visualizationCanvas.style.backgroundColor = config.theme === "dark" ? "#1e1e1e" : "transparent";
visualizationCanvas.style.transition = "all 0.3s ease";
playerContainer.appendChild(visualizationCanvas);
canvasContext = visualizationCanvas.getContext("2d");
const setCanvasSize = () => {
if (visualizationCanvas && canvasContext) {
const { width, height } = visualizationCanvas.getBoundingClientRect();
visualizationCanvas.width = width * window.devicePixelRatio;
visualizationCanvas.height = height * window.devicePixelRatio;
canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio);
}
};
setCanvasSize();
window.addEventListener("resize", setCanvasSize);
}
playerControls = document.createElement("div");
playerControls.className = "mstf-player-controls";
playerControls.style.display = "flex";
playerControls.style.alignItems = "center";
playerControls.style.padding = "10px 16px";
playerControls.style.height = config.theme === "minimal" ? "36px" : "48px";
playerControls.style.backgroundColor = config.theme === "dark" ? "#252525" : "#f8f8f8";
playerControls.style.borderTop = config.theme === "dark" ? "1px solid #333" : "1px solid #eee";
const playButton = document.createElement("button");
playButton.className = "mstf-player-play-btn";
playButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>`;
playButton.style.border = "none";
playButton.style.background = config.theme === "dark" ? "#444" : "#eee";
playButton.style.color = config.theme === "dark" ? "#fff" : "#333";
playButton.style.cursor = "pointer";
playButton.style.width = "36px";
playButton.style.height = "36px";
playButton.style.borderRadius = "50%";
playButton.style.display = "flex";
playButton.style.justifyContent = "center";
playButton.style.alignItems = "center";
playButton.style.marginRight = "12px";
playButton.style.transition = "all 0.2s ease";
playButton.onmouseover = () => {
playButton.style.background = config.theme === "dark" ? "#555" : "#ddd";
playButton.style.transform = "scale(1.05)";
};
playButton.onmouseout = () => {
playButton.style.background = config.theme === "dark" ? "#444" : "#eee";
playButton.style.transform = "scale(1)";
};
playButton.addEventListener("click", async () => {
if (audioElement == null ? void 0 : audioElement.paused) {
await player.play();
} else {
player.pause();
}
});
const updatePlayButton = () => {
if (audioElement == null ? void 0 : audioElement.paused) {
playButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>`;
} else {
playButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="6" y="4" width="4" height="16"></rect>
<rect x="14" y="4" width="4" height="16"></rect>
</svg>`;
}
};
const timeDisplay = document.createElement("div");
timeDisplay.className = "mstf-player-time";
timeDisplay.style.fontSize = "13px";
timeDisplay.style.fontWeight = "500";
timeDisplay.style.margin = "0 12px";
timeDisplay.style.minWidth = "90px";
timeDisplay.style.color = config.theme === "dark" ? "#aaa" : "#666";
timeDisplay.textContent = "0:00 / 0:00";
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? "0" + secs : secs}`;
};
const progressContainer = document.createElement("div");
progressContainer.className = "mstf-player-progress";
progressContainer.style.flex = "1";
progressContainer.style.height = "8px";
progressContainer.style.borderRadius = "4px";
progressContainer.style.backgroundColor = config.theme === "dark" ? "#444" : "#e0e0e0";
progressContainer.style.cursor = "pointer";
progressContainer.style.position = "relative";
progressContainer.style.overflow = "hidden";
progressContainer.style.transition = "height 0.2s ease";
progressContainer.onmouseover = () => {
progressContainer.style.height = "10px";
};
progressContainer.onmouseout = () => {
progressContainer.style.height = "8px";
};
const progressBar = document.createElement("div");
progressBar.className = "mstf-player-progress-bar";
progressBar.style.height = "100%";
progressBar.style.width = "0%";
progressBar.style.borderRadius = "4px";
progressBar.style.background = config.theme === "dark" ? "linear-gradient(90deg, #2ecc71, #1db954)" : "linear-gradient(90deg, #0070f3, #00a2ff)";
progressBar.style.transition = "width 0.1s linear";
progressContainer.appendChild(progressBar);
const progressThumb = document.createElement("div");
progressThumb.className = "mstf-player-progress-thumb";
progressThumb.style.width = "12px";
progressThumb.style.height = "12px";
progressThumb.style.borderRadius = "50%";
progressThumb.style.backgroundColor = config.theme === "dark" ? "#2ecc71" : "#0070f3";
progressThumb.style.position = "absolute";
progressThumb.style.top = "50%";
progressThumb.style.transform = "translate(-50%, -50%) scale(0)";
progressThumb.style.left = "0%";
progressThumb.style.opacity = "0";
progressThumb.style.transition = "transform 0.2s, opacity 0.2s";
progressThumb.style.boxShadow = "0 0 5px rgba(0,0,0,0.2)";
progressContainer.appendChild(progressThumb);
progressContainer.addEventListener("mouseover", () => {
progressThumb.style.opacity = "1";
progressThumb.style.transform = "translate(-50%, -50%) scale(1)";
});
progressContainer.addEventListener("mousemove", (e) => {
const rect = progressContainer.getBoundingClientRect();
const percent = Math.min(Math.max(0, (e.clientX - rect.left) / rect.width), 1);
progressThumb.style.left = `${percent * 100}%`;
});
progressContainer.addEventListener("mouseout", () => {
progressThumb.style.opacity = "0";
progressThumb.style.transform = "translate(-50%, -50%) scale(0)";
});
progressContainer.addEventListener("click", (e) => {
if (!audioElement) return;
const rect = progressContainer.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
player.seek(pos * audioElement.duration);
});
const volumeContainer = document.createElement("div");
volumeContainer.className = "mstf-player-volume";
volumeContainer.style.display = "flex";
volumeContainer.style.alignItems = "center";
volumeContainer.style.margin = "0 8px";
const volumeButton = document.createElement("button");
volumeButton.className = "mstf-player-volume-btn";
volumeButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
</svg>`;
volumeButton.style.border = "none";
volumeButton.style.background = config.theme === "dark" ? "#444" : "#eee";
volumeButton.style.width = "36px";
volumeButton.style.height = "36px";
volumeButton.style.borderRadius = "50%";
volumeButton.style.display = "flex";
volumeButton.style.justifyContent = "center";
volumeButton.style.alignItems = "center";
volumeButton.style.cursor = "pointer";
volumeButton.style.color = config.theme === "dark" ? "#fff" : "#333";
volumeButton.style.transition = "all 0.2s ease";
volumeButton.onmouseover = () => {
volumeButton.style.background = config.theme === "dark" ? "#555" : "#ddd";
volumeButton.style.transform = "scale(1.05)";
};
volumeButton.onmouseout = () => {
volumeButton.style.background = config.theme === "dark" ? "#444" : "#eee";
volumeButton.style.transform = "scale(1)";
};
volumeButton.addEventListener("click", () => {
player.toggleMute();
});
const volumeSlider = document.createElement("input");
volumeSlider.type = "range";
volumeSlider.min = "0";
volumeSlider.max = "1";
volumeSlider.step = "0.1";
volumeSlider.value = "1";
volumeSlider.style.width = "0";
volumeSlider.style.height = "4px";
volumeSlider.style.margin = "0";
volumeSlider.style.appearance = "none";
volumeSlider.style.background = config.theme === "dark" ? "#555" : "#ddd";
volumeSlider.style.borderRadius = "2px";
volumeSlider.style.outline = "none";
volumeSlider.style.opacity = "0";
volumeSlider.style.transition = "all 0.2s ease";
volumeSlider.style.cursor = "pointer";
volumeSlider.style.setProperty("--thumb-color", config.theme === "dark" ? "#1ed760" : "#0070f3");
volumeSlider.style.setProperty("--thumb-height", "12px");
volumeSlider.style.setProperty("--thumb-width", "12px");
volumeSlider.style.setProperty("--track-color", config.theme === "dark" ? "#555" : "#ddd");
const sliderStyle = document.createElement("style");
sliderStyle.textContent = `
.mstf-player-volume input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: var(--thumb-width);
height: var(--thumb-height);
border-radius: 50%;
background: var(--thumb-color);
cursor: pointer;
}
.mstf-player-volume input[type="range"]::-moz-range-thumb {
width: var(--thumb-width);
height: var(--thumb-height);
border-radius: 50%;
background: var(--thumb-color);
cursor: pointer;
border: none;
}
`;
document.head.appendChild(sliderStyle);
volumeContainer.addEventListener("mouseenter", () => {
volumeSlider.style.width = "60px";
volumeSlider.style.marginLeft = "8px";
volumeSlider.style.opacity = "1";
});
volumeContainer.addEventListener("mouseleave", () => {
volumeSlider.style.width = "0";
volumeSlider.style.marginLeft = "0";
volumeSlider.style.opacity = "0";
});
volumeSlider.addEventListener("input", (e) => {
const target = e.target;
const volume = parseFloat(target.value);
player.setVolume(volume);
});
volumeContainer.appendChild(volumeButton);
volumeContainer.appendChild(volumeSlider);
playerControls.appendChild(playButton);
playerControls.appendChild(timeDisplay);
playerControls.appendChild(progressContainer);
if (config.theme !== "minimal") {
playerControls.appendChild(volumeContainer);
}
playerContainer.appendChild(playerControls);
container.appendChild(playerContainer);
if (config.showVisualization) {
initAudioContext();
}
const updateUI = () => {
if (!audioElement) return;
const progress = audioElement.currentTime / (audioElement.duration || 1);
progressBar.style.width = `${progress * 100}%`;
progressThumb.style.left = `${progress * 100}%`;
timeDisplay.textContent = `${formatTime(audioElement.currentTime)} / ${formatTime(audioElement.duration || 0)}`;
updatePlayButton();
if (audioElement.muted || audioElement.volume === 0) {
volumeButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<line x1="23" y1="9" x2="17" y2="15"></line>
<line x1="17" y1="9" x2="23" y2="15"></line>
</svg>`;
} else if (audioElement.volume < 0.5) {
volumeButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>`;
} else {
volumeButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
</svg>`;
}
volumeSlider.value = audioElement.muted ? "0" : audioElement.volume.toString();
};
audioElement.addEventListener("timeupdate", updateUI);
audioElement.addEventListener("play", updateUI);
audioElement.addEventListener("pause", updateUI);
audioElement.addEventListener("volumechange", updateUI);
updateUI();
}
function initAudioContext() {
if (!audioElement) return;
try {
if (config.sampleRate) {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: config.sampleRate
});
console.info(`音频播放器使用指定采样率: ${config.sampleRate}Hz`);
} catch (sampleRateError) {
console.warn(`使用指定采样率 ${config.sampleRate}Hz 失败:`, sampleRateError);
console.info("回退到浏览器默认采样率");
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
} else {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
const actualSampleRate = audioContext.sampleRate;
console.info(`音频播放器使用的实际采样率: ${actualSampleRate}Hz`);
analyser = audioContext.createAnalyser();
switch (config.visualizationType) {
case "waveform":
analyser.fftSize = 2048;
break;
case "bars":
analyser.fftSize = 256;
break;
case "circle":
analyser.fftSize = 1024;
break;
}
try {
audioSource = audioContext.createMediaElementSource(audioElement);
audioSource.connect(analyser);
analyser.connect(audioContext.destination);
} catch (connectionError) {
console.warn("音频节点连接失败,可能是采样率不匹配:", connectionError);
if (audioSource) {
try {
audioSource.disconnect();
} catch (e) {
}
}
if (analyser) {
try {
analyser.disconnect();
} catch (e) {
}
}
if (audioContext) {
try {
audioContext.close();
} catch (e) {
}
}
audioContext = null;
audioSource = null;
analyser = null;
if (connectionError instanceof Error && connectionError.message && (connectionError.message.includes("different sample-rate") || connectionError.message.includes("采样率"))) {
console.info("已降级为基本播放模式,音频可视化功能将不可用");
return;
} else {
throw connectionError;
}
}
startVisualization();
} catch (error) {
console.error("初始化音频上下文失败:", error);
config.onError(new Error("初始化音频可视化失败,但不影响播放功能"));
if (audioSource) {
try {
audioSource.disconnect();
} catch (e) {
}
}
if (analyser) {
try {
analyser.disconnect();
} catch (e) {
}
}
if (audioContext) {
try {
audioContext.close();
} catch (e) {
}
}
audioContext = null;
audioSource = null;
analyser = null;
}
}
function startVisualization() {
if (!analyser || !canvasContext || !visualizationCanvas) return;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function draw() {
if (!analyser || !canvasContext || !visualizationCanvas || state === "destroyed") return;
animationFrameId = requestAnimationFrame(draw);
const width = visualizationCanvas.width / window.devicePixelRatio;
const height = visualizationCanvas.height / window.devicePixelRatio;
canvasContext.clearRect(0, 0, width, height);
switch (config.visualizationType) {
case "waveform":
drawWaveform(dataArray, width, height);
break;
case "bars":
drawBars(dataArray, width, height);
break;
case "circle":
drawCircle(dataArray, width, height);
break;
}
}
function drawWaveform(dataArray2, width, height) {
if (!analyser || !canvasContext) return;
analyser.getByteTimeDomainData(dataArray2);
canvasContext.lineWidth = 2;
canvasContext.strokeStyle = config.theme === "dark" ? "#1db954" : "#007bff";
canvasContext.beginPath();
const sliceWidth = width / dataArray2.length;
let x = 0;
for (let i = 0; i < dataArray2.length; i++) {
const v = dataArray2[i] / 128;
const y = v * height / 2;
if (i === 0) {
canvasContext.moveTo(x, y);
} else {
canvasContext.lineTo(x, y);
}
x += sliceWidth;
}
canvasContext.lineTo(width, height / 2);
canvasContext.stroke();
}
function drawBars(dataArray2, width, height) {
if (!analyser || !canvasContext) return;
analyser.getByteFrequencyData(dataArray2);
const barWidth = width / dataArray2.length * 2.5;
let x = 0;
for (let i = 0; i < dataArray2.length; i++) {
const barHeight = dataArray2[i] / 255 * height;
let hue = i / dataArray2.length * 360;
if (config.theme !== "minimal") {
canvasContext.fillStyle = config.theme === "dark" ? `hsl(${hue}, 70%, 60%)` : `hsl(${hue}, 70%, 50%)`;
} else {
canvasContext.fillStyle = "#007bff";
}
canvasContext.fillRect(x, height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
function drawCircle(dataArray2, width, height) {
if (!analyser || !canvasContext) return;
analyser.getByteFrequencyData(dataArray2);
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.max(5, Math.min(centerX, centerY) * 0.85);
canvasContext.beginPath();
canvasContext.arc(centerX, centerY, Math.max(2, radius / 4), 0, 2 * Math.PI);
canvasContext.fillStyle = config.theme === "dark" ? "#333" : "#f5f5f5";
canvasContext.fill();
for (let i = 0; i < dataArray2.length; i++) {
const value = dataArray2[i] / 255;
const angle = i / dataArray2.length * 2 * Math.PI;
const innerRadius = Math.max(2, radius / 3);
const outerRadius = Math.max(innerRadius + 1, innerRadius + radius * 0.7 * value);
const startX = centerX + innerRadius * Math.cos(angle);
const startY = centerY + innerRadius * Math.sin(angle);
const endX = centerX + outerRadius * Math.cos(angle);
const endY = centerY + outerRadius * Math.sin(angle);
let hue = i / dataArray2.length * 360;
canvasContext.strokeStyle = config.theme === "dark" ? `hsl(${hue}, 70%, 60%)` : `hsl(${hue}, 70%, 50%)`;
canvasContext.lineWidth = 2;
canvasContext.beginPath();
canvasContext.moveTo(startX, startY);
canvasContext.lineTo(endX, endY);
canvasContext.stroke();
}
}
draw();
}
function stopVisualization() {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
}
function handlePlay() {
state = "playing";
config.onPlay();
if (config.renderUI && config.showVisualization && (audioContext == null ? void 0 : audioContext.state) === "suspended") {
audioContext.resume();
}
}
function handlePause() {
state = "paused";
config.onPause();
}
function handleEnded() {
state = "ended";
config.onEnded();
}
function handleTimeUpdate() {
if (!audioElement) return;
config.onTimeUpdate(audioElement.currentTime, audioElement.duration);
}
function handleVolumeChange() {
if (!audioElement) return;
config.onVolumeChange(audioElement.volume, audioElement.muted);
}
function handleError() {
state = "error";
config.onError(new Error("音频播放错误"));
}
const player = {
async load(source) {
if (!audioElement) {
initAudio();
}
if (!audioElement) {
throw new Error("音频元素初始化失败");
}
if (currentAudioUrl && currentAudioUrl.startsWith("blob:")) {
URL.revokeObjectURL(currentAudioUrl);
}
state = "loading";
try {
if (typeof source === "string") {
if (source.startsWith("data:audio")) {
const mimeType = source.split(",")[0].split(":")[1].split(";")[0];
const base64 = source.split(",")[1];
const blob = base64ToAudioBlob(base64, mimeType);
currentAudioUrl = URL.createObjectURL(blob);
} else {
currentAudioUrl = source;
}
} else {
currentAudioUrl = URL.createObjectURL(source);
}
audioElement.src = currentAudioUrl;
await audioElement.load();
if (config.autoplay) {
try {
await player.play();
} catch (error) {
console.warn("自动播放失败,可能需要用户交互:", error);
}
}
state = "idle";
} catch (error) {
state = "error";
config.onError(error instanceof Error ? error : new Error("加载音频失败"));
throw error;
}
},
async play() {
if (!audioElement) {
throw new Error("播放器未初始化");
}
try {
await audioElement.play();
} catch (error) {
state = "error";
config.onError(error instanceof Error ? error : new Error("播放失败"));
throw error;
}
},
pause() {
if (!audioElement) return;
audioElement.pause();
},
stop() {
if (!audioElement) return;
audioElement.pause();
audioElement.currentTime = 0;
state = "idle";
},
seek(time) {
if (!audioElement) return;
audioElement.currentTime = Math.max(0, Math.min(time, audioElement.duration || 0));
},
setVolume(volume) {
if (!audioElement) return;
audioElement.volume = Math.max(0, Math.min(volume, 1));
},
setPlaybackRate(rate) {
if (!audioElement) return;
audioElement.playbackRate = Math.max(0.5, Math.min(rate, 2));
},
toggleMute() {
if (!audioElement) return;
audioElement.muted = !audioElement.muted;
},
getState() {
return state;
},
getCurrentTime() {
return (audioElement == null ? void 0 : audioElement.currentTime) || 0;
},
getDuration() {
return (audioElement == null ? void 0 : audioElement.duration) || 0;
},
getVolume() {
return (audioElement == null ? void 0 : audioElement.volume) || 0;
},
isMuted() {
return (audioElement == null ? void 0 : audioElement.muted) || false;
},
getProgress() {
if (!audioElement || !audioElement.duration) return 0;
return audioElement.currentTime / audioElement.duration;
},
isPlaying() {
return state === "playing";
},
destroy() {
stopVisualization();
if (currentAudioUrl && currentAudioUrl.startsWith("blob:")) {
URL.revokeObjectURL(currentAudioUrl);
}
if (audioElement) {
audioElement.removeEventListener("play", handlePlay);
audioElement.removeEventListener("pause", handlePause);
audioElement.removeEventListener("ended", handleEnded);
audioElement.removeEventListener("timeupdate", handleTimeUpdate);
audioElement.removeEventListener("volumechange", handleVolumeChange);
audioElement.removeEventListener("error", handleError);
audioElement.pause();
audioElement.src = "";
audioElement.load();
}
if (audioSource) {
audioSource.disconnect();
}
if (analyser) {
analyser.disconnect();
}
if (audioContext) {
audioContext.close();
}
if (playerContainer && playerContainer.parentNode) {
playerContainer.parentNode.removeChild(playerContainer);
}
state = "destroyed";
audioElement = null;
audioContext = null;
analyser = null;
audioSource = null;
visualizationCanvas = null;
canvasContext = null;
animationFrameId = null;
playerContainer = null;
playerControls = null;
currentAudioUrl = null;
}
};
initAudio();
return player;
}
async function playAudio(blob, options = {}) {
const player = createAudioPlayer({
autoplay: options.autoplay !== void 0 ? options.autoplay : true,
loop: options.loop || false,
volume: options.volume || 1,
onEnded: options.onEnded
});
await player.load(blob);
if (options.autoplay !== false) {
try {
await player.play();
} catch (error) {
console.warn("自动播放失败,可能需要用户交互:", error);
}
}
return player;
}
export {
createAudioPlayer,
playAudio
};