Upload videos from a React Native app

Add resumable, chunked video uploads to a React Native (Expo) app using the FastPix React Native Uploads SDK and a signed upload URL from your backend.

Uploading video from a phone is different from uploading other files. Connections drop, apps get backgrounded, and networks switch between Wi-Fi and cellular mid-transfer. A plain fetch of a whole file starts over every time that happens.

The FastPix React Native Uploads SDK uploads in chunks and resumes from the last confirmed byte, so an interrupted upload continues instead of restarting. Uploads go directly from the device to FastPix, so the video bytes never pass through your server.

This guide continues from the React Native quickstart app. By the end you can pick a video, upload it with progress and pause and resume, and play it back once FastPix has processed it.

WARNING:
The Uploads SDK depends on native modules, so uploading does not run in Expo Go. You get a “native module that doesn’t exist” error. Run a development build (npx expo run:ios or npx expo run:android, or EAS Build) to try it on a device. Playback (the quickstart) still works in Expo Go, and only the upload screen needs the development build.


How it works

  1. Your backend creates a signed upload URL using the FastPix Node SDK and your Secret Key.
  2. The app lets the user pick a video with expo-image-picker.
  3. The React Native Uploads SDK uploads the file to that URL in chunks, emitting progress events.
  4. FastPix processes the upload asynchronously and assigns it a playback ID.
  5. Once ready, you play it with the VideoPlayer from the quickstart.

What you’ll build

  • A screen with a “Pick a video” button.
  • A progress bar with working Pause and Resume controls.
  • Playback of the uploaded video once it’s ready.

Before you begin

Make sure you have the following:

  • The quickstart app, or any Expo app with a VideoPlayer component.
  • A FastPix account with an Access Token ID and Secret Key. See get your Access Token ID and Secret Key.
  • A backend you control (this guide uses a small Express server) to create signed upload URLs.
  • A development build to run the upload screen. The Uploads SDK uses native modules that don’t run in Expo Go.

WARNING:
Your Secret Key must never ship inside the app. Anyone can extract strings from a mobile binary. Create signed upload URLs on a server and have the app request them.

Each code block is a complete file. The bold filename above it tells you where the code goes. Create or replace that file with the code shown.


Create a signed upload URL (backend)

Signed upload URLs are created with your Secret Key, so this belongs on the server. Here’s a minimal Express route:

server.js

import express from "express";
import Client from "@fastpix/fastpix-node";
const fastpix = new Client({
accessTokenId: process.env.FASTPIX_USERNAME, // Access Token ID
secretKey: process.env.FASTPIX_PASSWORD, // Secret Key
});
const app = express();
// Don't advertise the framework/version in the X-Powered-By header.
app.disable("x-powered-by");
app.post("/uploads", async (_req, res) => {
const result = await fastpix.uploadMediaFromDevice({
corsOrigin: "*",
pushMediaSettings: { accessPolicy: "public" },
});
res.json({ uploadUrl: result.data.url, uploadId: result.data.uploadId });
});
app.listen(8787);

Install the server dependencies and set your credentials:

npm install @fastpix/fastpix-node express
export FASTPIX_USERNAME=your-access-token-id
export FASTPIX_PASSWORD=your-secret-key

NOTE:
Unlike a browser upload, a native device upload does not need the X-Client-Type: web-browser header. The signed URL from the call above is ready for the React Native SDK to upload to directly.

Keep the uploadId. It identifies the media in the rest of the API, and you use it to fetch the playback ID once processing finishes.


Install the app dependencies

In your Expo app:

npx expo install @fastpix/react-native-uploads expo-image-picker

expo-image-picker opens the system library so the user can choose a video. The Uploads SDK handles the chunked, resumable transfer.

NOTE:
If you use a bare React Native project (not managed Expo), run cd ios && pod install after installing so the native modules link. Managed Expo development builds handle this during prebuild.


Pick a video

Start with a component that opens the picker and shows the chosen file. Point CREATE_UPLOAD_ENDPOINT at your backend route:

src/components/upload-video.tsx

import * as ImagePicker from "expo-image-picker";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
// URL of your backend route that creates a FastPix signed upload URL.
const CREATE_UPLOAD_ENDPOINT = "https://your-backend.example.com/uploads";
export default function UploadVideo() {
const [fileUri, setFileUri] = useState<string | null>(null);
async function pickVideo() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: "videos",
});
if (result.canceled) return;
setFileUri(result.assets[0].uri);
}
return (
<View style={styles.container}>
<Button title="Pick a video to upload" onPress={pickVideo} />
{fileUri && <Text style={styles.label}>Selected: {fileUri}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24, gap: 16 },
label: { fontSize: 12, textAlign: "center" },
});

Upload with progress, pause, and resume

Now wire the picked file into FastPixUpload. Three things to know about the API:

  • endpoint takes an async function, not a URL. The SDK calls it to mint a fresh signed URL right when the upload begins, so URLs aren’t created for videos the user never uploads.
  • chunkSize is in KB and must be a multiple of 256 KB. 5 * 1024 is a 5 MB chunk (20 x 256 KB), a good default for mobile. Smaller chunks resume faster on flaky networks but add per-chunk overhead. Keep any custom size 256 KB-aligned, or a mid-upload chunk fails with HTTP 400.
  • The upload emits events. Subscribe with upload.on(...) to drive a progress bar, and keep a ref to it so buttons can pause() and resume().

This complete component replaces the placeholder from the previous step:

src/components/upload-video.tsx

