框架集成
本指南介绍与 React 等框架的集成模式、基于 Promise 的等待以及 UI 动画同步。
使用的模块: adapters, play
React 演示
在 React 中使用 waa-play 的实际运行演示。使用 useSyncExternalStore 和 subscribeSnapshot / getSnapshot 来响应式管理播放状态。
音源
生成合成音色或加载音频文件以开始使用。
演示源代码
此演示的核心逻辑。省略了 UI 渲染代码。
import { useCallback, useSyncExternalStore, useState, useMemo, useRef } from "react";import { WaaPlayer, getSnapshot, subscribeSnapshot } from "waa-play";import type { Playback, PlaybackSnapshot, PeakPair } from "waa-play";
// --- hooks ---
function usePlaybackSnapshot(playback: Playback | null): PlaybackSnapshot | null { const subscribe = useCallback( (cb: () => void) => (playback ? subscribeSnapshot(playback, cb) : () => {}), [playback], ); const snap = useCallback( () => (playback ? getSnapshot(playback) : null), [playback], ); return useSyncExternalStore(subscribe, snap, snap);}
function useReactPlayer() { const waaRef = useRef(new WaaPlayer()); const [buffer, setBuffer] = useState<AudioBuffer | null>(null); const [playback, setPlayback] = useState<Playback | null>(null); const [loop, setLoop] = useState(true); const snap = usePlaybackSnapshot(playback); const peaks = useMemo( () => (buffer ? waaRef.current.extractPeakPairs(buffer, { resolution: 200 }) : []), [buffer], );
async function handleGenerate() { const waa = waaRef.current; await waa.ensureRunning(); if (playback && playback.getState() !== "stopped") playback.stop(); setPlayback(null); setBuffer(waa.createSineBuffer(440, 3)); }
async function handleFile(e: React.ChangeEvent<HTMLInputElement>) { const file = e.target.files?.[0]; if (!file) return; const waa = waaRef.current; await waa.ensureRunning(); setPlayback(null); setBuffer(await waa.loadFromBlob(file)); }
function handleToggle() { if (!buffer) return; if (!playback || playback.getState() === "stopped") { if (playback) playback.dispose(); setPlayback(waaRef.current.play(buffer, { loop })); } else { playback.togglePlayPause(); } }
function handleStop() { playback?.stop(); }
function handleSeek(ratio: number) { if (playback && buffer) { playback.seek(ratio * buffer.duration); } }
function handleLoopToggle() { const next = !loop; setLoop(next); if (playback) playback.setLoop(next); }
return { buffer, snap, peaks, loop, handleGenerate, handleFile, handleToggle, handleStop, handleSeek, handleLoopToggle };}
// --- component ---
export default function ReactPlayerDemo() { const { buffer, snap, peaks, loop, handleGenerate, handleFile, handleToggle, handleStop, handleSeek, handleLoopToggle } = useReactPlayer(); const state = snap?.state ?? "stopped";
return ( <div> <button onClick={handleGenerate}>Generate Sine</button> <input type="file" accept="audio/*" onChange={handleFile} /> {buffer && ( <> <Waveform peaks={peaks} progress={snap?.progress ?? 0} onSeek={handleSeek} /> <span>{formatTime(snap?.position ?? 0)} / {formatTime(snap?.duration ?? 0)}</span> <button onClick={handleToggle}> {state === "playing" ? "Pause" : "Play"} </button> <button onClick={handleStop} disabled={state === "stopped"}> Stop </button> <label> <input type="checkbox" checked={loop} onChange={handleLoopToggle} /> Loop </label> </> )} </div> );}代码示例
1. React 自定义 Hook
subscribeSnapshot 和 getSnapshot 与 React 的 useSyncExternalStore 直接兼容。
import { useSyncExternalStore, useRef, useCallback } from "react";import { WaaPlayer } from "waa-play";import type { Playback, PlaybackSnapshot } from "waa-play";
// Shared WaaPlayer instanceconst waa = new WaaPlayer();
function usePlaybackSnapshot(playback: Playback | null): PlaybackSnapshot | null { const subscribe = useCallback( (callback: () => void) => { if (!playback) return () => {}; return waa.subscribeSnapshot(playback, callback); }, [playback], ); const snap = useCallback( () => (playback ? waa.getSnapshot(playback) : null), [playback], ); return useSyncExternalStore(subscribe, snap, snap);}
// Usage examplefunction AudioPlayer({ url }: { url: string }) { const playbackRef = useRef<Playback | null>(null); const snapshot = usePlaybackSnapshot(playbackRef.current);
async function handlePlay() { await waa.ensureRunning(); const buffer = await waa.load(url); playbackRef.current = waa.play(buffer); }
return ( <div> <button onClick={handlePlay}>Play</button> {snapshot && ( <div> <p>State: {snapshot.state}</p> <p>Position: {snapshot.position.toFixed(1)}s / {snapshot.duration.toFixed(1)}s</p> <progress value={snapshot.progress} max={1} /> </div> )} </div> );}说明: subscribeSnapshot 订阅 statechange、timeupdate、seek 和 ended 事件,在变化时调用 callback。getSnapshot 返回不可变的快照。这两个函数直接对应 useSyncExternalStore 的 subscribe / getSnapshot 参数。
函数 API 版
import { useSyncExternalStore, useRef, useCallback } from "react";import { createContext, ensureRunning } from "waa-play/context";import { loadBuffer } from "waa-play/buffer";import { play } from "waa-play/play";import { subscribeSnapshot, getSnapshot } from "waa-play/adapters";import type { Playback, PlaybackSnapshot } from "waa-play";
function usePlaybackSnapshot(playback: Playback | null): PlaybackSnapshot | null { const subscribe = useCallback( (callback: () => void) => { if (!playback) return () => {}; return subscribeSnapshot(playback, callback); }, [playback], ); const snap = useCallback( () => (playback ? getSnapshot(playback) : null), [playback], ); return useSyncExternalStore(subscribe, snap, snap);}
// Usage examplefunction AudioPlayer({ url }: { url: string }) { const ctxRef = useRef<AudioContext>(new AudioContext()); const playbackRef = useRef<Playback | null>(null); const snapshot = usePlaybackSnapshot(playbackRef.current);
async function handlePlay() { const ctx = ctxRef.current; await ensureRunning(ctx); const buffer = await loadBuffer(ctx, url); playbackRef.current = play(ctx, buffer); }
return ( <div> <button onClick={handlePlay}>Play</button> {snapshot && ( <div> <p>State: {snapshot.state}</p> <p>Position: {snapshot.position.toFixed(1)}s / {snapshot.duration.toFixed(1)}s</p> <progress value={snapshot.progress} max={1} /> </div> )} </div> );}2. 基于 Promise 的等待
您可以使用 Promise 等待播放完成或到达指定位置。
import { WaaPlayer } from "waa-play";
const waa = new WaaPlayer();await waa.ensureRunning();const buffer = await waa.load("/audio/track.mp3");const playback = waa.play(buffer);
// Wait for playback to completeawait waa.whenEnded(playback);console.log("Playback completed");
// Wait for a specific positionconst playback2 = waa.play(buffer);await waa.whenPosition(playback2, 10); // 10 second markconsole.log("Reached 10 seconds");说明: whenEnded 返回一个在 ended 事件(自然结束)时 resolve 的 Promise。手动 stop() 不会 resolve。whenPosition 监听 timeupdate 事件,在到达(或超过)指定位置时 resolve。
函数 API 版
import { createContext, ensureRunning, loadBuffer, play } from "waa-play";import { whenEnded, whenPosition } from "waa-play/adapters";
const ctx = createContext();await ensureRunning(ctx);const buffer = await loadBuffer(ctx, "/audio/track.mp3");const playback = play(ctx, buffer);
// Wait for playback to completeawait whenEnded(playback);console.log("Playback completed");
// Wait for a specific positionconst playback2 = play(ctx, buffer);await whenPosition(playback2, 10); // 10 second markconsole.log("Reached 10 seconds");3. 平滑 UI 动画
onFrame 基于 requestAnimationFrame 实现平滑的进度条和播放器更新。
import { WaaPlayer } from "waa-play";
const waa = new WaaPlayer();await waa.ensureRunning();const buffer = await waa.load("/audio/track.mp3");
const progressBar = document.querySelector(".progress-bar") as HTMLElement;const timeDisplay = document.querySelector(".time") as HTMLElement;
const playback = waa.play(buffer);
// Update UI every frame with onFrameconst stopFrame = waa.onFrame(playback, (snapshot) => { // Progress bar progressBar.style.width = `${snapshot.progress * 100}%`;
// Time display const pos = snapshot.position; const dur = snapshot.duration; timeDisplay.textContent = `${formatTime(pos)} / ${formatTime(dur)}`;});
function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, "0")}`;}
// Cleanupplayback.on("ended", () => stopFrame());说明: onFrame 内部管理 requestAnimationFrame 循环,每帧将 PlaybackSnapshot 传递给 callback。调用返回的函数可停止循环。相比 timeupdate(基于 setInterval,默认 50ms),可实现更平滑的更新。
与 timeupdate 事件的区别: timeupdate 基于 setInterval,在后台标签页中也能工作。onFrame 基于 requestAnimationFrame,适合视觉更新,但在后台会暂停。
函数 API 版
import { createContext, ensureRunning, loadBuffer, play } from "waa-play";import { onFrame } from "waa-play/adapters";
const ctx = createContext();await ensureRunning(ctx);const buffer = await loadBuffer(ctx, "/audio/track.mp3");
const progressBar = document.querySelector(".progress-bar") as HTMLElement;const timeDisplay = document.querySelector(".time") as HTMLElement;
const playback = play(ctx, buffer);
// Update UI every frame with onFrameconst stopFrame = onFrame(playback, (snapshot) => { // Progress bar progressBar.style.width = `${snapshot.progress * 100}%`;
// Time display const pos = snapshot.position; const dur = snapshot.duration; timeDisplay.textContent = `${formatTime(pos)} / ${formatTime(dur)}`;});
function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, "0")}`;}
// Cleanupplayback.on("ended", () => stopFrame());