Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: How to build a timeline-based video editor #4383

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions packages/docs/docs/building-a-timeline.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
---
image: /generated/articles-docs-building-a-timeline.png
id: building-a-timeline
title: Build a timeline-based video editor
crumb: 'Building video apps'
---

This document describes on a high-level how the [Remotion Player](/player) can be synchronized with a timeline.
Read this document for guidance on building a video editor with the following characteristics:

- Multiple tracks that overlay each other
- Items can be arbitrarily placed on a track
- Items can be of different types (e.g. video, audio, text, etc.)

<img src="/img/timelineitems.png" />
<br />
<br />
<Step>1</Step> Define a TypeScript type <code>Item</code> defining the different
item types. Create another one for defining the shape of a <code>Track</code>:

```tsx twoslash title="types.ts"
type BaseItem = {
from: number;
durationInFrames: number;
id: string;
};

export type SolidItem = BaseItem & {
type: 'solid';
color: string;
};

export type TextItem = BaseItem & {
type: 'text';
text: string;
color: string;
};

export type VideoItem = BaseItem & {
type: 'video';
src: string;
};

export type Item = SolidItem | TextItem | VideoItem;

export type Track = {
name: string;
items: Item[];
};
```

<Step>2</Step> Create a component that can render a list of tracks.

```tsx twoslash title="remotion/Main.tsx"
// @filename: types.ts
type BaseItem = {
from: number;
durationInFrames: number;
id: string;
};

export type SolidItem = BaseItem & {
type: 'solid';
color: string;
};

export type TextItem = BaseItem & {
type: 'text';
text: string;
color: string;
};

export type VideoItem = BaseItem & {
type: 'video';
src: string;
};

export type Item = SolidItem | TextItem | VideoItem;

export type Track = {
name: string;
items: Item[];
};

// @filename: Main.tsx
// ---cut---
import type {Track, Item} from './types';
import React from 'react';
import {AbsoluteFill, Sequence, OffthreadVideo} from 'remotion';

const ItemComp: React.FC<{
item: Item;
}> = ({item}) => {
if (item.type === 'solid') {
return <AbsoluteFill style={{backgroundColor: item.color}} />;
}

if (item.type === 'text') {
return <h1>{item.text}</h1>;
}

if (item.type === 'video') {
return <OffthreadVideo src={item.src} />;
}

throw new Error(`Unknown item type: ${JSON.stringify(item)}`);
};

const Track: React.FC<{
track: Track;
}> = ({track}) => {
return (
<AbsoluteFill>
{track.items.map((item) => {
return (
<Sequence
key={item.id}
from={item.from}
durationInFrames={item.durationInFrames}
>
<ItemComp item={item} />
</Sequence>
);
})}
</AbsoluteFill>
);
};

export const Main: React.FC<{
tracks: Track[];
}> = ({tracks}) => {
return (
<AbsoluteFill>
{tracks.map((track) => {
return <Track track={track} key={track.name} />;
})}
</AbsoluteFill>
);
};
```

:::tip
In CSS, the elements that are rendered at the bottom appear at the top. See: [Layers](/docs/layers)
:::

<Step>3</Step> Keep a state of tracks each containing an array of items.{' '}

