Skip to content

Audio Player

Metadata

  • System type: library

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.