managarten/memoro/apps/mobile/EXPO_AUDIO_MIGRATION.md
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

7.4 KiB

expo-av to expo-audio Migration Guide

Overview

This guide documents the migration from expo-av to expo-audio completed as part of the Expo SDK 54 upgrade to address Android 16 compatibility issues and deprecation warnings.

Background

Why We Migrated

  1. Deprecation: expo-av was deprecated in Expo SDK 54
  2. Android 16 Issues: Recording functionality broken on Android 16 devices
  3. Performance: expo-audio offers better performance and smaller bundle size
  4. Future Support: expo-audio is actively maintained and developed

Timeline

  • Date: January 2025
  • Expo SDK: 53 → 54
  • React Native: 0.79.2 → 0.81.4
  • expo-av: 15.1.4 → removed
  • expo-audio: not used → 1.0.13

Migration Steps

1. Package Installation

# Remove expo-av
npm uninstall expo-av

# Install expo-audio
npm install expo-audio@~1.0.13

# Clean and reinstall
npx expo install --fix

2. Import Changes

Recording

// Before (expo-av)
import { Audio } from 'expo-av';

// After (expo-audio)
import {
  AudioRecorder,
  RecordingPresets,
  setAudioModeAsync,
  requestRecordingPermissionsAsync,
  getRecordingPermissionsAsync
} from 'expo-audio';

Playback

// Before (expo-av)
import { Audio } from 'expo-av';

// After (expo-audio)
import {
  AudioPlayer,
  createAudioPlayer,
  setAudioModeAsync
} from 'expo-audio';

3. API Changes

Recording API

Starting Recording
// Before (expo-av)
const recording = new Audio.Recording();
await recording.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
await recording.startAsync();

// After (expo-audio)
const recorder = new AudioRecorder(RecordingPresets.HIGH_QUALITY);
await recorder.prepareToRecordAsync();
recorder.record(); // Note: synchronous, not async
Stopping Recording
// Before (expo-av)
await recording.stopAndUnloadAsync();
const uri = recording.getURI();

// After (expo-audio)
await recorder.stop();
const uri = recorder.uri; // Direct property access
Pause/Resume
// Before (expo-av)
await recording.pauseAsync();
await recording.startAsync(); // Resume

// After (expo-audio)
recorder.pause(); // Synchronous
recorder.record(); // Resume (same as start)

Playback API

Creating Player
// Before (expo-av)
const { sound } = await Audio.Sound.createAsync(
  { uri },
  { progressUpdateIntervalMillis: 100 }
);

// After (expo-audio)
const player = createAudioPlayer(uri);
// Note: No progress update interval option; use polling
Playback Control
// Before (expo-av)
await sound.playAsync();
await sound.pauseAsync();
await sound.stopAsync();
await sound.setPositionAsync(positionMillis);
await sound.unloadAsync();

// After (expo-audio)
await player.play();
await player.pause();
await player.stop();
player.currentTime = positionSeconds; // Note: seconds, not milliseconds
player.release(); // Synchronous cleanup
Status Updates
// Before (expo-av)
sound.setOnPlaybackStatusUpdate((status) => {
  if (status.isLoaded) {
    console.log(status.positionMillis, status.durationMillis);
  }
});

// After (expo-audio)
// No built-in status updates; use polling
setInterval(() => {
  console.log(player.currentTime, player.duration); // In seconds
  console.log(player.playing); // Boolean
}, 100);

Audio Mode Configuration

// Before (expo-av)
await Audio.setAudioModeAsync({
  allowsRecordingIOS: true,
  playsInSilentModeIOS: true,
  staysActiveInBackground: true,
  interruptionModeIOS: InterruptionModeIOS.DoNotMix,
  interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
  shouldDuckAndroid: true,
  playThroughEarpieceAndroid: false,
});

// After (expo-audio)
await setAudioModeAsync({
  allowsRecording: true,
  playsInSilentMode: true,
  shouldPlayInBackground: true,
  interruptionMode: 'doNotMix', // String literals instead of enums
  // Note: Some options like shouldDuckAndroid are not available
});

Permissions

// Before (expo-av)
const { status } = await Audio.requestPermissionsAsync();
const { status } = await Audio.getPermissionsAsync();

// After (expo-audio)
const { granted } = await requestRecordingPermissionsAsync();
const { granted } = await getRecordingPermissionsAsync();

Files Modified

Core Recording Services

  • features/audioRecording/audioRecording.service.ts
  • features/audioRecording/audioRecording.service.android.ts
  • features/audioRecording/audioRecording.service.ios.ts
  • features/audioRecording/audioRecording.service.web.ts
  • features/audioRecording/audioRecording.types.ts

Audio Player

  • features/audioPlayer/useAudioPlayer.ts
  • features/audioPlayer/store/audioPlaybackStore.ts

Storage & Utilities

  • features/storage/fileStorage.service.ts
  • features/storage/fileStorage.service.web.ts
  • utils/mediaUtils.ts

Sound Effects

  • features/audioRecording/services/recordingSoundManager.ts

Configuration

  • package.json
  • app.json (plugin configuration)

Platform-Specific Considerations

Android 16

  • Added foreground state verification before recording
  • Implemented AppState monitoring for background restrictions
  • Added explicit error handling for permission denials

iOS

  • Maintained compatibility with existing iOS audio session configuration
  • No significant changes required for iOS implementation

Web

  • Updated to use Web Audio API compatible methods
  • Maintained fallback for permissions API

Known Issues & Workarounds

1. Zero-byte Audio Files (Expo SDK 54)

Issue: Some Android devices create zero-byte audio files Reference: GitHub issue #39646 Workaround: Added logging and validation after recording stops

2. Missing Status Updates

Issue: No built-in playback status updates like expo-av Solution: Implemented polling mechanism with setInterval

3. Time Units Difference

Issue: expo-audio uses seconds, expo-av used milliseconds Solution: Added conversion where necessary (÷ 1000 for ms → s)

Benefits Achieved

  1. Android 16 Compatibility: Recording works on latest Android devices
  2. Smaller Bundle: Reduced app size by ~200KB
  3. Better Performance: Faster audio initialization and lower memory usage
  4. Simpler API: More intuitive method names and patterns
  5. Future-Proof: Active development and support from Expo team

Testing Checklist

  • Audio recording starts successfully
  • Recording can be paused and resumed
  • Recording stops and saves properly
  • Audio playback works correctly
  • Seek/scrub functionality works
  • Volume controls function properly
  • Background recording (iOS)
  • Permissions are requested correctly
  • Sound effects play correctly
  • Multiple audio instances don't conflict

Rollback Plan

If issues arise, rollback by:

  1. Revert package.json changes
  2. Run npm install expo-av@~15.1.4
  3. Revert all file changes listed above
  4. Run npx expo install --fix
  5. Clean build folders and rebuild

Resources

Contact

For questions about this migration, please refer to the project maintainers or create an issue in the project repository.