๋์์ ์์ถ์ ์๋ฒ์์ ํ๋ ๊ฒ์ด๋ผ๋ ๊ณ ์ ๊ด๋ ์ด ์์์ต๋๋ค. ๊ทธ๋ฐ๋ฐ ๋ธ๋ผ์ฐ์ ์์ ์ง์ ๋์์์ ์์ถํ ์ ์๋ค๋ ๊ฒ์ ์๊ฒ ๋์ต๋๋ค.
FFmpeg.wasm์ ์๊ฒ ๋ ๊ณ๊ธฐ
์ด๋ฏธ์ง ์์ถ ๋๊ตฌ๋ฅผ ๋จผ์ ๋ง๋ค์์ต๋๋ค. Canvas API๋ก ๋ธ๋ผ์ฐ์ ์์ ์ด๋ฏธ์ง๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒ์ ๋น๊ต์ ๊ฐ๋จํ์ต๋๋ค.
๋์์์ ๋ฌ๋์ต๋๋ค. ๋์์์ ์์ญ~์๋ฐฑ ๊ฐ์ ์ด๋ฏธ์ง ํ๋ ์์ ์ค๋์ค๊น์ง ํฉ์ณ์ง ๋ณต์กํ ๋ฐ์ดํฐ์ ๋๋ค. Canvas API๋ก ์ฒ๋ฆฌํ๊ธฐ์๋ ํ๊ณ๊ฐ ์์์ต๋๋ค.
์กฐ์ฌํ๋ค๊ฐ FFmpeg.wasm์ ๋ฐ๊ฒฌํ์ต๋๋ค. FFmpeg๋ฅผ WebAssembly๋ก ์ปดํ์ผํด์ ๋ธ๋ผ์ฐ์ ์์ ์คํํ ์ ์๊ฒ ๋ง๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
์ค์น์ ์ฒซ ๋ฒ์งธ ๋ฒฝ
์ค์น๋ ๊ฐ๋จํฉ๋๋ค.
npm install @ffmpeg/ffmpeg @ffmpeg/util
๊ทธ๋ฐ๋ฐ ์คํํ๋ฉด ๋ฐ๋ก ์๋ฌ๊ฐ ๋ฉ๋๋ค.
SharedArrayBuffer is not defined
FFmpeg.wasm์ ๋ฉํฐ์ค๋ ๋ฉ์ ์ํด SharedArrayBuffer๊ฐ ํ์ํฉ๋๋ค. ์ด API๋ ๋ณด์์์ ์ด์ ๋ก ํน์ HTTP ํค๋๊ฐ ์์ด์ผ๋ง ํ์ฑํ๋ฉ๋๋ค.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Next.js ์ค์ ํ์ผ์ ์ด ํค๋๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
// next.config.js
headers: [
{
source: "/(.*)",
headers: [
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
],
},
]
๋ก์ปฌ์์๋ ๋ฉ๋๋ค. ๊ทธ๋ฐ๋ฐ Vercel์ ๋ฐฐํฌํ๋ฉด ๋ ์ ๋ฉ๋๋ค. Vercel์ ๊ฒฝ์ฐ vercel.json์๋ ๋์ผํ ํค๋ ์ค์ ์ด ํ์ํ์ต๋๋ค.
์ด๊ธฐ ๋ก๋ฉ ์๊ฐ ๋ฌธ์
FFmpeg.wasm์ ์ฝ์ด ํ์ผ์ ์ฝ 30MB์ ๋๋ค. ์ฒ์ ์ฌ์ฉํ ๋ ์ด ํ์ผ์ ๋ค์ด๋ก๋ํด์ผ ํฉ๋๋ค.
์ฌ์ฉ์๊ฐ ๋์์ ์์ถ ๋ฒํผ์ ์ฒ์ ๋๋ฅด๋ฉด 30์ด ๊ฐ๊น์ด ๊ธฐ๋ค๋ ค์ผ ํฉ๋๋ค. ์ด๊ฑด UX ๋ฌธ์ ์ ๋๋ค.
ํด๊ฒฐ์ฑ ์ ๋ ๊ฐ์ง์์ต๋๋ค.
๋ก๋ฉ ์ํ ํ์: ์งํ ์ํฉ์ ์ ์ ์๊ฒ ํ๋ก๊ทธ๋ ์ค๋ฐ์ "FFmpeg ๋ก๋ฉ ์ค (28MB)..." ๊ฐ์ ๋ฉ์์ง๋ฅผ ํ์ํ์ต๋๋ค.
์บ์ฑ ํ์ฉ: ํ ๋ฒ ๋ค์ด๋ก๋ํ๋ฉด ๋ธ๋ผ์ฐ์ ์บ์์ ์ ์ฅ๋ฉ๋๋ค. ๋ค์ ๋ฐฉ๋ฌธ๋ถํฐ๋ ์ฆ์ ๋ก๋๋ฉ๋๋ค.
๊ฐ์ ํ์ผ์์คํ ์ดํดํ๊ธฐ
FFmpeg๋ ํ์ผ์ ์ฝ๊ณ ์๋๋ค. ๋ธ๋ผ์ฐ์ ์๋ ํ์ผ์์คํ ์ด ์์ต๋๋ค. FFmpeg.wasm์ ๋ฉ๋ชจ๋ฆฌ ์์ ๊ฐ์ ํ์ผ์์คํ ์ ์ ๊ณตํฉ๋๋ค.
// ํ์ผ์ ๊ฐ์ ํ์ผ์์คํ
์ ์ฐ๊ธฐ
await ffmpeg.writeFile('input.mp4', await fetchFile(file));
// FFmpeg ๋ช
๋ น ์คํ
await ffmpeg.exec([
'-i', 'input.mp4',
'-crf', '28', // ์์ถ ํ์ง (0=๋ฌด์์ค, 51=์ต๋์์ถ)
'-preset', 'fast', // ์ธ์ฝ๋ฉ ์๋
'output.mp4'
]);
// ๊ฒฐ๊ณผ ํ์ผ ์ฝ๊ธฐ
const data = await ffmpeg.readFile('output.mp4');
// ๋ค์ด๋ก๋ ๋งํฌ ์์ฑ
const blob = new Blob([data.buffer], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
์ฒ๋ฆฌ ์๊ฐ ๋ฌธ์
100MB ๋์์์ ์์ถํ๋ ๋ฐ ์ผ๋ง๋ ๊ฑธ๋ฆด๊น์?
์๋ฒ์์ FFmpeg๋ฅผ ๋๋ฆฌ๋ฉด 12๋ถ ์ ๋์
๋๋ค. ๋ธ๋ผ์ฐ์ ์์๋? ๊ธฐ๊ธฐ๋ง๋ค ๋ค๋ฅด์ง๋ง 515๋ถ์ด ๊ฑธ๋ฆด ์ ์์ต๋๋ค.
์๋ฒ๋ ๊ณ ์ฑ๋ฅ CPU๋ฅผ ์ฌ์ฉํ์ง๋ง ๋ธ๋ผ์ฐ์ ๋ ์ฌ์ฉ์ ๊ธฐ๊ธฐ์ CPU๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๊ตฌํ ๋ ธํธ๋ถ์ด๋ผ๋ฉด ๋ ์ค๋ ๊ฑธ๋ฆฝ๋๋ค.
์ด ํ๊ณ๋ฅผ ์์งํ๊ฒ UI์ ํ์ํ์ต๋๋ค. "ํ์ผ ํฌ๊ธฐ์ ๋ฐ๋ผ ์ ๋ถ์ด ๊ฑธ๋ฆด ์ ์์ต๋๋ค. ํญ์ ๋ซ์ง ๋ง์ธ์."
์งํ ์ํฉ ํ์ํ๊ธฐ
FFmpeg.wasm์ ์ฒ๋ฆฌ ์งํ ์ํฉ์ ์ ์ ์๋ ์ฝ๋ฐฑ์ ์ ๊ณตํฉ๋๋ค.
ffmpeg.on('progress', ({ progress }) => {
setProgress(Math.round(progress * 100));
});
0์์ 100๊น์ง ํ๋ก๊ทธ๋ ์ค๋ฐ๊ฐ ์ฑ์์ง๋ ๊ฒ์ ๋ณด์ฌ์ฃผ๋ฉด ์ฌ์ฉ์๊ฐ ๊ธฐ๋ค๋ฆฌ๊ธฐ ํจ์ฌ ์ฌ์์ง๋๋ค.
๊ฒฐ๊ณผ
๋์์ ์์ถ, ํฌ๋งท ๋ณํ(MP4ยทMOVยทAVIยทWebM), GIF ๋ณํ, ์ค๋์ค ์ถ์ถ๊น์ง ๊ตฌํํ์ต๋๋ค. ๋ชจ๋ ๋ธ๋ผ์ฐ์ ์์ ์ฒ๋ฆฌ๋๊ณ ํ์ผ์ด ์๋ฒ๋ก ์ ์ก๋์ง ์์ต๋๋ค.
์ฒ์ FFmpeg.wasm์ ์ ํ์ ๋ "์ด๊ฒ ๊ฐ๋ฅํ๋ค๊ณ ?" ํ๋๋ฐ, ์ค์ ๋ก ๋์๊ฐ๋ ๊ฒ์ ๋ณด๋ฉด ์์ง๋ ์ ๊ธฐํฉ๋๋ค.