Render
a [`<Player />`](/docs/player/player) component and pass the `tracks` as [`inputProps`](/docs/player/player#inputprops).

```tsx twoslash title="Editor.tsx"
// @filename: types.ts
type BaseItem = {
from: number;
durationInFrames: number;
};

export type SolidItem = BaseItem & {
type: 'shape';
color: string;
};

export type TextItem = BaseItem & {
type: 'text';
text: string;
color: string;
};

export type VideoItem = BaseItem & {
type: 'video';
src: string;
};

export type Item = SolidItem | TextItem | VideoItem;

export type Track = {
name: string;
items: Item[];
};

// @filename: remotion/Main.tsx
import React from 'react';
import type {Track} from '../types';
export const Main: React.FC<{
tracks: Track[];
}> = ({tracks}) => {
return null;
};

// @filename: Editor.tsx
// ---cut---
import React, {useMemo, useState} from 'react';
import {Player} from '@remotion/player';
import type {Item} from './types';
import {Main} from './remotion/Main';

type Track = {
name: string;
items: Item[];
};

export const Editor = () => {
const [tracks, setTracks] = useState<Track[]>([
{name: 'Track 1', items: []},
{name: 'Track 2', items: []},
]);

const inputProps = useMemo(() => {
return {
tracks,
};
}, []);

return (
<>
<Player
component={Main}
fps={30}
inputProps={inputProps}
durationInFrames={600}
compositionWidth={1280}
compositionHeight={720}
/>
</>
);
};
```

<Step>4</Step> Build a timeline component: You now have access to the <code>
tracks
</code> state and can update it using the <code>setTracks</code> function.

We do not currently provide samples how to build a timeline component, since everybody has different needs and styling preferences.

An opinionated sample implementation is [available for purchase in the Remotion Store](https://www.remotion.pro/timeline).

```tsx twoslash title="remotion/Timeline.tsx" {23}
type Item = {};
type Track = {};
const inputProps = {};
import {Player} from '@remotion/player';
import {useState, useMemo} from 'react';

const Main: React.FC<{
tracks: Track[];
}> = ({tracks}) => {
return null;
};

const Timeline: React.FC<{
tracks: Track[];
setTracks: React.Dispatch<React.SetStateAction<Track[]>>;
}> = () => {
return null;
};
// ---cut---
const Editor: React.FC = () => {
const [tracks, setTracks] = useState<Track[]>([
{name: 'Track 1', items: []},
{name: 'Track 2', items: []},
]);

const inputProps = useMemo(() => {
return {
tracks,
};
}, []);

return (
<>
<Player
component={Main}
fps={30}
inputProps={inputProps}
durationInFrames={600}
compositionWidth={1280}
compositionHeight={720}
/>
<Timeline tracks={tracks} setTracks={setTracks} />
</>
);
};
```

## See also

- [Layers](/docs/layers)
15 changes: 8 additions & 7 deletions packages/docs/docs/layers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
image: /generated/articles-docs-layers.png
id: layers
title: Layers
crumb: "Designing videos"
crumb: 'Designing videos'
---

Unlike normal websites, a video has fixed dimensions. That means, it is okay to use `position: "absolute"`!
Expand All @@ -13,14 +13,14 @@ This is a common pattern in video editors, and in Photoshop.
An easy way to do it is using the [`<AbsoluteFill>`](/docs/absolute-fill) component:

```tsx twoslash title="MyComp.tsx"
import React from "react";
import { AbsoluteFill, Img, staticFile } from "remotion";
import React from 'react';
import {AbsoluteFill, Img, staticFile} from 'remotion';

export const MyComp: React.FC = () => {
return (
<AbsoluteFill>
<AbsoluteFill>
<Img src={staticFile("bg.png")} />
<Img src={staticFile('bg.png')} />
</AbsoluteFill>
<AbsoluteFill>
<h1>This text appears on top of the video!</h1>
Expand All @@ -35,14 +35,14 @@ This will render the text on top of the image.
If you want to only show an element for a certain duration, you can use a [`<Sequence>`](/docs/sequence) component, which by default is also absolutely positioned.

```tsx twoslash title="MyComp.tsx"
import React from "react";
import { AbsoluteFill, Img, staticFile, Sequence } from "remotion";
import React from 'react';
import {AbsoluteFill, Img, staticFile, Sequence} from 'remotion';

export const MyComp: React.FC = () => {
return (
<AbsoluteFill>
<Sequence>
<Img src={staticFile("bg.png")} />
<Img src={staticFile('bg.png')} />
</Sequence>
<Sequence from={60} durationInFrames={40}>
<h1>This text appears after 60 frames!</h1>
Expand All @@ -59,3 +59,4 @@ If you are aware of this behavior, you can use it to your advantage and avoid us

- [`<AbsoluteFill>`](/docs/absolute-fill)
- [`<Sequence>`](/docs/sequence)
- [Build a timeline-based video editor](/docs/building-a-timeline)
10 changes: 4 additions & 6 deletions packages/docs/docs/player/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ To make it smaller, pass a `style` prop to give the player a different width: `{

The height you would like the video to have when rendered as an MP4. Use `style={{height: <height>}}` to define a height to be assumed in the browser.

### `inputProps`

Pass props to the component that you have specified using the `component` prop. The Typescript definition takes the shape of the props that you have given to your `component`. Default `undefined`.

### `loop`

_optional_
Expand Down Expand Up @@ -124,12 +128,6 @@ _optional_

A boolean property defining whether the video position should go back to zero once the video has ended. Only works if `loop` is disabled. Default `true`.

### `inputProps`

_optional_

Pass props to the component that you have specified using the `component` prop. The Typescript definition takes the shape of the props that you have given to your `component`. Default `undefined`.

### `style`

_optional_
Expand Down
1 change: 1 addition & 0 deletions packages/docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ module.exports = {
'video-uploads',
'presigned-urls',
'font-picker',
'building-a-timeline',
'multiple-fps',
],
},
Expand Down
7 changes: 7 additions & 0 deletions packages/docs/src/data/articles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,13 @@ export const articles = [
compId: 'articles-docs-absolute-fill',
crumb: 'API',
},
{
id: 'building-a-timeline',
title: 'Build a timeline-based video editor',
relativePath: 'docs/building-a-timeline.mdx',
compId: 'articles-docs-building-a-timeline',
crumb: 'Building video apps',
},
{
id: 'offthreadvideo',
title: '<OffthreadVideo>',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/docs/static/img/timelineitems.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading