Audio
This enhancer adds audiobook playback support to the reader. It detects audio spine items in the manifest, renders them using a dedicated audio renderer, and provides a reactive playback API with play/pause, seeking, track selection, and a real-time frequency visualizer. It uses the standard HTML5 Audio element internally and integrates with the reader's pagination and navigation.
Getting started
Section titled “Getting started”npm install @prose-reader/enhancer-audioConnect the enhancer to your reader:
import { createReader } from "@prose-reader/core"import { audioEnhancer } from "@prose-reader/enhancer-audio"
const createAppReader = audioEnhancer(createReader)
const reader = createAppReader({ manifest })The enhancer has no required configuration. When composing with other enhancers, wrap createReader as usual:
import { gesturesEnhancer } from "@prose-reader/enhancer-gestures"import { audioEnhancer } from "@prose-reader/enhancer-audio"
const createAppReader = gesturesEnhancer( audioEnhancer( createReader ))How It Works
Section titled “How It Works”When the reader is created, the enhancer filters the manifest's spine items to build a list of audio tracks. For each audio spine item, a dedicated AudioRenderer is used instead of the default HTML renderer. This renderer produces a minimal placeholder page (pre-paginated, no iframe) since audio content has no visual document to display.
The first audio track at the pagination boundaries is the current track, including the one the book opens on: its source starts loading without any user action. Navigating off audio content stops playback and clears the current track.
Playback is handled by a single internal HTMLAudioElement. Track resources are resolved through the reader's resource handler, supporting blob URLs, direct URLs, and HTTP responses. When a track ends, the enhancer automatically advances to the next track in the current pagination window or navigates to the next page.
The API is under the audio namespace.
.play()
Section titled “.play()”Start playback of the current track.
reader.audio.play().pause()
Section titled “.pause()”Pause playback.
reader.audio.pause().toggle()
Section titled “.toggle()”Toggle between play and pause.
reader.audio.toggle().setCurrentTime(value)
Section titled “.setCurrentTime(value)”Seek to a specific position in the current track.
reader.audio.setCurrentTime(30) // seek to 30 seconds.select(trackId, options?)
Section titled “.select(trackId, options?)”Select and load a specific audio track by its spine item id.
reader.audio.select("track-3")Options:
type SelectAudioTrackOptions = { play?: boolean navigate?: boolean}navigate: When notfalse, the reader will navigate to the spine item before loading the track. Defaults to navigating.play: Whether to start playback after selection.
reader.audio.select("track-3", { play: true, navigate: true }).isAudioRenderer(renderer)
Section titled “.isAudioRenderer(renderer)”Type guard to check whether a given DocumentRenderer is an AudioRenderer.
if (reader.audio.isAudioRenderer(renderer)) { // renderer is AudioRenderer}.state / .state$
Section titled “.state / .state$”Access the current playback state synchronously or as an RxJS observable.
type AudioEnhancerState = { tracks: AudioTrack[] currentTrack: AudioTrack | undefined isPlaying: boolean isLoading: boolean hasError: boolean currentTime: number duration: number | undefined}
type AudioTrack = { id: string href: string index: number mediaType: string | undefined}Synchronous access:
const { isPlaying, currentTrack, currentTime, duration } = reader.audio.stateReactive access:
reader.audio.state$.subscribe((state) => { console.log(state.isPlaying, state.currentTime)}).visualizer / .visualizer$
Section titled “.visualizer / .visualizer$”Access the audio frequency visualizer state synchronously or as an observable. The visualizer uses the Web Audio API to provide real-time frequency levels that can drive visual representations like waveforms or spectrum bars.
type AudioVisualizerState = { levels: number[] isActive: boolean trackId: string | undefined}levels: Array of frequency bin values (0–1 range) suitable for rendering a waveform or bar visualizer.isActive: Whether the visualizer is currently analyzing audio.trackId: The id of the track being visualized.
reader.audio.visualizer$.subscribe(({ levels, isActive }) => { if (isActive) { renderWaveform(levels) }}).visibleTrackIds$
Section titled “.visibleTrackIds$”Observable that emits the audio track ids at the current pagination boundaries.
reader.audio.visibleTrackIds$.subscribe((trackIds) => { console.log("Tracks at current page boundaries:", trackIds)})Playback Behavior
Section titled “Playback Behavior”Track Resolution
Section titled “Track Resolution”Audio resources are resolved using the reader's resource handler. The enhancer supports multiple resource types: direct URLs, blob URLs from fetched responses, and track href fallbacks. Resolved URLs are cached to avoid redundant fetching.
End of Track
Section titled “End of Track”When a track finishes:
- If there is a next track within the current pagination window, it is loaded automatically.
- Otherwise, the reader navigates to the next page via
navigation.goToRightOrBottomSpineItem().
Error Handling
Section titled “Error Handling”If the audio element refuses to play a track, because its source cannot be decoded or the browser's autoplay policy blocks it, the state is updated with hasError: true and playback returns to paused (isPlaying: false). Nothing is retried automatically: the next play(), or selecting another track, clears the flag and tries again. A play request cut short by pause() or by switching tracks is not an error.
Auto-Selection
Section titled “Auto-Selection”When the user navigates pages, the enhancer automatically selects the first audio track visible in the new pagination window. If the user was playing before navigation, playback intent is preserved.
React Integration
Section titled “React Integration”If you are using @prose-reader/react-reader, there are hooks available to consume the audio state:
import { useAudioState } from "@prose-reader/react-reader"
const MyAudioControls = () => { const audioState = useAudioState()
if (!audioState) return null
return ( <div> <button onClick={() => reader.audio.toggle()}> {audioState.isPlaying ? "Pause" : "Play"} </button> <span> {audioState.currentTime}s / {audioState.duration}s </span> </div> )}You can check whether the reader has the audio enhancer using the provided type guard:
import { hasAudioEnhancer } from "@prose-reader/react-reader"
if (hasAudioEnhancer(reader)) { reader.audio.play()}Supported Formats
Section titled “Supported Formats”The enhancer recognizes spine items as audio based on MIME type (audio/*) or file extension:
| Extension | Format |
|---|---|
| mp3 | MPEG Audio Layer 3 |
| m4a | MPEG-4 Audio |
| m4b | MPEG-4 Audiobook |
| aac | Advanced Audio Coding |
| ogg | Ogg Vorbis |
| oga | Ogg Audio |
| wav | Waveform Audio |
| flac | Free Lossless Audio Codec |
| opus | Opus |