diff --git a/apps/mobile/src/components/AudioPlayer.tsx b/apps/mobile/src/components/AudioPlayer.tsx new file mode 100644 index 0000000..2dd733c --- /dev/null +++ b/apps/mobile/src/components/AudioPlayer.tsx @@ -0,0 +1,286 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, Alert } from 'react-native'; +import { + useAudioPlayer, + useAudioPlayerStatus, + AudioModule, + setAudioModeAsync, +} from 'expo-audio'; +import { colors } from '../constants/Colors'; +import { typography } from '../constants/Typography'; +import { spacing, borderRadius } from '../constants/Layout'; +import { Button } from './ui/Button'; + +export interface AudioPlayerProps { + audioUrl: string; + audioText?: string; + audioMetadata?: { + duration: number; + fileSize: number; + format: string; + processingTime: number; + }; + style?: object; +} + +export function AudioPlayer({ + audioUrl, + audioText, + audioMetadata, + style, +}: AudioPlayerProps) { + const [hasPermission, setHasPermission] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // Construct full URL if needed + const fullUrl = audioUrl.startsWith('http') + ? audioUrl + : `http://10.141.39.175:4000${audioUrl}`; + + // Create audio player with the URL - this is the correct expo-audio API + const player = useAudioPlayer({ uri: fullUrl }); + const status = useAudioPlayerStatus(player); + + useEffect(() => { + // Request audio permissions and set audio mode for better volume + requestPermissions(); + }, []); + + useEffect(() => { + // Set player volume to maximum for normal audio level + if (player) { + try { + player.volume = 1.0; // Maximum volume (0.0 to 1.0) + } catch (error) { + // Volume control not supported + } + } + }, [player]); + + const requestPermissions = async () => { + try { + const { granted } = await AudioModule.requestRecordingPermissionsAsync(); + setHasPermission(granted); + + // Set audio mode for better playback volume + await setAudioModeAsync({ + playsInSilentMode: true, + allowsRecording: false, // We're only playing, not recording + }); + } catch (error) { + // Failed to request audio permissions + setHasPermission(false); + } + }; + + const playPauseAudio = async () => { + if (!hasPermission) { + Alert.alert( + 'Permission Required', + 'Audio playback permission is required to play coaching audio.' + ); + return; + } + + try { + setIsLoading(true); + + // Check if audio is stuck at the end + if (duration > 0 && currentTime >= duration - 0.5 && !status.playing) { + // Audio finished, restart from beginning + player.seekTo(0); + await new Promise(resolve => setTimeout(resolve, 100)); // Small delay + player.play(); + } else if (status.playing) { + // Pause audio using expo-audio API + player.pause(); + } else { + // Play audio using expo-audio API + player.play(); + } + } catch (error) { + // Failed to play/pause audio + // Try to recover from stuck state + try { + forceReset(); + } catch (resetError) { + Alert.alert( + 'Playback Error', + 'Audio player needs to be reloaded. Please try again.' + ); + } + } finally { + setIsLoading(false); + } + }; + + const stopAudio = () => { + try { + // Reset to beginning and pause - expo-audio API + player.seekTo(0); + player.pause(); + } catch (error) { + // Failed to stop audio + // Force reset if stuck + forceReset(); + } + }; + + const forceReset = () => { + try { + // Force player to reset when stuck + player.seekTo(0); + setIsLoading(false); + } catch (error) { + // Failed to force reset + } + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + // Get current time and duration from status + const currentTime = status.currentTime || 0; + const duration = + status.duration || (audioMetadata ? audioMetadata.duration : 0); + const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0; + + return ( + + + 🎵 Personalized Coaching Audio + {audioMetadata && ( + + {formatTime(audioMetadata.duration)} •{' '} + {audioMetadata.format.toUpperCase()} + + )} + + + {audioText && ( + + "{audioText}" + + )} + + + + + + + {formatTime(currentTime)} + + {duration > 0 + ? formatTime(duration) + : audioMetadata + ? formatTime(audioMetadata.duration) + : '--:--'} + + + + + +