expo-media-edit
v0.5.1
Published
Native video editing for Expo — trim, overlay text/images, mix audio. No FFmpeg.
Maintainers
Readme
expo-media-edit
Native video editing for Expo apps. Trim, overlay text/images, and mix audio — fully on-device via AVFoundation (iOS) and MediaCodec (Android). No FFmpeg, no cloud rendering.
Requirements
- iOS 16+
- Android API 26+
- Expo SDK 54+
- Requires a native build (EAS Build or
expo run:ios/expo run:android). Does not work in Expo Go.
Installation
npx expo install expo-media-editThen rebuild your native app:
# iOS
npx expo run:ios
# or
eas build --platform ios
# Android
npx expo run:androidUsage
import { editVideo, addProgressListener, cancelEdit } from 'expo-media-edit';
// Subscribe to progress before starting
const sub = addProgressListener(({ progress }) => {
console.log(`Progress: ${Math.round(progress * 100)}%`);
});
const outputUri = await editVideo({
inputUri: 'file:///path/to/video.mp4',
trim: { startMs: 0, endMs: 15000 },
quality: 'medium',
overlays: [
{
type: 'text',
content: 'Hello World',
x: 0.05,
y: 0.85,
fontSize: 48,
color: '#FFFFFF',
fontWeight: 'bold',
},
{
type: 'image',
uri: 'file:///path/to/logo.png',
x: 0.7,
y: 0.05,
width: 0.2,
height: 0.1,
opacity: 0.9,
},
],
audio: {
uri: 'file:///path/to/music.mp3',
volume: 0.8,
originalVolume: 0.2,
trimToVideo: true,
},
});
sub.remove(); // always clean up the listenerTo cancel an in-progress edit:
await cancelEdit(); // the editVideo() promise rejects with code "CANCELLED"API
editVideo(job: EditJob): Promise<string>
Edit a video and return the URI of the output file.
type EditJob = {
inputUri: string; // Local file URI (file://...) or https:// URL
outputUri?: string; // Optional output path; defaults to a temp file
trim?: {
startMs: number; // Start time in milliseconds
endMs: number; // End time in milliseconds
};
overlays?: OverlayItem[];
audio?: AudioMix;
quality?: 'low' | 'medium' | 'high'; // Default: 'high'
};
type OverlayItem =
| {
type: 'text';
content: string;
x: number; // 0.0–1.0 relative to video width
y: number; // 0.0–1.0 relative to video height
fontSize?: number; // Default: 32
color?: string; // CSS hex, e.g. '#FFFFFF'. Default: '#FFFFFF'
fontWeight?: 'normal' | 'bold';
backgroundColor?: string;
startMs?: number; // Show from this time (default: 0)
endMs?: number; // Hide after this time (default: video end)
}
| {
type: 'image';
uri: string; // Local image URI
x: number;
y: number;
width: number; // 0.0–1.0 relative to video width
height: number; // 0.0–1.0 relative to video height
opacity?: number; // 0.0–1.0. Default: 1.0
startMs?: number;
endMs?: number;
};
type AudioMix = {
uri: string; // Local audio URI (.mp3, .m4a, .wav)
volume?: number; // Music track volume 0.0–1.0. Default: 1.0
originalVolume?: number; // Original audio volume 0.0–1.0. Default: 0.0
startMs?: number; // Music start offset in ms. Default: 0
trimToVideo?: boolean; // Cut music at video end. Default: true
};getVideoInfo(uri: string): Promise<VideoInfo>
Read metadata about a video file.
type VideoInfo = {
durationMs: number;
width: number;
height: number;
fps: number;
fileSize: number; // Bytes
codec?: string;
};generateThumbnail(uri: string, timeMs: number, options?: { width?: number; height?: number }): Promise<string>
Extract a JPEG thumbnail from a video at the given timestamp. Returns a file:// URI.
addProgressListener(callback: (event: ProgressEvent) => void): Subscription
Subscribe to progress events during editVideo(). Returns a subscription with a remove() method.
type ProgressEvent = { progress: number }; // 0.0–1.0Always call sub.remove() after editVideo() resolves or rejects.
cancelEdit(): Promise<void>
Cancel an in-progress editVideo() call. The editVideo promise rejects with error code "CANCELLED".
cleanTempFiles(): Promise<number>
Delete all temporary files created by expo-media-edit. Returns the number of files deleted.
Platform notes
iOS
Uses AVFoundation:
- Trim:
AVAssetExportSessionwith a time range — lossless stream copy. - Overlays:
AVVideoCompositionCoreAnimationToolwithCATextLayer/CALayerburn-in. - Audio:
AVMutableAudioMixInputParametersfor volume control; additionalAVMutableCompositionTrackfor music.
Android
Uses MediaCodec + MediaMuxer:
- Trim:
MediaExtractor+MediaMuxerstream copy (no re-encode). - Overlays: Frame-by-frame decode via
MediaMetadataRetriever, Canvas draw, YUV420 re-encode viaMediaCodec. Quality controlled via bitrate (low: 1Mbps, medium: 2Mbps, high: 4Mbps). - Audio: Stream copy when
volume == 1.0; PCM decode → scale → AAC re-encode for other volume values. Rotation metadata preserved viaMediaMuxer.setOrientationHint().
Known limitations
- Background processing is not supported (the app must stay in the foreground during editing).
- Android audio mixing does not support simultaneous multi-track volume scaling (original + music both at non-1.0 volume in the same output file). Each track is scaled independently.
License
MIT