import { FastPixUpload } from "@fastpix/react-native-uploads";
import * as ImagePicker from "expo-image-picker";
import { useRef, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
const CREATE_UPLOAD_ENDPOINT = "https://your-backend.example.com/uploads";
type UploadStatus = "idle" | "uploading" | "paused" | "success" | "error";
async function createUploadUrl(): Promise<string> {
const response = await fetch(CREATE_UPLOAD_ENDPOINT, { method: "POST" });
if (!response.ok) throw new Error("Could not create an upload URL");
const { uploadUrl } = (await response.json()) as { uploadUrl: string };
return uploadUrl;
}
export default function UploadVideo() {
const uploadRef = useRef<FastPixUpload | null>(null);
const [status, setStatus] = useState<UploadStatus>("idle");
const [percentage, setPercentage] = useState(0);
const [error, setError] = useState<string | null>(null);
async function pickAndUpload() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: "videos",
});
if (result.canceled) return;
const upload = new FastPixUpload({
endpoint: createUploadUrl,
fileUri: result.assets[0].uri,
chunkSize: 5 * 1024,
});
uploadRef.current = upload;
upload.on("progress", ({ percentage }) => setPercentage(percentage));
upload.on("success", () => setStatus("success"));
upload.on("error", ({ message }) => {
setError(message);
setStatus("error");
});
setError(null);
setStatus("uploading");
await upload.start();
}
function togglePause() {
const upload = uploadRef.current;
if (!upload) return;
if (status === "uploading") {
upload.pause();
setStatus("paused");
} else if (status === "paused") {
upload.resume();
setStatus("uploading");
}
}
return (
<View style={styles.container}>
<Button title="Pick a video to upload" onPress={pickAndUpload} />
{status !== "idle" && (
<View style={styles.progress}>
<Text style={styles.label}>
{status === "success"
? "Upload complete"
: status === "error"
? `Failed: ${error}`
: `${percentage}%`}
</Text>
<View style={styles.track}>
<View style={[styles.fill, { width: `${percentage}%` }]} />
</View>
{(status === "uploading" || status === "paused") && (
<Button
title={status === "paused" ? "Resume" : "Pause"}
onPress={togglePause}
/>
)}
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24, gap: 16 },
progress: { gap: 8 },
label: { fontSize: 16, textAlign: "center" },
track: {
height: 8,
borderRadius: 4,
backgroundColor: "#e0e0e0",
overflow: "hidden",
},
fill: { height: "100%", backgroundColor: "#0a84ff" },
});

NOTE:
The SDK also resumes on its own when the network drops and comes back, and when the device switches between Wi-Fi and cellular. pause() and resume() are for user-driven control on top of that. Other events you can subscribe to include stateChange, pause, and resume.


Play the uploaded video

After success, FastPix processes the video before it can be streamed. Once it’s ready, fetch the playback ID for your uploadId from your backend and hand it to the quickstart player:

src/app/watch.tsx

import VideoPlayer from "../components/video-player";
export default function Watch({ playbackId }: { readonly playbackId: string }) {
return <VideoPlayer playbackId={playbackId} />;
}

NOTE:
Processing is asynchronous, so the playback ID isn’t available the instant the upload finishes. The reliable signal is the video.media.ready webhook to your backend, and you can also poll get media by uploadId.


Troubleshooting

The upload fails immediately with a URL or credentials error.

Your backend route isn’t returning a valid signed URL. Test it directly with curl -X POST https://your-backend.example.com/uploads. It should return JSON with an uploadUrl. Check that FASTPIX_USERNAME and FASTPIX_PASSWORD are set on the server.

The picker returns a URI that the SDK can’t read.

expo-image-picker returns a file:// URI. Pass result.assets[0].uri straight through as fileUri, and don’t strip the scheme. On Android, make sure the app has media permission (the picker requests it on first use).

The chunk size is rejected, or a mid-upload chunk fails with HTTP 400.

chunkSize is in KB, and every chunk except the last must be a multiple of 256 KB. This is a hard requirement of the underlying resumable-upload storage, not FastPix-specific. 5 * 1024 (5 MB, which is 20 x 256 KB) is a safe default and what this guide uses. If you set a custom size, keep it 256 KB-aligned (n * 256). A non-aligned value uploads the first chunk fine but fails a later one with a 400. Also stay within the valid range (roughly 5 MB to 500 MB per chunk).

Every chunk fails with HTTP 400.

Your backend is sending the X-Client-Type: web-browser header when it creates the upload URL. That header is for browser uploaders (which perform a resumable-session handshake). The React Native uploader PUTs chunks directly, so it needs the plain device upload URL you get by omitting that header. Remove it from the backend call and the chunks upload correctly.

Nothing happens after pod install on a bare project.

Rebuild the native app (npx expo run:ios, or open Xcode and run). Installing pods alone doesn’t rebuild an already-installed app.


Frequently asked questions

Can I upload from the app without a backend server?

No. Uploads are authorized by a signed upload URL created with your Secret Key, and the Secret Key must never ship inside a mobile app, since anyone can extract strings from the binary. Create the signed URL on a server you control (this guide uses a small Express route) and have the app request it.

Does uploading work in Expo Go?

No. The Uploads SDK depends on native modules that aren’t in Expo Go, so the upload screen throws a “native module that doesn’t exist” error there. Run a development build with npx expo run:ios or npx expo run:android (or EAS Build). Playback from the quickstart still works in Expo Go; only uploading needs the development build.

How do I know when an uploaded video is ready to play?

Processing is asynchronous, so the playback ID isn’t ready the instant the upload finishes. Listen for the video.media.ready webhook on your backend, or poll get media by uploadId until its status is ready, then use the playback ID with the player.


What’s next