typescript
// components/ProtectedMediaDisplay.tsx
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, Image, Button, StyleSheet, ActivityIndicator, Pressable } from 'react-native';
import { getUrl } from 'aws-amplify/storage';
import { ResizeMode, Video } from 'expo-av'; // Import Expo AV components
import { Audio } from 'expo-av';
interface ProtectedMediaDisplayProps {
path: string;
alt?: string; // Used as accessibility label
style?: object; // Pass custom styles
}
function getMediaType(filePath: string): 'image' | 'video' | 'audio' | 'unknown' {
// (Keep the same getMediaType function from the web version)
const extension = filePath.split('.').pop()?.toLowerCase();
if (!extension) return 'unknown';
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) return 'image'; // Removed svg for simplicity
if (['mp4', 'webm', 'mov'].includes(extension)) return 'video'; // Simplified list
if (['mp3', 'wav', 'm4a', 'aac'].includes(extension)) return 'audio'; // Simplified list
return 'unknown';
}
export default function ProtectedMediaDisplay({ path, alt = "Protected Media", style }: ProtectedMediaDisplayProps) {
const [mediaUrl, setMediaUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mediaType, setMediaType] = useState<'image' | 'video' | 'audio' | 'unknown'>('unknown');
// Specific state/refs for media players
const videoRef = useRef<Video>(null);
const [sound, setSound] = useState<Audio.Sound | null>(null);
const [isPlayingAudio, setIsPlayingAudio] = useState(false);
useEffect(() => {
// --- Cleanup audio on unmount or path change ---
return () => {
sound?.unloadAsync();
};
}, [sound]);
useEffect(() => {
setMediaUrl(null); setLoading(true); setError(null); setMediaType('unknown'); setIsPlayingAudio(false);
// Ensure previous sound is unloaded if path changes
sound?.unloadAsync().then(() => setSound(null)).catch(e => console.log("Error unloading previous sound:", e));
if (!path) {
setError('No media path provided.'); setLoading(false); return;
}
const fetchMediaUrl = async () => {
try {
const type = getMediaType(path);
setMediaType(type);
const result = await getUrl({ path, options: { validateObjectExistence: true } });
setMediaUrl(result.url.toString());
setError(null);
// Pre-configure Audio Session (Good Practice for iOS/Android)
if (type === 'audio') {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
playsInSilentModeIOS: true, // Allow playing even if phone is on silent
staysActiveInBackground: false, // Adjust if background play needed
shouldDuckAndroid: true, // Lower other audio on Android
playThroughEarpieceAndroid: false,
});
}
} catch (err: any) {
console.error(`Error getting URL for ${path}:`, err);
setError(err.name === 'NotFound' ? `Media not found: ${path}` : `Failed to load: ${err.message || err.name}`);
setMediaUrl(null);
} finally {
setLoading(false);
}
};
fetchMediaUrl();
}, [path]); // Re-run if path changes
// --- Audio Playback Logic ---
async function playSound() {
if (!mediaUrl) return;
console.log('Loading Sound');
setIsPlayingAudio(true); // Indicate loading/playing attempt
try {
// Check if sound is already loaded
if(sound) {
console.log('Playing existing sound');
await sound.replayAsync();
} else {
console.log('Loading new sound from:', mediaUrl)
const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: mediaUrl },
{ shouldPlay: true } // Start playing immediately
);
setSound(newSound);
console.log('Playing Sound');
newSound.setOnPlaybackStatusUpdate((status) => {
if (status.isLoaded && status.didJustFinish) {
// Reset button state when finished
setIsPlayingAudio(false);
console.log("Playback finished");
// Optionally unload or prepare for replay
// sound?.setPositionAsync(0); // Rewind
} else if (status.isLoaded && !status.isPlaying && !status.didJustFinish) {
// Handle pausing if pause functionality is added
setIsPlayingAudio(false);
} else if (!status.isLoaded && status.error){
console.error("Playback Error:", status.error);
setError(`Playback Error: ${status.error}`)
setIsPlayingAudio(false);
}
});
}
} catch (error) {
console.error('Failed to load or play sound', error);
setError(`Failed to play audio: ${error}`);
setIsPlayingAudio(false);
}
}
// --- Rendering Logic ---
if (loading) return <ActivityIndicator size="large" color="#007AFF" style={[styles.centered, style]} />;
if (error) return <Text style={[styles.errorText, style]}>{error}</Text>;
if (!mediaUrl) return null;
switch (mediaType) {
case 'image':
return (
<Image
source={{ uri: mediaUrl }}
style={[styles.image, style]} // Combine default and passed styles
resizeMode="contain" // Or 'cover', 'stretch'
accessibilityLabel={alt}
/>
);
case 'video':
return (
<Video
ref={videoRef}
style={[styles.video, style]}
source={{ uri: mediaUrl }}
useNativeControls // Use platform-native video controls
resizeMode={ResizeMode.CONTAIN} // Or COVER
isLooping={false} // Optional
// onPlaybackStatusUpdate={status => setStatus(() => status)} // For custom controls
onError={(e) => { console.error("Video Error:", e); setError(`Video playback error.`);}}
accessibilityLabel={alt}
/>
);
case 'audio':
return (
<View style={[styles.audioContainer, style]}>
<Text style={styles.audioLabel}>{alt}: </Text>
<Button
title={isPlayingAudio ? "Playing..." : "Play Audio"}
onPress={playSound}
disabled={isPlayingAudio && sound != null} // Disable if currently playing/loading
/>
</View>
);
default:
return (
<View style={style}>
<Text style={styles.errorText}>Unsupported type for: {path}</Text>
{/* Optionally add a button to open URL in browser */}
</View>
);
}
}
// --- Basic Styles ---
const styles = StyleSheet.create({
centered: { alignSelf: 'center', marginVertical: 20 },
errorText: { color: 'red', textAlign: 'center', marginVertical: 10 },
image: { width: '100%', aspectRatio: 16/9, marginBottom: 10, alignSelf: 'center' }, // Default aspect ratio
video: { width: '100%', aspectRatio: 16/9, marginBottom: 10, alignSelf: 'center', backgroundColor: 'black'},
audioContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 10, borderWidth: 1, borderColor: '#ccc', borderRadius: 5, marginVertical: 10 },
audioLabel: { marginRight: 10 },
});