はじめに
YouTube shortsやTiktokなどといった「ショート動画」を作るためのシンプルなツールを作ってみたいと思い、まずはReactで動画編集方法を調べてみたところ「Remotion」を使うのが良さそうだったので、調べてみました。
Remotionとは
Remotionは、プログラミングによって動画を作ることができるパッケージであり、Reactの状態管理の仕組みを使って動画を作り込むことができます。
Remotionは主に次の3つで構成されています。
- Remotion
プログラミングによって動画コンポーネントを生成し、Remotionが提供するエディター上でプレビュー・レンタリングを行う。ローカル環境で完結できる。 - Remotion Player
Remotionの動画プレイヤー部分のみを切り出したもの。Remotionによる動画生成を自分のアプリケーションに組み込むことができる。 - Remotion Lambda
Remotionで作成した動画の書き出しをAWS Lambda上で実行するもの。(Remotionはブラウザでの動画書き出しをサポートしていない)
なお、使用条件によってライセンスが異なり、有料プランを契約する必要がある場合があります。詳細は以下を確認してください。
https://www.remotion.pro/license
今回は以下のように、ローカルの動画を配置し、タイトルテキストを編集できるアプリケーションを作りたいので、RemotionとRemotion Playerを使って実装してみます。
Remotionを使ってみる
導入
pnpmでインストールします。
1 | pnpm i remotion @remotion/player |
動画のレイアウト作成
まずは、動画のレンダリング用コンポーネントを作ります。先ほどお見せしたように、上部にタイトル、真ん中に動画が配置されるようなレイアウトを作っていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | "use client"; import { AbsoluteFill, Video } from "remotion"; import { CSSProperties } from "react"; export const ShortVideo = ({ videoUrl, title, titleStyle, }: { videoUrl: string; title: string; titleStyle: CSSProperties; }) => { return ( <AbsoluteFill style={{ background: "black", }} > <div style={{ textAlign: "center", marginTop: titleStyle.marginTop, marginLeft: 24, marginRight: 24, }} > <span style={{ boxDecorationBreak: "clone", WebkitBoxDecorationBreak: "clone", ...titleStyle, marginTop: undefined, }} > {title} </span> </div> <AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}> <Video src={videoUrl} style={{ width: 1080 }} /> </AbsoluteFill> </AbsoluteFill> ); }; |
AbsoluteFill
はremotionの便利コンポーネントで、実際には以下のスタイルが適用されているdivです。
1 2 3 4 5 6 7 8 9 10 | const style: React.CSSProperties = { top: 0, left: 0, right: 0, bottom: 0, width: "100%", height: "100%", display: "flex", flexDirection: "column", }; |
Video
もremotionのコンポーネントで、任意の動画ファイルを埋め込むことができます。
Playerでの動画プレビュー
ローカルの動画ファイルをすると、Playerが表示され、先ほどのコンポーネントを使った動画がプレビューされるように実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | "use client"; import { Player } from "@remotion/player"; import { ShortVideo } from "@/app/mv-shortener/ShortVideo"; import { useCallback, useState } from "react"; export default function MVShortener() { 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]; setVideoUrl(URL.createObjectURL(file)); }, [], ); if (!videoUrl) { return <input type="file" onChange={handleChange} />; } return ( <Player component={ShortVideo} // 先ほど作ったコンポーネントを指定 durationInFrames={30 * 60} // 動画の長さをフレーム数で指定 30fps x 60 = 60sec compositionWidth={1080} // 動画の横幅 compositionHeight={1920} // 動画の縦幅 fps={30} // フレームレート controls // 再生・一時停止・シーク・ボリューム調整などのコントロールを表示 style={{ width: "100%", height: "60vh" }} inputProps={{ // ShortVideのPropsを渡す videoUrl, title: "タイトルです", titleStyle: { marginTop: 200, fontSize: "6rem", lineHeight: "7rem", backgroundColor: "red", borderRadius: "30px", border: `10px solid red`, color: "white", }, }} /> ); } |
まず、動画ファイルの読み込みは
<input type="file"
で行います。remotionに動画を埋め込むにはURLで渡す必要があるのですが、
URL.createObjectURL
で生成したBlob URLを渡したら問題ありませんでした。
動画プレイヤーは
Player
コンポーネントを使います。propsの
component
に先ほど作った動画レイアウト用のコンポーネントを渡し、
inputProps
に定義したpropsの内容を渡します。
その他に動画のサイズ、長さ、フレームレートなどを指定します。
テキスト編集機能を付け加える
タイトルテキストの編集機能を付け加えます。今回は、テキスト・文字色・背景色を変更できるようにします。UIはJoy UIで構築しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | "use client"; import { Player } from "@remotion/player"; import { ShortVideo } from "@/app/mv-shortener/ShortVideo"; import { FormControl, FormLabel, Input, Stack, Tab, TabList, TabPanel, Tabs, } from "@mui/joy"; import { useCallback, useMemo, useState } from "react"; import { grey, red } from "@mui/material/colors"; import ColorPicker from "@/app/mv-shortener/ColorPicker"; export default function MVShortener() { const [videoUrl, setVideoUrl] = useState<string | null>(null); const [title, setTitle] = useState<string>("タイトル"); const [bgColor, setBgColor] = useState<string>(red[500]); const [titleColor, setTitleColor] = useState<string>(grey[50]); const titleStyle = useMemo( () => ({ marginTop: 200, fontSize: "6rem", lineHeight: "7rem", backgroundColor: bgColor, borderRadius: "30px", border: `10px solid ${bgColor}`, color: titleColor, }), [bgColor, titleColor], ); const handleChange = useCallback( async (event: React.ChangeEvent<HTMLInputElement>) => { if (event.target.files === null) { return; } const file = event.target.files[0]; setVideoUrl(URL.createObjectURL(file)); }, [], ); if (!videoUrl) { return <input type="file" onChange={handleChange} />; } return ( <> {videoUrl && ( <Player component={ShortVideo} durationInFrames={30 * 60} compositionWidth={1080} compositionHeight={1920} fps={30} controls style={{ width: "100%", height: "60vh", position: "sticky" }} inputProps={{ videoUrl, title, titleStyle }} /> )} <Stack spacing={2} sx={{ mt: 2 }}> <FormControl> <FormLabel>テキスト</FormLabel> <Input fullWidth value={title} onChange={(e) => setTitle(e.target.value)} /> </FormControl> <ColorPicker label="背景色" value={bgColor} onChange={setBgColor} /> <ColorPicker label="文字色" value={titleColor} onChange={setTitleColor} /> </Stack> </> ); } |
テキスト・文字色・背景色をstate化し、それぞれを値を変更できるフォームを用意しました。これで、UI上でタイトルテキストを編集できるようなりました。
おまけ : ColerPicker
上記で使用されている
ColorPicker
コンポーネントは、Joy UIのサンプルをベースに作ってみたものです。中身は以下のような感じになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | import { FormLabel, Radio, radioClasses, RadioGroup, Sheet } from "@mui/joy"; import { amber, blue, blueGrey, brown, cyan, deepOrange, deepPurple, green, grey, indigo, lightBlue, lightGreen, lime, orange, pink, purple, red, teal, yellow, } from "@mui/material/colors"; import { Done } from "@mui/icons-material"; import Box from "@mui/joy/Box"; import { ReactNode } from "react"; export default function ColorPicker({ label, value, onChange, }: { label: ReactNode; value: string; onChange: (color: string) => void; }) { return ( <Box> <FormLabel id="product-color-attribute" sx={{ mb: 1.5, }} > {label} </FormLabel> <RadioGroup aria-labelledby="product-color-attribute" value={value} onChange={(e) => onChange(e.target.value)} sx={{ gap: 2, mx: -2, px: 2, flexDirection: "row", overflowX: "auto", overflowY: "hidden", }} > {( [ grey[50], grey[900], red[500], pink[500], purple[500], deepPurple[500], indigo[500], blue[500], lightBlue[500], cyan[500], teal[500], green[500], lightGreen[500], lime[500], yellow[500], amber[500], orange[500], deepOrange[500], brown[500], grey[500], blueGrey[500], ] as const ).map((color) => ( <Sheet key={color} sx={{ position: "relative", width: 40, height: 40, flexShrink: 0, bgcolor: color, borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", border: color === grey[50] ? `2px solid ${grey[500]}` : undefined, }} > <Radio overlay variant="solid" checkedIcon={ <Done fontSize="large" htmlColor={color === grey[50] ? grey[900] : grey[50]} /> } value={color} slotProps={{ input: { "aria-label": color }, radio: { sx: { display: "contents", "--variant-borderWidth": "2px", }, }, }} sx={{ "--joy-focus-outlineOffset": "4px", [`& .${radioClasses.action}.${radioClasses.focusVisible}`]: { outlineWidth: "2px", }, bgcolor: color, }} /> </Sheet> ))} </RadioGroup> </Box> ); } |
さいごに
ここまでは良い感じでした。ただ、ブラウザで動画書き出しできないというのは痛いです。なんとかブラウザで動画書き出しする方法がないか調べてみたいと思います。