跳转到内容

框架集成

本指南介绍与 React 等框架的集成模式、基于 Promise 的等待以及 UI 动画同步。

使用的模块: adapters, play

React 演示

在 React 中使用 waa-play 的实际运行演示。使用 useSyncExternalStoresubscribeSnapshot / 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

subscribeSnapshotgetSnapshot 与 React 的 useSyncExternalStore 直接兼容。

import { useSyncExternalStore, useRef, useCallback } from "react";
import { WaaPlayer } from "waa-play";
import type { Playback, PlaybackSnapshot } from "waa-play";
// Shared WaaPlayer instance
const 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 example
function 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 订阅 statechangetimeupdateseekended 事件,在变化时调用 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 example
function 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 complete
await waa.whenEnded(playback);
console.log("Playback completed");
// Wait for a specific position
const playback2 = waa.play(buffer);
await waa.whenPosition(playback2, 10); // 10 second mark
console.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 complete
await whenEnded(playback);
console.log("Playback completed");
// Wait for a specific position
const playback2 = play(ctx, buffer);
await whenPosition(playback2, 10); // 10 second mark
console.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 onFrame
const 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")}`;
}
// Cleanup
playback.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 onFrame
const 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")}`;
}
// Cleanup
playback.on("ended", () => stopFrame());

相关 API