Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Official Expo team skill for building native UI in Expo apps, fine-tuned for Opus models and usable with Claude Code or Cursor.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/media.md
1# Media23## Camera45- Hide navigation headers when there's a full screen camera6- Ensure to flip the camera with `mirror` to emulate social apps7- Use liquid glass buttons on cameras8- Icons: `arrow.triangle.2.circlepath` (flip), `photo` (gallery), `bolt` (flash)9- Eagerly request camera permission10- Lazily request media library permission1112```tsx13import React, { useRef, useState } from "react";14import { View, TouchableOpacity, Text, Alert } from "react-native";15import { CameraView, CameraType, useCameraPermissions } from "expo-camera";16import * as MediaLibrary from "expo-media-library";17import * as ImagePicker from "expo-image-picker";18import * as Haptics from "expo-haptics";19import { SymbolView } from "expo-symbols";20import { PlatformColor } from "react-native";21import { GlassView } from "expo-glass-effect";22import { useSafeAreaInsets } from "react-native-safe-area-context";2324function Camera({ onPicture }: { onPicture: (uri: string) => Promise<void> }) {25const [permission, requestPermission] = useCameraPermissions();26const cameraRef = useRef<CameraView>(null);27const [type, setType] = useState<CameraType>("back");28const { bottom } = useSafeAreaInsets();2930if (!permission?.granted) {31return (32<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: PlatformColor("systemBackground") }}>33<Text style={{ color: PlatformColor("label"), padding: 16 }}>Camera access is required</Text>34<GlassView isInteractive tintColor={PlatformColor("systemBlue")} style={{ borderRadius: 12 }}>35<TouchableOpacity onPress={requestPermission} style={{ padding: 12, borderRadius: 12 }}>36<Text style={{ color: "white" }}>Grant Permission</Text>37</TouchableOpacity>38</GlassView>39</View>40);41}4243const takePhoto = async () => {44await Haptics.selectionAsync();45if (!cameraRef.current) return;46const photo = await cameraRef.current.takePictureAsync({ quality: 0.8 });47await onPicture(photo.uri);48};4950const selectPhoto = async () => {51await Haptics.selectionAsync();52const result = await ImagePicker.launchImageLibraryAsync({53mediaTypes: "images",54allowsEditing: false,55quality: 0.8,56});57if (!result.canceled && result.assets?.[0]) {58await onPicture(result.assets[0].uri);59}60};6162return (63<View style={{ flex: 1, backgroundColor: "black" }}>64<CameraView ref={cameraRef} mirror style={{ flex: 1 }} facing={type} />65<View style={{ position: "absolute", left: 0, right: 0, bottom: bottom, gap: 16, alignItems: "center" }}>66<GlassView isInteractive style={{ padding: 8, borderRadius: 99 }}>67<TouchableOpacity onPress={takePhoto} style={{ width: 64, height: 64, borderRadius: 99, backgroundColor: "white" }} />68</GlassView>69<View style={{ flexDirection: "row", justifyContent: "space-around", paddingHorizontal: 8 }}>70<GlassButton onPress={selectPhoto} icon="photo" />71<GlassButton onPress={() => setType(t => t === "back" ? "front" : "back")} icon="arrow.triangle.2.circlepath" />72</View>73</View>74</View>75);76}77```7879## Audio Playback8081Use `expo-audio` not `expo-av`:8283```tsx84import { useAudioPlayer } from 'expo-audio';8586const player = useAudioPlayer({ uri: 'https://stream.nightride.fm/rektory.mp3' });8788<Button title="Play" onPress={() => player.play()} />89```9091## Audio Recording (Microphone)9293```tsx94import {95useAudioRecorder,96AudioModule,97RecordingPresets,98setAudioModeAsync,99useAudioRecorderState,100} from 'expo-audio';101import { useEffect } from 'react';102import { Alert, Button } from 'react-native';103104function App() {105const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);106const recorderState = useAudioRecorderState(audioRecorder);107108const record = async () => {109await audioRecorder.prepareToRecordAsync();110audioRecorder.record();111};112113const stop = () => audioRecorder.stop();114115useEffect(() => {116(async () => {117const status = await AudioModule.requestRecordingPermissionsAsync();118if (status.granted) {119setAudioModeAsync({ playsInSilentMode: true, allowsRecording: true });120} else {121Alert.alert('Permission to access microphone was denied');122}123})();124}, []);125126return (127<Button128title={recorderState.isRecording ? 'Stop' : 'Start'}129onPress={recorderState.isRecording ? stop : record}130/>131);132}133```134135## Video Playback136137Use `expo-video` not `expo-av`:138139```tsx140import { useVideoPlayer, VideoView } from 'expo-video';141import { useEvent } from 'expo';142143const videoSource = 'https://example.com/video.mp4';144145const player = useVideoPlayer(videoSource, player => {146player.loop = true;147player.play();148});149150const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });151152<VideoView player={player} fullscreenOptions={{}} allowsPictureInPicture />153```154155VideoView options:156- `allowsPictureInPicture`: boolean157- `contentFit`: 'contain' | 'cover' | 'fill'158- `nativeControls`: boolean159- `playsInline`: boolean160- `startsPictureInPictureAutomatically`: boolean161162## Saving Media163164```tsx165import * as MediaLibrary from "expo-media-library";166167const { granted } = await MediaLibrary.requestPermissionsAsync();168if (granted) {169await MediaLibrary.saveToLibraryAsync(uri);170}171```172173### Saving Base64 Images174175`MediaLibrary.saveToLibraryAsync` only accepts local file paths. Save base64 strings to disk first:176177```tsx178import { File, Paths } from "expo-file-system/next";179180function base64ToLocalUri(base64: string, filename?: string) {181if (!filename) {182const match = base64.match(/^data:(image\/[a-zA-Z]+);base64,/);183const ext = match ? match[1].split("/")[1] : "jpg";184filename = `generated-${Date.now()}.${ext}`;185}186187if (base64.startsWith("data:")) base64 = base64.split(",")[1];188const binaryString = atob(base64);189const len = binaryString.length;190const bytes = new Uint8Array(new ArrayBuffer(len));191for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);192193const f = new File(Paths.cache, filename);194f.create({ overwrite: true });195f.write(bytes);196return f.uri;197}198```199