Skip to main content

Handling user video uploads

In an app where users can upload videos and edit them, we can create a better user experience by loading the video into a player even before the upload is finished. Good news: This can be done pretty easily!

Allowing user uploads

We have a component which returns a <Video> tag that includes an URL as source.

MyComposition.tsx
tsx
import { AbsoluteFill, Video } from "remotion";
 
type VideoProps = {
videoURL: string;
};
 
export const MyComponent: React.FC<VideoProps> = ({ videoURL }) => {
return (
<AbsoluteFill>
<Video src={videoURL} />
</AbsoluteFill>
);
};
MyComposition.tsx
tsx
import { AbsoluteFill, Video } from "remotion";
 
type VideoProps = {
videoURL: string;
};
 
export const MyComponent: React.FC<VideoProps> = ({ videoURL }) => {
return (
<AbsoluteFill>
<Video src={videoURL} />
</AbsoluteFill>
);
};

The video URL will be passed from the Remotion Player to our component.
Using a <input type="file"> element, we allow a user upload.
As soon as a file is fully uploaded to the cloud, the URL will be set and can be used by the component to display the video.

App.tsx
tsx
import { Player } from "@remotion/player";
import { useState } from "react";
 
export const RemotionPlayer: React.FC = () => {
const [videoUrl, setVideoUrl] = useState<string | null>(null);
 
const handleChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) {
return;
}
 
const file = event.target.files[0];
//upload is an example function & returns a URL when a file is uploaded on the cloud.
const cloudURL = await upload(file);
// E.g., cloudURL = https://exampleBucketName.s3.ExampleAwsRegion.amazonaws.com
setVideoUrl(cloudURL);
},
[]
);
 
return (
<div>
{videoUrl === null ? null : (
<Player
component={MyComposition}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
inputProps={{ videoUrl }}
/>
)}
 
<input type="file" onChange={handleChange} />
</div>
);
};
App.tsx
tsx
import { Player } from "@remotion/player";
import { useState } from "react";
 
export const RemotionPlayer: React.FC = () => {
const [videoUrl, setVideoUrl] = useState<string | null>(null);
 
const handleChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) {
return;
}
 
const file = event.target.files[0];
//upload is an example function & returns a URL when a file is uploaded on the cloud.
const cloudURL = await upload(file);
// E.g., cloudURL = https://exampleBucketName.s3.ExampleAwsRegion.amazonaws.com
setVideoUrl(cloudURL);
},
[]
);
 
return (
<div>
{videoUrl === null ? null : (
<Player
component={MyComposition}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
inputProps={{ videoUrl }}
/>
)}
 
<input type="file" onChange={handleChange} />
</div>
);
};

The implementation of the upload() function is provider-specific and we do not show an implementation in this article. We assume it is a function that takes a file, uploads it and returns an URL.

Optimistic updates

When we start the upload of the file, we can create a blob URL using URL.createObjectURL() which can be used to display the local file in a <Video> tag. When the file is done uploading and we get the remote URL, the component shall use remote URL as source.

App.tsx
tsx
import { Player } from "@remotion/player";
import { useCallback, useState } from "react";
 
type VideoState =
| {
type: "empty";
}
| {
type: "blob" | "cloud";
url: string;
};
 
export const RemotionPlayer: React.FC = () => {
const [videoState, setVideoState] = useState<VideoState>({
type: "empty",
});
 
const handleChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) {
return;
}
 
const file = event.target.files[0];
const blobUrl = URL.createObjectURL(file);
setVideoState({ type: "blob", url: blobUrl });
const cloudUrl = await upload(file);
setVideoState({ type: "cloud", url: cloudUrl });
URL.revokeObjectURL(blobUrl);
},
[]
);
 
return (
<div>
{videoState.type !== "empty" ? (
<Player
component={MyComposition}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
inputProps={{ videoUrl: videoState.url }}
/>
) : null}
<input type="file" onChange={handleChange} />
</div>
);
};
App.tsx
tsx
import { Player } from "@remotion/player";
import { useCallback, useState } from "react";
 
type VideoState =
| {
type: "empty";
}
| {
type: "blob" | "cloud";
url: string;
};
 
export const RemotionPlayer: React.FC = () => {
const [videoState, setVideoState] = useState<VideoState>({
type: "empty",
});
 
const handleChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) {
return;
}
 
const file = event.target.files[0];
const blobUrl = URL.createObjectURL(file);
setVideoState({ type: "blob", url: blobUrl });
const cloudUrl = await upload(file);
setVideoState({ type: "cloud", url: cloudUrl });
URL.revokeObjectURL(blobUrl);
},
[]
);
 
return (
<div>
{videoState.type !== "empty" ? (
<Player
component={MyComposition}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
inputProps={{ videoUrl: videoState.url }}
/>
) : null}
<input type="file" onChange={handleChange} />
</div>
);
};

This will result in the user immediately seeing the video as they drag it into the input field. It is a good practice to call URL.revokeObjectURL() after the local video is not used anymore to free up the used memory.

See also