UNPKG

mstf-kit

Version:

一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数

756 lines (755 loc) 30.3 kB
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 };