6.3 KiB
6.3 KiB
Media
Camera
- Hide navigation headers when there's a full screen camera
- Ensure to flip the camera with
mirrorto emulate social apps - Use liquid glass buttons on cameras
- Icons:
arrow.triangle.2.circlepath(flip),photo(gallery),bolt(flash) - Eagerly request camera permission
- Lazily request media library permission
import React, { useRef, useState } from "react";
import { View, TouchableOpacity, Text, Alert } from "react-native";
import { CameraView, CameraType, useCameraPermissions } from "expo-camera";
import * as MediaLibrary from "expo-media-library";
import * as ImagePicker from "expo-image-picker";
import * as Haptics from "expo-haptics";
import { SymbolView } from "expo-symbols";
import { PlatformColor } from "react-native";
import { GlassView } from "expo-glass-effect";
import { useSafeAreaInsets } from "react-native-safe-area-context";
function Camera({ onPicture }: { onPicture: (uri: string) => Promise<void> }) {
const [permission, requestPermission] = useCameraPermissions();
const cameraRef = useRef<CameraView>(null);
const [type, setType] = useState<CameraType>("back");
const { bottom } = useSafeAreaInsets();
if (!permission?.granted) {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: PlatformColor("systemBackground"),
}}
>
<Text style={{ color: PlatformColor("label"), padding: 16 }}>
Camera access is required
</Text>
<GlassView
isInteractive
tintColor={PlatformColor("systemBlue")}
style={{ borderRadius: 12 }}
>
<TouchableOpacity onPress={requestPermission} style={{ padding: 12, borderRadius: 12 }}>
<Text style={{ color: "white" }}>Grant Permission</Text>
</TouchableOpacity>
</GlassView>
</View>
);
}
const takePhoto = async () => {
await Haptics.selectionAsync();
if (!cameraRef.current) return;
const photo = await cameraRef.current.takePictureAsync({ quality: 0.8 });
await onPicture(photo.uri);
};
const selectPhoto = async () => {
await Haptics.selectionAsync();
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: "images",
allowsEditing: false,
quality: 0.8,
});
if (!result.canceled && result.assets?.[0]) {
await onPicture(result.assets[0].uri);
}
};
return (
<View style={{ flex: 1, backgroundColor: "black" }}>
<CameraView ref={cameraRef} mirror style={{ flex: 1 }} facing={type} />
<View
style={{
position: "absolute",
left: 0,
right: 0,
bottom: bottom,
gap: 16,
alignItems: "center",
}}
>
<GlassView isInteractive style={{ padding: 8, borderRadius: 99 }}>
<TouchableOpacity
onPress={takePhoto}
style={{ width: 64, height: 64, borderRadius: 99, backgroundColor: "white" }}
/>
</GlassView>
<View
style={{ flexDirection: "row", justifyContent: "space-around", paddingHorizontal: 8 }}
>
<GlassButton onPress={selectPhoto} icon="photo" />
<GlassButton
onPress={() => setType((t) => (t === "back" ? "front" : "back"))}
icon="arrow.triangle.2.circlepath"
/>
</View>
</View>
</View>
);
}
Audio Playback
Use expo-audio not expo-av:
import { useAudioPlayer } from "expo-audio";
const player = useAudioPlayer({ uri: "https://stream.nightride.fm/rektory.mp3" });
<Button title="Play" onPress={() => player.play()} />;
Audio Recording (Microphone)
import {
useAudioRecorder,
AudioModule,
RecordingPresets,
setAudioModeAsync,
useAudioRecorderState,
} from "expo-audio";
import { useEffect } from "react";
import { Alert, Button } from "react-native";
function App() {
const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
const recorderState = useAudioRecorderState(audioRecorder);
const record = async () => {
await audioRecorder.prepareToRecordAsync();
audioRecorder.record();
};
const stop = () => audioRecorder.stop();
useEffect(() => {
(async () => {
const status = await AudioModule.requestRecordingPermissionsAsync();
if (status.granted) {
setAudioModeAsync({ playsInSilentMode: true, allowsRecording: true });
} else {
Alert.alert("Permission to access microphone was denied");
}
})();
}, []);
return (
<Button
title={recorderState.isRecording ? "Stop" : "Start"}
onPress={recorderState.isRecording ? stop : record}
/>
);
}
Video Playback
Use expo-video not expo-av:
import { useVideoPlayer, VideoView } from "expo-video";
import { useEvent } from "expo";
const videoSource = "https://example.com/video.mp4";
const player = useVideoPlayer(videoSource, (player) => {
player.loop = true;
player.play();
});
const { isPlaying } = useEvent(player, "playingChange", { isPlaying: player.playing });
<VideoView player={player} fullscreenOptions={{}} allowsPictureInPicture />;
VideoView options:
allowsPictureInPicture: booleancontentFit: 'contain' | 'cover' | 'fill'nativeControls: booleanplaysInline: booleanstartsPictureInPictureAutomatically: boolean
Saving Media
import * as MediaLibrary from "expo-media-library";
const { granted } = await MediaLibrary.requestPermissionsAsync();
if (granted) {
await MediaLibrary.saveToLibraryAsync(uri);
}
Saving Base64 Images
MediaLibrary.saveToLibraryAsync only accepts local file paths. Save base64 strings to disk first:
import { File, Paths } from "expo-file-system/next";
function base64ToLocalUri(base64: string, filename?: string) {
if (!filename) {
const match = base64.match(/^data:(image\/[a-zA-Z]+);base64,/);
const ext = match ? match[1].split("/")[1] : "jpg";
filename = `generated-${Date.now()}.${ext}`;
}
if (base64.startsWith("data:")) base64 = base64.split(",")[1];
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(new ArrayBuffer(len));
for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
const f = new File(Paths.cache, filename);
f.create({ overwrite: true });
f.write(bytes);
return f.uri;
}