公共 API 与回调事件
AudioPlayer 和 useAudioPlayer() 都是 @trsoliu/audio-core 的 React 适配层。回调不是
原生 <audio> DOM 事件的逐条透传,而是根据不可变 AudioSnapshot 的变化调用。业务代码
应以 callback payload 或 Hook 的 snapshot 为播放真相。
导出边界
从 react-audio-native 导入:
| 导出 | 类型 | 用途 |
|---|---|---|
AudioPlayer | value | forwardRef React 播放器组件 |
useAudioPlayer() | function | callback ref 驱动的 Headless Hook |
detectAudioCapabilities() | function | 无 UA 的运行时能力检测 |
formatMediaTime() | function | 把秒数格式化为 mm:ss 或 hh:mm:ss |
AudioPlayerProps、AudioPlayerSize、UseAudioPlayerResult | type | React 适配层类型 |
| core 共享类型 | type | track、snapshot、error、Bridge、handle 等类型 |
完整 type-only 导出为:
import type {
AudioPlayerProps,
AudioPlayerSize,
UseAudioPlayerResult,
AudioBridgeEvent,
AudioControllerOptions,
AudioInput,
AudioPlayerBridge,
AudioPlayerError,
AudioPlayerErrorCode,
AudioPlayerHandle,
AudioRuntimeCapabilities,
AudioSnapshot,
AudioSource,
AudioSourceInput,
AudioTrack,
BufferedRange,
PlaybackState,
PreloadMode,
RepeatMode,
} from 'react-audio-native'
createAudioController()、normalizeInput()、normalizeSources() 和
AudioControllerError 只从 @trsoliu/audio-core 导出,React 包不会重新导出这些底层
value:
import { createAudioController } from '@trsoliu/audio-core'
import { AudioPlayer, useAudioPlayer } from 'react-audio-native'
输入模型
AudioSource 与 AudioSourceInput
interface AudioSource {
src: string
type?: string
}
type AudioSourceInput = string | AudioSource
src 可以是一个 URL、一个带 MIME type 的 source,或它们的有序数组。空 URL 会被
忽略,同一 URL 只保留第一次出现的位置。提供 type 时,内核优先选择
HTMLMediaElement.canPlayType() 支持的 source;媒体失败后继续尝试后续 source。
AudioTrack
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 调用方维护的稳定曲目标识 |
sources | AudioSource | readonly AudioSource[] | 是 | 一个或多个有序回退源 |
title | string | 否 | 标题与 Media Session metadata |
artist | string | 否 | 艺术家与 Media Session metadata |
album | string | 否 | 专辑与 Media Session metadata |
artwork | MediaImage[] | 否 | 默认封面与 Media Session artwork |
downloadName | string | 否 | 覆盖组件级下载文件名 |
peaks | readonly number[] | 否 | 调用方提供的波形峰值;播放器不会下载音频计算峰值 |
非空 tracks 优先于 src。无可用 source 的 track 会被过滤;单独的 src 会被规范化
为 id: 'audio-source' 的单曲播放列表。
<AudioPlayer> props
AudioPlayerProps 继承全部 AudioControllerOptions,再增加展示与 callback props。
输入与播放
| prop | 类型 | 默认值 | 行为 |
|---|---|---|---|
tracks | readonly AudioTrack[] | — | 播放列表;非空时覆盖 src |
src | AudioSourceInput | readonly AudioSourceInput[] | — | 单曲或多格式回退输入 |
autoplay | boolean | false | 挂载或换源后尝试播放;浏览器阻止时报告可恢复的 AUTOPLAY_BLOCKED |
preload | 'none' | 'metadata' | 'auto' | 'metadata' | 写入原生 audio 的 preload 策略 |
volume | number | 1 | 调用方必须传有限值;有限值限制到 0–1 |
muted | boolean | false | 静音状态 |
playbackRate | number | 1 | 调用方必须传有限值;有限值限制到 0.25–4 |
repeatMode | 'off' | 'one' | 'all' | 'off' | 不循环、单曲循环或列表循环 |
waitBuffer | boolean | true | true 允许跳到已知 duration 内任意位置;false 把 seek 限制到当前最大缓冲终点 |
协作与宿主集成
| prop | 类型 | 默认值 | 行为 |
|---|---|---|---|
exclusive | boolean | false | 仅当两个实例都开启且 group 相同时,新播放实例才暂停另一个实例 |
group | string | 'default' | 互斥分组;空白字符串按 'default' 处理 |
mediaSession | boolean | false | 显式开启锁屏 metadata、播放状态与媒体按键处理 |
bridge | AudioPlayerBridge | null | null | 协议无关的宿主事件出口;传 null 或移除 prop 会解绑旧宿主 |
展示与组合
| prop | 类型 | 默认值 | 行为 |
|---|---|---|---|
nativeControls | boolean | false | 强制使用浏览器原生 controls;能力不足时也自动降级为原生 controls |
showCurrentTime | boolean | true | 显示当前时间与 duration |
showVolume | boolean | true | 显示静音按钮和音量滑杆 |
showDownload | boolean | true | 有 source 时显示下载链接;浏览器决定是否支持 download 和跨域下载 |
downloadName | string | '' | 默认下载文件名;track 的同名字段优先 |
size | 'small' | 'default' | 'large' | 'default' | 写入根节点 data-size,切换包内尺寸样式 |
className | string | — | 追加到根节点的 audio-native class 后 |
hint | ReactNode | 'No playable audio source.' | 没有可播放 track 时显示 |
artwork | ReactNode | ((track) => ReactNode) | 默认图片 | 固定 artwork 节点,或根据当前 track 渲染 |
beforeControls | ReactNode | — | 自定义 controls 内、标准控件之前渲染 |
afterControls | ReactNode | — | 自定义 controls 内、标准控件之后渲染 |
beforeControls 和 afterControls 是普通 ReactNode,不是 render props;需要命令或状态
时使用 forwarded ref 或 Headless Hook。三个组合点在原生 controls 模式下只有 artwork
仍会渲染,before/after controls 不会渲染。
回调事件
| callback | payload | 触发条件与注意事项 |
|---|---|---|
onStateChange | (snapshot: AudioSnapshot) => void | 每次 PlaybackState 真正变化时调用,包括 loading、buffering 和 error |
onReady | (snapshot: AudioSnapshot) => void | 状态进入 ready 时调用;通常来自暂停时的 canplay,加载阶段的 play() 被拒绝后也会进入这一可重试状态;不是 loadedmetadata 的别名 |
onPlay | (snapshot: AudioSnapshot) => void | 状态进入 playing 时调用;从 buffering 恢复也会再次调用 |
onPause | (snapshot: AudioSnapshot) => void | 状态进入 paused 时调用,包括命令暂停和同组互斥暂停 |
onEnded | (snapshot: AudioSnapshot) => void | 状态进入 ended 时调用;单曲循环、列表自动前进或列表循环时不会先调用终止 callback |
onTimeUpdate | (currentTime: number, snapshot: AudioSnapshot) => void | snapshot 的 currentTime 变化时调用;播放中也会由进度帧同步,频率可能高于原生 timeupdate |
onTrackChange | (track: AudioTrack | null, trackIndex: number) => void | 当前 track 的索引或对象发生变化时调用;初始选择请直接读取 snapshot,不要依赖一次“挂载 callback” |
onError | (error: AudioPlayerError) => void | 出现新的公开错误时调用;多 source 回退只在没有后续可尝试 source 后报告媒体错误 |
<AudioPlayer
tracks={tracks}
onStateChange={(snapshot) => setPlaybackState(snapshot.state)}
onTimeUpdate={(seconds, snapshot) =>
saveProgress(snapshot.track?.id, seconds)
}
onTrackChange={(track, index) => console.log(index, track?.id)}
onError={(error) => console.error(error.code, error.message)}
/>
callbacks 在 React effect 中根据前后 snapshot 调用,不要依赖它们与浏览器 DOM event 的
同一调用栈。AUTOPLAY_BLOCKED 会调用 onError,但 snapshot 通常保持 ready 或
paused,用户仍可通过手势再次调用 play()。
AudioSnapshot
每次通知提供一个新的、顶层冻结的 snapshot:
| 字段 | 类型 | 说明 |
|---|---|---|
state | PlaybackState | 当前状态,见下表 |
track | AudioTrack | null | 当前规范化 track |
trackIndex | number | 当前索引;没有 track 时为 -1 |
currentTime | number | 非负播放位置,单位秒 |
duration | number | null | 有效非负时长;未知、Infinity 或无效时为 null |
buffered | readonly BufferedRange[] | { start, end } 秒区间列表 |
volume | number | 0–1 |
muted | boolean | 是否静音 |
playbackRate | number | 当前倍速 |
repeatMode | RepeatMode | 当前循环模式 |
error | AudioPlayerError | null | 最近公开错误;成功播放或重新加载后清空 |
| state | 含义 |
|---|---|
idle | 尚未附着媒体元素,或当前没有可用输入 |
loading | 已选择 source,等待媒体就绪 |
ready | 暂停且可重试;通常已收到 canplay,也可能刚从加载阶段的播放拒绝恢复 |
playing | 正在播放 |
paused | 已暂停 |
buffering | 播放期间收到 waiting 或 stalled |
ended | 播放列表到达终点且没有继续循环 |
error | 所有可用 source 均失败,或发生其他致命媒体错误 |
结构化错误
interface AudioPlayerError {
code: AudioPlayerErrorCode
message: string
mediaErrorCode?: number
cause?: unknown
}
| code | 常见来源 | 建议处理 |
|---|---|---|
AUTOPLAY_BLOCKED | play() 被浏览器的用户手势策略拒绝 | 显示播放按钮,让用户手势重试 |
SOURCE_NOT_SUPPORTED | 没有可用 source、原生错误码 4,或 audio ref 尚未附着就调用异步命令 | 更换 source、检查 MIME/type,或先挂载 audio |
MEDIA_ABORTED | 原生媒体错误码 1 | 允许重新加载或换源 |
NETWORK | 原生媒体错误码 2 | 提供重试并检查 URL/CORS/网络 |
DECODE | 原生媒体错误码 3 | 换用兼容编码或后备 source |
UNKNOWN | 其他 play() 或媒体失败 | 记录 cause,向用户提供重试或后备路径 |
Imperative ref
import { useRef } from 'react'
import { AudioPlayer, type AudioPlayerHandle } from 'react-audio-native'
export function Player() {
const playerRef = useRef<AudioPlayerHandle>(null)
return (
<>
<AudioPlayer ref={playerRef} src="/episode.mp3" />
<button type="button" onClick={() => void playerRef.current?.play()}>
Play
</button>
</>
)
}
| 方法 | 返回值 | 语义 |
|---|---|---|
play() | Promise<void> | 请求播放;失败时以结构化 AudioControllerError 拒绝 |
pause() | void | 暂停当前元素 |
toggle() | Promise<void> | 根据原生元素的 paused 状态播放或暂停 |
stop() | void | 暂停并跳回 0 秒 |
seekTo(seconds) | void | 忽略非有限值,并把位置限制到合法时长;waitBuffer=false 时不能超过最大缓冲终点 |
skipBy(seconds) | void | 从当前时间相对跳转 |
selectTrack(index) | Promise<void> | 选择合法整数索引;越界时抛出 RangeError,原先在播或 autoplay 时继续播放 |
previous() | Promise<void> | 当前时间大于 3 秒时先回到本曲开头,否则切上一曲;all 可从首曲回绕 |
next() | Promise<void> | 切下一曲;all 可从末曲回绕,否则进入 ended |
setVolume(volume) | void | 要求有限数值,并限制到 0–1 |
setMuted(muted) | void | 更新静音状态 |
setPlaybackRate(rate) | void | 要求有限数值,并限制到 0.25–4 |
setRepeatMode(mode) | void | 更新 'off' | 'one' | 'all' |
getElement() | HTMLAudioElement | null | 获取当前原生元素;SSR 或未挂载时为 null |
useAudioPlayer()
interface UseAudioPlayerResult {
audioRef: RefCallback<HTMLAudioElement>
snapshot: AudioSnapshot
controls: AudioPlayerHandle
}
Hook 只在 callback ref 收到真实 HTMLAudioElement 后创建 controller:
'use client'
import { useAudioPlayer } from 'react-audio-native'
export function HeadlessPlayer() {
const { audioRef, controls, snapshot } = useAudioPlayer({
src: '/episode.mp3',
exclusive: true,
group: 'podcast',
})
return (
<>
<audio ref={audioRef} />
<button type="button" onClick={() => void controls.toggle()}>
{snapshot.state === 'playing' ? 'Pause' : 'Play'}
</button>
</>
)
}
audioRef必须交给一个真实<audio>;传入null时会销毁旧 controller。snapshot是当前不可变值;React 重新渲染由 core subscription 驱动。controls的对象身份稳定。ref 尚未附着时,play、toggle、previous、next和selectTrack返回以SOURCE_NOT_SUPPORTED拒绝的 Promise;同步 setter/pause 为安全 no-op,getElement()返回null。- options 变化会更新当前 controller;输入变化会重置到首个有效 track。
- callback ref 的 attach/detach 负责 StrictMode 重挂载和卸载清理,不残留媒体监听器、 进度帧、互斥注册或 Media Session 所有权。
WebView Bridge Events
组件和 Hook 都接受可选的 bridge:
const bridge = {
emit(event) {
hostTransport.postMessage(JSON.stringify(event))
},
}
<AudioPlayer src="/episode.mp3" bridge={bridge} />
type AudioBridgeEvent =
| { type: 'statechange'; snapshot: AudioSnapshot }
| {
type: 'trackchange'
snapshot: AudioSnapshot
track: AudioTrack | null
trackIndex: number
}
| { type: 'error'; snapshot: AudioSnapshot; error: AudioPlayerError }
statechange只在snapshot.state改变时发送。trackchange在当前 track/index 改变时发送;附着已有选中 track 的 controller 时也会 向当前 bridge 发送一次。error只在出现新的公开错误对象时发送。bridge.emit()抛出的宿主异常会被隔离,不会中断播放。- 删除
bridgeprop、改为undefined/null,或在 Hook options 中传null,都会在同一 次换源前解绑旧宿主。
Bridge 不规定 WKWebView、Android JavascriptInterface 或 ArkWeb 的通道、序列化、鉴权
协议;这些属于宿主应用边界。
能力检测与降级
detectAudioCapabilities(element?) 返回:
interface AudioRuntimeCapabilities {
customControls: boolean
download: boolean
mediaSession: boolean
nativeHls: boolean
pointerEvents: boolean
touch: boolean
}
SSR 中所有字段为 false。组件只依据能力决定自定义/原生 controls,不解析 UA。Media
Session 是 opt-in;原生 HLS 可透传,但不捆绑 HLS 引擎。
具体声明以 npm 包 .d.ts 为最终机器契约。发布门禁还会验证 React 18/19、ESM、CJS、
Next SSR 导入和 npm tarball 内容。