Audio Player
System Intent
- What this is: A full-screen audio playback component (
main/app/components/memory/AudioPlayer.tsx) that renders inside MemoryViewerModal when the active memory has type: "audio". It fetches audio metadata and a presigned S3 URL via the audio API, then drives playback through expo-av (Audio.Sound). Controls (play/pause, scrubber, mute) are overlaid on a black background and auto-hide after 3 s of inactivity. Processing and error states are surfaced with overlays. Playback state management is delegated to the useMediaPlayer hook. When rendered inside MemoryViewerModal, AudioPlayer is placed above a TranscriptDisplay component that shows the scrollable transcript for the same memory.
Mermaid Diagram
flowchart TD
Mount[AudioPlayer mounts] --> InitSound[Audio.setAudioModeAsync + new Audio.Sound]
Mount --> FetchMeta[getMemoryAudioMetadata]
FetchMeta -->|status pending/failed| ProcOverlay[ProcessingOverlay shown, play disabled]
FetchMeta -->|status complete| FetchURL[getMemoryAudio → presigned_url]
FetchURL --> LoadAsync[sound.loadAsync uri]
LoadAsync --> Ready[Player ready]
Ready -->|tap play| PlayAsync[sound.playAsync]
PlayAsync -->|onPlaybackStatusUpdate| PollState[position / duration / isPlaying updated]
PollState -->|didJustFinish| ResetPos[setPosition duration, show controls]
Ready -->|tap scrubber| SetPos[sound.setPositionAsync]
Ready -->|tap mute| SetMuted[sound.setIsMutedAsync]
Flows
Flow: audioInitialize
- Core files:
main/app/components/memory/AudioPlayer.tsx, main/app/lib/api/memory/audio.ts - Test files: none currently
Types
AudioPlayerProps {
memoryId: string (required)
memory?: MemoryFeedItem
}
GetMemoryAudioMetadataResponse {
duration_seconds: number
processing_status: "complete" | "pending" | "failed"
error?: string
}
GetMemoryAudioResponse {
presigned_url: string
}
Paths
| path | input | output | path-type | notes |
audioInitialize.ready | memoryId, processing_status = complete | presigned_url loaded into Audio.Sound | happy path | |
audioInitialize.pending | memoryId, processing_status = pending | ProcessingOverlay "Processing…", play disabled | happy path | |
audioInitialize.failed | memoryId, processing_status = failed | ProcessingOverlay "Processing failed" (red overlay) | error | |
audioInitialize.metadataError | API throws | error = "Could not load audio metadata" | error | |
audioInitialize.urlError | getMemoryAudio throws | error = "Could not load audio" | error | |
audioInitialize.loadError | loadAsync throws | error = "Failed to load audio file" | error | |
Pseudocode
mount:
Audio.setAudioModeAsync({ playsInSilentModeIOS: true, staysActiveInBackground: false, shouldDuckAndroid: true })
sound = new Audio.Sound()
sound.setOnPlaybackStatusUpdate(status =>
setPosition(status.positionMillis / 1000)
setDuration(status.durationMillis / 1000)
setIsPlaying(status.isPlaying)
if status.didJustFinish:
setIsPlaying(false); setPosition(duration); showControlsBriefly()
)
metadata = await getMemoryAudioMetadata({ memory_id: memoryId })
setDuration(metadata.duration_seconds)
setProcessingStatus(metadata.processing_status)
if metadata.processing_status === "complete":
data = await getMemoryAudio({ memory_id: memoryId })
setAudioUrl(data.presigned_url)
when audioUrl set:
sound.loadAsync({ uri: audioUrl }, { shouldPlay: false })
unmount:
sound.unloadAsync()
Flow: audioPlayback
- Core files:
main/app/components/memory/AudioPlayer.tsx, main/app/lib/hooks/useMediaPlayer.ts - Test files: none currently
Paths
| path | input | output | path-type | notes |
audioPlayback.play | tap play button (audioUrl loaded) | sound.playAsync(), isPlaying = true | happy path | |
audioPlayback.pause | tap pause button | sound.pauseAsync(), isPlaying = false | happy path | |
audioPlayback.scrub | tap scrubber track at ratio r | sound.setPositionAsync(r * duration * 1000) | happy path | position clamped to [0, duration] |
audioPlayback.mute | tap mute button | sound.setIsMutedAsync(true) | happy path | |
audioPlayback.unmute | tap mute button again | sound.setIsMutedAsync(false) | happy path | |
audioPlayback.playbackError | playAsync/pauseAsync throws | error = "Playback control failed" | error | |
Pseudocode
// useEffect on [isPlaying, audioUrl]
if isPlaying: sound.playAsync()
else: sound.pauseAsync()
// useEffect on [isMuted]
sound.setIsMutedAsync(isMuted)
// useEffect on [position] — handles scrub
sound.setPositionAsync(position * 1000)
// Scrubber tap handler
ratio = locationX / scrubberTrackWidth // clamped [0,1]
setPosition(ratio * duration)
showControlsBriefly()
Flow: audioControlsVisibility
- Core files:
main/app/components/memory/AudioPlayer.tsx, main/app/lib/hooks/useMediaPlayer.ts - Test files: none currently
Paths
| path | input | output | path-type | notes |
audioControlsVisibility.tapToHide | tap while controls visible | controls hidden, timer cleared | happy path | via handleMediaTap |
audioControlsVisibility.tapToShow | tap while controls hidden | controls shown, auto-hide after 3 s | happy path | via handleMediaTap |
audioControlsVisibility.playEnd | didJustFinish | showControlsBriefly() — controls shown briefly | happy path | |
Logs
| Source | Location |
| React Native | Metro / device console |
Deployment
- Mechanism:
local only - Deploy command:
cd main/app && npx expo start
- Notes: Expo managed workflow. Uses
expo-av for audio playback (Audio.Sound). Audio mode is set with playsInSilentModeIOS: true so audio plays even when the iOS silent switch is on.