fix(mobile): migrate expo-av to expo-video and expo-audio

expo-av has no SDK 56 compatible release, and its EXEventEmitter.h
interface was removed from expo-modules-core, so the iOS release
archive fails compiling Pods/EXAV. Move video playback to
expo-video's useVideoPlayer/VideoView, and audio playback and
recording to expo-audio's createAudioPlayer/AudioModule.AudioRecorder,
preserving existing controls, mute/loop/resize behavior, and the
recording audio session configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Fable 5 committed 2026-08-31 14:35:51 +01:00
1 parent 5e28117662
commit b8cb2addff
11 files changed
+524 -387

No files matched your search

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { View, Text, StyleSheet, Platform, TouchableOpacity } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { Audio } from "expo-av";
import { createAudioPlayer, type AudioPlayer } from "expo-audio";
import Animated, {
useSharedValue,
useAnimatedStyle,
@@ -59,7 +59,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
maxDurationSeconds: maxDuration,
});
const [sound, setSound] = useState<Audio.Sound | null>(null);
const [sound, setSound] = useState<AudioPlayer | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [playbackPosition, setPlaybackPosition] = useState(0);
@@ -86,7 +86,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
useEffect(() => {
return () => {
if (sound) {
sound.unloadAsync();
sound.remove();
}
};
}, [sound]);
@@ -113,30 +113,26 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
await triggerHaptic('light');
if (isPlaying && sound) {
await sound.pauseAsync();
sound.pause();
setIsPlaying(false);
return;
}
if (sound) {
await sound.playAsync();
sound.play();
setIsPlaying(true);
return;
}
const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: recordingUri },
{ shouldPlay: true },
(status) => {
if (status.isLoaded) {
setPlaybackPosition(status.positionMillis / 1000);
if (status.didJustFinish) {
setIsPlaying(false);
setPlaybackPosition(0);
}
}
const newSound = createAudioPlayer({ uri: recordingUri });
newSound.addListener('playbackStatusUpdate', (status) => {
setPlaybackPosition(status.currentTime);
if (status.didJustFinish) {
setIsPlaying(false);
setPlaybackPosition(0);
}
);
});
newSound.play();
setSound(newSound);
setIsPlaying(true);
};
@@ -145,7 +141,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
if (!recordingUri) return;
await triggerHaptic('medium');
if (sound) {
await sound.unloadAsync();
sound.remove();
setSound(null);
}
onAnswer('yes', { type: 'audio', media_path: recordingUri, duration_seconds: durationSeconds });
@@ -154,7 +150,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
const handleReRecord = async () => {
await triggerHaptic('light');
if (sound) {
await sound.unloadAsync();
sound.remove();
setSound(null);
}
setIsPlaying(false);
@@ -165,7 +161,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
const handleCancel = async () => {
await triggerHaptic('light');
if (sound) {
await sound.unloadAsync();
sound.remove();
setSound(null);
}
setIsPlaying(false);
@@ -176,7 +172,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
const handleSkip = async () => {
await triggerHaptic('light');
if (sound) {
await sound.unloadAsync();
sound.remove();
setSound(null);
}
onAnswer('skip');