[TIL] 211213

dev·2021년 12ė›” 13ėž
0

TIL

ëŠĐ록 ëģīęļ°
117/204
post-thumbnail

📝 ė˜Ī늘 한 ęēƒ

  1. video recorder - getUserMedia / MediaRecorder / a.download

  2. webassembly video transcode - FFmpeg.wasm / webm → mp4 / file → blob → url


📚 ë°°ėšī ęēƒ

1. video recorder

1) recorder.js íŒŒėž ėķ”ę°€

client íīë”ė— recorder.js íŒŒėžė„ 만든 후 webpackėī ėīëĨž ëģ€í™˜í•  눘 ėžˆë„ëĄ webpack.config.js íŒŒėžė—ė„œ entry뗐 ėķ”ę°€í•œë‹Ī.
webpackė„ ë‹Īė‹œ ė‹œėž‘í•ī ëģ€ęē―ė‚Ží•­ė„ ė—…ë°ėīíŠļ한ë‹Ī.

// webpack.config.js
module.exports = {
  entry: {
    main: "./src/client/js/main.js",
    videoPlayer: "./src/client/js/videoPlayer.js",
    recorder: "./src/client/js/recorder.js",
  },
  // ėĪ‘ëžĩ
};

upload.pug íŒŒėžė— recorder.js íŒŒėžė„ ė—°ęē°í•œë‹Ī.
server.js íŒŒėžė—ė„œ 맀렕í•īėĪ€ route ėīëĶ„ė„ ė°ļęģ í•ī upload.pug íŒŒėžė— scriptëĨž ėķ”ę°€í•œë‹Ī.

//- upload.pug

block scripts
  script(src="/static/js/recorder.js")
// server.js
app.use("/static", express.static("assets"));

2) ë…đ화 ëē„튞 만ë“Īęļ°

upload 페ėīė§€ė—ė„œ user가 ë…đ화 ëē„íŠžė„ 눌럮 videoëĨž ë…đ화할 눘 ėžˆë„ëĄ 하ë Īęģ  í•œë‹Ī.

① ėđīëĐ”ëžė™€ ė˜Ī디ė˜Ī뗐 대한 ė ‘ęķŒ ęķŒí•œ 가ė ļė˜Īęļ°: user가 ë…đ화 ëē„íŠžė„ 누ëĨīëĐī userė˜ ėđīëĐ”ëžė™€ ė˜Ī디ė˜Ī뗐 대한 ė ‘ę·ž ęķŒí•œė„ ė–ŧ는ë‹Ī.
② ëđ„ë””ė˜Ī ė‹Īė‹œę°„ ëģīęļ° & ëŊļëĶŽëģīęļ°: ë…đ화된 videoëĨž ë‹Īėšī로드하ęļ° ė „ė— user가 ëŊļëĶŽ ëģž ėˆ˜ ėžˆë„ëĄ 한ë‹Ī.
â‘Ē ëđ„ë””ė˜Ī ë…đ화 및 ë‹Īėšī로드: videoëĨž ë…đ화한 후 ë‹Īėšī로드 할 눘 ėžˆë„ëĄ 한ë‹Ī.

(1) upload.pug íŒŒėž ėˆ˜ė •

block content
  div
    button#startBtn Start Recording

(2) getUserMedia()

MDN - mediaDevices.getUserMedia() ė°ļęģ 

① navigator.mediaDevices.getUserMedia() í•Ļėˆ˜ëŠ” mediaStream 객ėēīëĨž 반환í•ĻėœžëĄœėĻ userė˜ navigatorė—ė„œ ėđīëĐ”ëžė™€ ė˜Ī디ė˜ĪëĨž 가ė ļë‹Ī ėĪ€ë‹Ī.
ę·ļ런데 ėđīëĐ”ëžė™€ ė˜Ī디ė˜Ī는 가ė ļė˜Ī는 데 ė‹œę°„ėī ęąļëĶŽęļ° ë•ŒëŽļ뗐 async & await 또는 promiseëĨž ė‚ŽėšĐí•īė•ž 한ë‹Ī.

ðŸ’Ą regeneratorRuntime

프론íŠļė—”ë“œ ėƒė—ė„œ async & awaitė„ ė‚ŽėšĐ하ë ĪëĐī regeneratorRuntimeė„ ė„Īėđ˜í•īė•ž 한ë‹Ī.

$ npm i regenerator-runtime

async & awaitė„ ė‚ŽėšĐ할 íŒŒėžė—ė„œ 링렑 import 할 ėˆ˜ë„ ėžˆė§€ë§Œ
ė—Žęļ°ė„œëŠ” 프론íŠļė—”ë“œ ėƒė˜ ëŠĻ든 ėžë°”ėŠĪ큎ëĶ―íŠļ íŒŒėžė—ė„œ async & awaitė„ ė‚ŽėšĐ할 눘 ėžˆë„ëĄ
client íīë”ė˜ main.js íŒŒėžė—ė„œ regeneratorRuntimeė„ import 한 후
base.pug íŒŒėžė— script로 main.js íŒŒėžė„ ė—°ęē°í–ˆë‹Ī.

// main.js
import regeneratorRuntime from "regenerator-runtime";
//- base.pug

// ėĪ‘ëžĩ

script(src="/static/js/main.js")
block scripts

ėī렜 base.pug íŒŒėžė—ė„œ main.js íŒŒėžė˜ regeneratorRuntimeė„ 로드하ęģ 
upload.pug íŒŒėžė—ė„œ recorder.js íŒŒėžė„ 로드í•ĻėœžëĄœėĻ
recorder.js íŒŒėž ė•ˆė—ė„œ (async & awaitė„ ė‚ŽėšĐ한) getUserMedia() í•Ļ눘ëĨž ė‹Ī행할 눘 ėžˆë‹Ī.

② (땄링 videoëĨž ë…đí™”í•˜ė§€ëŠ” ė•ŠėŒ) ėđīëĐ”ëžė™€ ė˜Ī디ė˜Ī뗐 대한 ė ‘ę·ž ęķŒí•œė„ ė–ŧė–ī뙀 ë…đ화된 videoëĨž user가 ëŊļëĶŽ ëģž ėˆ˜ ėžˆë„ëĄ ė―”ë“œëĨž ėž‘ė„ąí•œë‹Ī.
upload.pug íŒŒėžė— srcëĨž ëķ€ė—Ží•īėĢžė§€ ė•Šė€ video 태ę·ļëĨž ėķ”ę°€í•˜ęģ , getUserMedia() í•Ļėˆ˜ëĄœëķ€í„° ė–ŧė€ mediaStream 객ėēīëĨž video뗐 ë„Ģė–īėĪ€ë‹Ī.

// recorder.js
const startBtn = document.getElementById("startBtn");
const video = document.getElementById("preview");

const handleStart = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { width: 1280px, height: 720px },
    audio: true,
  }); // mediaStream 객ėēīëĨž 반환
  video.srcObject = stream; // video 태ę·ļ뗐 streamė„ ë„Ģė–īėΌ
  video.play(); // ęē°ęģžė ėœžëĄœ ë…đ화 ëē„íŠžė„ 누ëĨīëĐī ėđīëД띾가 í™œė„ąí™”ëĻ (ëŊļëĶŽëģīęļ°)
};

startBtn.addEventListener("click", handleStart);

ėī렜 ë…đ화 ëē„íŠžė„ 누ëĨīëĐī ėđīëД띾가 í™œė„ąí™”ë˜ė–ī ë‚ī ėķ”레한 ëŠĻėŠĩėī 화ëĐī뗐 ëœĻ는 ęēƒė„ 확ėļ할 눘 ėžˆë‹Ī.

(3) addEventListener / removeEventListener

í˜„ėžŽëŠ” ë…đ화 ëē„íŠžė„ ëˆŒëŸŽė•ž ëŊļëĶŽëģīęļ° í™”ëĐīėī 뜮ë‹Ī.
ėīëĨž ⓐ upload 페ėīė§€ëĨž ë“Īė–ī가ëĐī (ėđīëĐ”ëžė™€ ė˜Ī디ė˜Ī ęķŒí•œė„ ė–ŧė–īė˜Ļ 후) 바로 ëŊļëĶŽëģīęļ° í™”ëĐīėī ëœĻ도록 하ęģ , ⓑ ë…đ화 ëē„íŠžė„ 누ëĨīëĐī ė‹Īė œëĄœ ë…đ화가 ė‹œėž‘ë˜ęģ , ëē„íŠžė„ ë‹Īė‹œ 누ëĨīëĐī ë…đ화가 ėĪ‘ë‹Ļ되도록 ėˆ˜ė •í–ˆë‹Ī.

// recorder.js

// ⓑ - 2
const handleStop = () => {
  startBtn.innerText = "Start Recording";
  startBtn.removeEventListener("click", handleStop);
  startBtn.addEventListener("click", handleStart);
};

// ⓑ - 1
const handleStart = () => {
  startBtn.innerText = "Stop Recording";
  startBtn.removeEventListener("click", handleStart);
  startBtn.addEventListener("click", handleStop);
};

// ⓐ
const init = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { width: 1280, height: 720 },
    audio: false,
  });
  video.srcObject = stream;
  video.play();
};

init();

// ⓑ
startBtn.addEventListener("click", handleStart);

(4) MediaRecorder

MDN - MediaRecorder ė°ļęģ 

â‘Ē MediaRecorderëĨž ėīėšĐí•ī ëđ„ë””ė˜Ī í˜đė€ ė˜Ī디ė˜ĪëĨž ë…đ화하ęģ  ë‹Īėšī로드 할 눘 ėžˆë‹Ī.

ðŸ’Ą ëđ„ë””ė˜Ī ë…đ화

  1. MediaRecorder가 streamė„ ë°›ė•„ė˜Ļë‹Ī.

  2. ė‹Ī렜 ë…đ화가 ė‹œėž‘ë˜ë„ëĄ ë…đ화 ëē„íŠžė„ 눌럮 MediaRecorderëĨž í™œė„ąí™”í•œë‹Ī.
    ë…đ화된 videoëĨž ëŊļëĶŽ ëģž ėˆ˜ ėžˆë‹Ī.

  3. ë…đ화 ëē„íŠžė„ ë‹Īė‹œ 누ëĨīëĐī MediaRecorder가 ëđ„í™œė„ąí™”ë˜ė–ī ë…đ화가 ėĪ‘ë‹Ļ된ë‹Ī.

  4. ë…đ화가 ėĪ‘ë‹Ļ되ëĐī ė €ėžĨ된 데ėīí„°ė˜ ėĩœėĒ… videoëĨž ë‹īė€ dataavailable ėīëēĪíŠļ가 ë°œėƒí•œë‹Ī.
    MediaRecorder.ondataavailable í•Ļ눘ëĨž ėīėšĐí•ī dataavailable ėīëēĪíŠļëĨž í•ļë“Ī링할 눘 ėžˆë‹Ī.
    dataavailable ėīëēĪíŠļ는 data ė†ė„ąė„ 氀맄 BlobEvent로ėĻ ė—Žęļ°ė— ë…đ화된 video뗐 대한 ė •ëģī가 ë‹īęēĻ ėžˆë‹Ī.

    event.data는 ėžėĒ…ė˜ íŒŒėž(blob - ë’Īė—ė„œ ė„Ī멅)ėļ데, ėīëĨž ėīėšĐí•ī ëŽīė–ļ가ëĨž 하ęļ° ėœ„í•īė„œëŠ” url뗐 ė§‘ė–īë„Ģė–ī í•īë‹đ íŒŒėžė— ė ‘ę·ží•  눘 ėžˆë„ëĄ 만ë“Īė–īė•ž 한ë‹Ī.
    URL.createObjectURL()ė€ ëļŒëžėš°ė €ė˜ ëДëŠĻëĶŽė—ė„œë§Œ ėĄīėžŽí•˜ëŠ” urlė„ 만든ë‹Ī.
    ėī urlė€ ė‹Īė œëĄœ ė›đ ė‚ŽėīíŠļė—ëŠ” ėĄīėžŽí•˜ė§€ ė•ŠëŠ” url로ėĻ ë‹Ļėˆœížˆ ė ‘ę·ží•  눘 ėžˆëŠ” íŒŒėžė„ 가ëĶŽí‚Īęģ  ėžˆë‹Ī.
    ë‹Īė‹œ 말í•ī, í•īë‹đ urlė€ ëļŒëžėš°ė €ė— ė˜í•ī 만ë“Īė–īė ļ ëļŒëžėš°ė € ėƒė—ė„œë§Œ ėĄīėžŽí•˜ëŠ” ęēƒėœžëĄœėĻ, ëļŒëžėš°ė €ė˜ ëДëŠĻëĶŽė— íŒŒėžė„ ė €ėžĨ한 후 ëļŒëžėš°ė €ę°€ ę·ļ íŒŒėžė— ė ‘ę·ží•  눘 ėžˆë„ëĄ 한ë‹Ī.
    ė‹Īė œëĄœ ėĄīėžŽí•˜ëŠ” urlėī ė•„ë‹ˆëž ëļŒëžėš°ė €ę°€ íŒŒėžė„ ëģīė—ŽėĢžëŠ” ë°Đëē•ėž ëŋėīëŊ€ëĄœ ë°ąė—”ë“œė—ëŠ” ėĄīėžŽí•˜ė§€ ė•Šęļ° ë•ŒëŽļ뗐 ė„œëē„ę°€ ė—īë Ī ėžˆė–ī도 ėŧīí“Ļ터ëĨž ęŧë‹Ī ë‹Īė‹œ ëŒė•„ė˜ĪëĐī í•īë‹đ urlė€ ė—†ė–īė§„ë‹Ī.

// recorder.js
let stream;
let recorder;

const handleStop = () => {
  // ėĪ‘ëžĩ
  recorder.stop(); // 3
};

const handleStart = () => {
  // ėĪ‘ëžĩ
  recorder = new MediaRecorder(stream); // 1
  recorder.ondataavailable = (event) => {
    const videoFile = URL.createObjectURL(event.data); // 4
    video.srcObject = null;
    video.src = videoFile;
    video.loop = true;
    video.play();
  } 
  recorder.start(); // 2
};

const init = async () => {
  stream = await navigator.mediaDevices.getUserMedia({
    video: { width: 1280, height: 720 },
    audio: false,
  });
  video.srcObject = stream;
  video.play();
};

startBtn.addEventListener("click", handleStart);

ðŸ’Ą ëđ„ë””ė˜Ī ë‹Īėšī로드

Stop Recording ëē„íŠžė„ 누ëĨīëĐī ëē„튞ėī Download Video ëē„íŠžėœžëĄœ 바뀌도록 한 후 ë‹Īė‹œ ę°™ė€ ëē„íŠžė„ ëˆŒë €ė„ 때 ë…đ화한 video가 ë‹Īėšī되도록 한ë‹Ī.

// recorder.js
const handleStop = () => {
  startBtn.innerText = "Download Video";
  startBtn.removeEvenetListener("click", handleStop);
  startBtn.addEventListener("click", handleDownload);
  recorder.stop();
};

a 태ę·ļ(ė͉, 링큎)ëĨž 만ë“Īė–ī href ė†ė„ąė— ėœ„ė—ė„œ ė–ŧė–īė˜Ļ videoFileė„ ë„Ģ는ë‹Ī.
a.download는 링큎ëĨž íīëĶ­ ė‹œ í•īë‹đ url로 ėī동하는 ëŒ€ė‹  urlė„ ė €ėžĨ하도록 한ë‹Ī. ėī때 íŒŒėž ė €ėžĨ ė‹œ ė œė•ˆí•  íŒŒėž ėīëĶ„ė„ ė§€ė •í•  눘 ėžˆë‹Ī.
a 태ę·ļëĨž body뗐 ėķ”ę°€í•œë‹Ī.
링큎ëĨž íīëĶ­í•˜ë„ëĄ 한ë‹Ī. (ė‹Īė œëĄœ ė‚ŽėšĐėžę°€ 링큎 íīëĶ­x)

// recorder.js
let videoFile;

const handleDownload = () => {
  const a = document.createElement("a");
  a.href = videoFile;
  a.download = "myRecording.webm";
  document.body.appendChild(a);
  a.click();
};

ėīëĨž í†ĩí•ī user는 Download Video ëē„íŠžė„ ëˆŒë €ė„ 때 링큮가 가ëĶŽí‚Ī는 íŒŒėžė„ ė €ėžĨ할 눘 ėžˆë‹Ī.


2. WebAssembly video transcode

ę·ļ런데 í˜„ėžŽ ë‹Īėšī로드한 ëđ„ë””ė˜Ī는 ėžŽėƒė€ ë˜ė§€ë§Œ ęļļėī(duration)ëĨž ę°–ė§€ ëŠŧ하ęļ° ë•ŒëŽļ뗐 ėī ė ė„ í•īęē°í•īė•ž 한ë‹Ī.
또한 ëŠĻ든 ęļ°ęļ°ë“Īėī webmė„ ėīí•īí•˜ė§€ëŠ” ëŠŧ하ęļ° ë•ŒëŽļ뗐 webm íŒŒėžė„ mp4 íŒŒėžëĄœ 바ęŋ”ė•ž 하ęģ , ëđ„ë””ė˜Īė—ė„œ ėļë„Īėžë„ ėķ”ėķœí•īė•ž 한ë‹Ī.
ėīëĨž ėœ„í•ī ffmpeg.wasmė„ ėīėšĐ할 눘 ėžˆë‹Ī.

1) FFmpeg뙀 WebAssembly

ffmpeg란 ëđ„ë””ė˜Ī나 ė˜Ī디ė˜Ī뙀 ę°™ė€ ëŊļ디ė–ī íŒŒėžė„ ë‹Īė–‘í•œ ėĒ…ëĨ˜ė˜ 형태로 ęļ°ëĄí•˜ęģ  ëģ€í™˜í•īėĢžëŠ” ėŧīí“Ļ터 프로ę·ļëžĻė„ 말한ë‹Ī.

똈ëĨž ë“Īė–ī, FFmpegëĨž ėīėšĐí•ī ëđ„ë””ė˜ĪëĨž ė••ėķ•í•˜ęą°ë‚˜ ëđ„ë””ė˜Īė—ė„œ ė˜Ī디ė˜ĪëĨž ėķ”ėķœí•  눘 ėžˆë‹Ī.
ëđ„ë””ė˜Īė—ė„œ ėŠĪ큎ëĶ°ėƒ·ė„ ė°ęą°ë‚˜ ëđ„ë””ė˜ĪëĨž gif íŒŒėžëĄœ 만ë“Ī거나 ëđ„ë””ė˜Ī뗐 ėžë§‰ė„ ėķ”가할 ėˆ˜ë„ ėžˆë‹Ī.
또한, ėœ íŠœëļŒėē˜ëŸž ę°™ė€ ëđ„ë””ė˜ĪëĨž ė—ŽëŸŽ ę°œė˜ 폎맷ęģž í™”ė§ˆëĄœ ėļė―”ë”Đ할 ėˆ˜ë„ ėžˆë‹Ī.

ę·ļ런데 FFmpeg는 ë°ąė—”ë“œė—ė„œ ė‹Ī행í•īė•ž 하ęļ° ë•ŒëŽļ뗐 ė›ëž˜ëŠ” ė„œëē„ëĨž ėīėšĐ한 만큾 ëđ„ėšĐė„ ė§€ëķˆí•īė•ž 한ë‹Ī.
ėī ëŽļ렜ëĨž í•īęē°í•˜ęļ° ėœ„í•ī WebAssemblyëĨž ėīėšĐ한ë‹Ī.

ė›đ ė–īė…ˆëļ”ëĶŽëŠ” ėžë°”ėŠĪ큎ëĶ―íŠļė˜ ëŽīëĪėžęđŒ? & ėīˆëģī ę°œë°œėžëĨž ėœ„í•œ ė›đ ė‹ ęļ°ėˆ  WebAssembly ė„Ī멅 ė°ļęģ 

WebAssembly란 프론íŠļė—”ë“œė—ė„œ ë§Ī뚰 ëđ ëĨīęēŒ ė―”ë“œëĨž ė‹Ī행할 눘 ėžˆë„ëĄ 하는 氜ë°Đ형 표ėĪ€ėīë‹Ī.
ëļŒëžėš°ė €ė—ė„œ ė‹Ī행 가ëŠĨ한 ė–ļė–ī는 ęļ°ëģļė ėœžëĄœ HTML, CSS, JavaScriptėīė§€ë§Œ, ėžë°”ėŠĪ큎ëĶ―íŠļ가 ė•„ë‹Œ ë‹ĪëĨļ ėĒ…ëĨ˜ė˜ ė–ļė–ī 또한 WebAssembly로 ėŧīíŒŒėží•ĻėœžëĄœėĻ ėžë°”ėŠĪ큎ëĶ―íŠļ가 ė•„ë‹Œ ė–ļė–ī도 ëļŒëžėš°ė €ė—ė„œ ė‹Ī행할 눘 ėžˆë‹Ī.

똈ëĨž ë“Īė–ī, ęēŒėž„ ë“ąė€ ėžë°”ėŠĪ큎ëĶ―íŠļ로 만ë“Īęļ°ė— ėžë°”ėŠĪ큎ëĶ―íŠļ ė†ë„ę°€ 너ëŽī 느ëĶŽęą°ë‚˜ ėžë°”ėŠĪ큎ëĶ―íŠļ로 ęĩŽí˜„í•  눘 ė—†ëŠ” ëķ€ëķ„ë“Īėī ėžˆė–ī ë‹ĪëĨļ ė–ļė–īëĨž ė‚ŽėšĐí•ī 만든 후 ė‹Īė œëĄœ ė‹Ī행하ęļ° ėœ„í•īė„œëŠ” ęēŒėž„ ė„Īėđ˜ ęģžė •ė„ ęą°ėģė•ž 한ë‹Ī.
ę·ļ럮나 WebAssembly로 ėŧīíŒŒėží•˜ëĐī ė„Īėđ˜í•˜ė§€ ė•Šęģ ë„ ëļŒëžėš°ė €ė—ė„œ ęēŒėž„ė„ ė‹Ī행할 눘 ėžˆë‹Ī.
ėīëĨž í†ĩí•ī ė‹Ī행 ëđ„ėšĐėī 큰 프로ę·ļëžĻë“Ī도 ëđ„ėšĐė„ ė ˆė•―í•˜ęģ  ëļŒëžėš°ė €ė—ė„œ ė‹Ī행할 눘 ėžˆë‹Ī.

한íŽļ, 뚰ëĶŽëŠ” WebAssembly로 ėŧīíŒŒėžë  ė–ļė–īëĨž ėīėšĐí•ī 프로ę·ļëžĻė„ 만ë“Ī ëŋ, ëģīí†ĩ WebAssemblyëĨž 링렑 ėž‘ė„ąí•˜ė§€ëŠ” ė•ŠëŠ”ë‹Ī.

2) FFmpeg.wasm

ė•žė„œ 말했ë“Ŋ ëđ„ë””ė˜ĪëĨž ë‹Īė–‘í•œ 형태로 ëģ€í™˜í•˜ëŠ” 데 FFmpegëĨž ė‚ŽėšĐ한ë‹Ī.
ėī때 ėœ íŠœëļŒė˜ ęē―ėš°ė—ëŠ” ė‚ŽėšĐėžę°€ ė—…ëĄœë“œí•œ ëđ„ë””ė˜ĪëĨž ę·ļë“Īė˜ ëđ„ė‹ž ė„œëē„ė—ė„œ ëģ€í™˜í•˜ė§€ë§Œ
Fmpeg.wasmëĨž ėīėšĐ하ëĐī ė‚ŽėšĐėžę°€ ė—…ëĄœë“œí•œ ëđ„ë””ė˜ĪëĨž ė‚ŽėšĐėžė˜ ëļŒëžėš°ė €ė—ė„œ ëģ€í™˜í•˜ë„록 할 눘 ėžˆë‹Ī.
FFmpeg는 ė›ëž˜ ėžë°”ėŠĪ큎ëĶ―íŠļ가 ė•„ë‹Œ C ė–ļė–ī로 만ë“Īė–īė§„ 프로ę·ļëžĻėīė§€ë§Œ, WebAssemblyëĨž ėīėšĐ하ëĐī ëļŒëžėš°ė €ė—ė„œ FFmpegëĨž ė‹Īí–‰ė‹œí‚Ž 눘 ėžˆęļ° ë•ŒëŽļėīë‹Ī.

ė—Žęļ°ė„œëŠ” handleDownload í•Ļ눘 ė•ˆė—ė„œ user가 videoëĨž ë‹Īėšī로드 하ęļ° ė „ė— mp4 íŒŒėžëĄœ 바ęŋ”ëģīë Īęģ  í•œë‹Ī.

(1) ė„Īėđ˜ 및 ė‚ŽėšĐ (FFmpeg 로드하ęļ°)

ffmpegwasm/ffmpeg.wasm ė°ļęģ 

ė•„ëž˜ 멅ë đė–īëĨž ė‹Īí–‰í•˜ė—Ž FFmpeg.wasmëĨž ė„Īėđ˜í•œë‹Ī.

$ npm install @ffmpeg/ffmpeg @ffmpeg/core

recorder.js íŒŒėžė—ė„œ ėīëĨž import 한ë‹Ī.

createFFmpeg()ëĨž í†ĩí•ī ffmpeg instanceëĨž 만든ë‹Ī.
{log: true}는 ė―˜ė†”ė—ė„œ 로ę·ļëĨž 확ėļ하ęļ° ėœ„í•ī ë„Ģė–īėĪ€ ė˜ĩė…˜ėīë‹Ī.

ė‚ŽėšĐėžę°€ ffmpegëĨž ė‚ŽėšĐ할 눘 ėžˆë„ëĄ load한ë‹Ī.
ė†Œí”„íŠļė›Ļė–ī가 ëŽīęą°ėšļ 눘 ėžˆėœžëŊ€ëĄœ awaitė„ ė‚ŽėšĐí•īė•ž 한ë‹Ī.
ėī는 뚰ëĶŽė˜ ė„œëē„ę°€ ė•„ë‹ˆëž ė‚ŽėšĐėžė˜ ėŧīí“Ļí„°ė—ė„œ ėžė–ī나는 ėžėīë‹Ī.

// recorder.js
import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg";

const hanldeDownload = async () => {
  const ffmpeg = createFFmpeg({ log: true });
  await ffmpeg.load();
  
  // ėĪ‘ëžĩ
};

(2) ffmpeg뗐 íŒŒėž 만ë“Īęļ°

ëĻžė €, FFmpeg ę°€ėƒė˜ ė„ļęģ„뗐 íŒŒėžė„ 만ë“Īė–īė•ž 한ë‹Ī.
ėī렇ęēŒ ë§Œë“Īė–īė§„ íŒŒėžė€ ė‹ĪėĄīí•˜ė§€ ė•Šė§€ë§Œ ëļŒëžėš°ė €ė˜ ëДëŠĻëĶŽė— ė €ėžĨ된ë‹Ī.

ėīëĨž ėœ„í•īė„œëŠ” 0ęģž 1ė˜ ė •ëģī(binaryData)ëĨž ė „í•īėĪ˜ė•ž 한ë‹Ī.
ë…đ화된 video뗐 대한 ė •ëģīëĨž ë‹īęģ  ėžˆëŠ” url, ė͉ videoFileėī 바로 ę·ļęēƒėīë‹Ī.
ë”°ëžė„œ, fetchFile()ė„ ėīėšĐí•ī videoFileė„ ė „í•īėĢžë„ëĄ 한ë‹Ī.

ffmpeg.FS("writeFile", "íŒŒėž ėīëĶ„.확ėžĨėž", await fetchFile(binaryData));

(3) íŒŒėž ëģ€í™˜í•˜ęļ° (webm → mp4)

ë‹ĪėŒėœžëĄœ, FFmpeg 멅ë đė–īëĨž ė‚ŽėšĐí•ī ėœ„ė—ė„œ 만든 ę°€ėƒė˜ íŒŒėžė„ ëģ€í™˜í•˜ë Īęģ  í•œë‹Ī.
FFmpegëĨž ė‚ŽėšĐėžė˜ ëļŒëžėš°ė €ė—ė„œ 로ë”Đ하ęļ° ë•ŒëŽļ뗐 FFmpeg 멅ë đė–ī 또한 ė‚ŽėšĐėžė˜ ëļŒëžėš°ė €ė—ė„œ ė‹Ī행되도록 할 눘 ėžˆë‹Ī.

ffmpeg.run()ė€ ė•žė„œ 만든 ę°€ėƒė˜ íŒŒėž(recording.webm)ė„ ę°€ėƒė˜ ėŧīí“Ļí„°ė—ė„œ inputėœžëĄœ ë°›ė€ 후 ėīëĨž ė§€ė •í•œ ęē°ęģžëŽž(output.mp4)로 ëģ€í™˜í•˜ë„록 한ë‹Ī.
ėī때 videoëĨž ėīˆë‹đ 60 í”„ë ˆėž„ėœžëĄœ ėļė―”ë”Đ하ęļ° ėœ„í•ī ė•„ëž˜ė™€ 같ėī ė―”ë“œëĨž ėž‘ė„ąí•˜ė˜€ë‹Ī.

await ffmpeg.run("-i", "recording.webm", "-r", "60", "output.mp4");

ėī렜 ę°€ėƒ íŒŒėž ė‹œėŠĪ템(ëļŒëžėš°ė €ė˜ ëДëŠĻëĶŽ)ė—ëŠ” output.mp4 íŒŒėžėī ėžˆë‹Ī.

(4) mp4 íŒŒėž ė‚ŽėšĐ하ęļ° (file → blob → url)

ė‹Īė œëĄœ íŒŒėžė„ 만ë“Īęļ° ėœ„í•ī ę°€ėƒė˜ ė„ļęģ„뗐 ėĄīėžŽí•˜ëŠ” ouput.mp4 íŒŒėžė„ ëķˆëŸŽė™€ė•ž 한ë‹Ī.

const mp4File = await ffmpeg.FS("readFile", "output.mp4");

ėī때 ëķˆëŸŽė˜Ļ íŒŒėž(mp4File)ė„ ė―˜ė†”ė—ė„œ 확ėļí•īëģīëĐī ë‹ĪėŒęģž ę°™ë‹Ī.

console.log(mp4File);
console.log(mp4File.buffer); 

ëķˆëŸŽė˜Ļ íŒŒėž(mp4File)ė€ ęļ°ëģļė ėœžëĄœ ėžë°”ėŠĪ큎ëĶ―íŠļ ė„ļęģ„ė—ė„œ íŒŒėžė„ 표현하는 ë°Đëē•ėļ Unit8Array(8ëđ„íŠļ ė–‘ė˜ ė •ėˆ˜ëĄœ ėīëĢĻė–īė§„ ë°°ė—ī) íƒ€ėž…ėīë‹Ī.
console.log(mp4File)ė„ ė‹Ī행한 ęē°ęģž ė―˜ė†” ė°―ė„ ė‚īíŽīëģīëĐī ėˆŦėžëĄœ ėīëĢĻė–īė§„ ęļļėī가 ë§Ī뚰 ęļī ë°°ė—īė„ 확ėļ할 눘 ėžˆë‹Ī.

ę·ļ럮나 ėīęēƒë§ŒėœžëĄœëŠ” ė•„ëŽīęēƒë„ 할 눘 ė—†ęļ° ë•ŒëŽļ뗐 ėī ë°°ė—īė„ blobėœžëĄœ 만ë“Īė–īė•ž 한ë‹Ī.
blobėī란 binary dataëĨž ë‹īęģ  ėžˆëŠ” íŒŒėžëĨ˜ė˜ ëķˆëģ€í•˜ëŠ” raw dataëĨž 말한ë‹Ī.

ę·ļ런데 Unit8Array(가ęģĩ 가ëŠĨ)ė„ blobėœžëĄœ 만ë“Ī ėˆ˜ëŠ” ė—†ë‹Ī.
ė‹Ī렜 íŒŒėžė„ ė˜ëŊļ하는 raw binary data(ëŊļ가ęģĩ ėīė§„ 데ėī터)ëĨž ė‚ŽėšĐ하ęļ° ėœ„í•īė„œëŠ” ArrayBufferëĨž ėīėšĐí•īė•ž 한ë‹Ī.

ėīëĨž í†ĩí•ī ė‹Ī렜 íŒŒėžėļ blobė„ 만든ë‹Ī.
ėī때 ėžë°”ėŠĪ큎ëĶ―íŠļ뗐ęēŒ ėīęēƒėī mp4 íŒŒėžėī띞ęģ  ė•Œë ĪėĪ˜ė•ž 한ë‹Ī.

const mp4Blob = new blob([mp4File.buffer], { type: "video/mp4" });

ė‹Ī렜 íŒŒėžė— ė ‘ę·ží•  눘 ėžˆë„ëĄ ė‹Ī렜 íŒŒėžė— 대한 ė •ëģīëĨž ë‹īęģ  ėžˆëŠ” blobė„ ę·ļ íŒŒėžė„ 가ëĶŽí‚Ī는 url로 만든ë‹Ī.

const mp4Url = URL.createObjectURL(mp4Blob);

(5) ėĩœėĒ… ė―”ë“œ

const handleDownload = async () => {
  // FFmpegëĨž 로드한ë‹Ī
  const ffmpeg = createFFmpeg({
    log: true,
    corePath: "https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js",
  });
  await ffmpeg.load();

  // FFmpeg ė„ļęģ„뗐 íŒŒėžė„ 만든ë‹Ī - ė‹ĪėĄīí•˜ė§€ëŠ” ė•Šė§€ë§Œ ëļŒëžėš°ė € ëДëŠĻëĶŽė— ė €ėžĨ된ë‹Ī
  ffmpeg.FS("writeFile", "recording.webm", await fetchFile(videoFile));

  // íŒŒėžė„ ëģ€í™˜í•œë‹Ī (webm → mp4)
  await ffmpeg.run("-i", "recording.webm", "-r", "60", "output.mp4");

  // íŒŒėž → blob → url
  const mp4File = await ffmpeg.FS("readFile", "output.mp4");
  const mp4Blob = new Blob([mp4File.buffer], { type: "video/mp4" });
  const mp4Url = URL.createObjectURL(mp4Blob);

  // a.download ėīėšĐí•ī ëđ„ë””ė˜Ī íŒŒėž ë‹Īėšī로드
  const a = document.createElement("a");
  a.href = mp4Url;
  a.download = "myRecording.mp4";
  document.body.appendChild(a);
  a.click();
};

ėī렜 ë‹Īėšī로드된 videoëĨž ëģīëĐī ęļļėī가 ė •ėƒė ėœžëĄœ ėĢžė–īė ļ ėžˆęģ , 확ėžĨėžę°€ mp4ėļ ęēƒė„ 확ėļ할 눘 ėžˆë‹Ī.

3) ė—ëŸŽ í•īęē°

(1) Cannot find module '@ffmpeg/core'

ėĩœė‹  ëē„ė „ė˜ ffmpeg/coreëĨž ė„Īėđ˜í•œë‹Ī.

$ npm install @ffmpeg/core@latest

ffpmeg instanceëĨž 만ë“Ī 때 corePathëĨž ë‹ĪėŒęģž ę°™ėī ėķ”ę°€í•œë‹Ī.

// recorder.js
const ffpmeg = createFFmpeg({
  log: true,
  corePath: "https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js", // ėķ”ę°€ ❗
});

(2) SharedArrayBuffer is not defined

server.js íŒŒėžė—ė„œ ëžėš°í„°ë“Ī ė•žė— ë‹ĪėŒęģž ę°™ė€ ė―”ë“œëĨž ėķ”ę°€í•œë‹Ī.

// server.js
app.use((req, res, next) => {
  res.header("Cross-Origin-Embedder-Policy", "require-corp");
  res.header("Cross-Origin-Opener-Policy", "same-origin");
  next();
});

header.pug뙀 profile.pug íŒŒėžė˜ github ė•„ë°”íƒ€ė— crossoriginė„ ėķ”ę°€í•œë‹Ī.

+github로ëķ€í„° 가ė ļė˜Ļ ė•„ë°”íƒ€ė™€ 링렑 프로필 ė‚Žė§„ė„ ė—…ëĄœë“œí•īė„œ 만든 ė•„ë°”íƒ€ëĨž 나눈ë‹Ī.
ėī렇ęēŒ ė•ˆ 하ëĐī 링렑 ė—…ëĄœë“œí•œ 프로필 ė‚Žė§„ė˜ ęē―로가 ėƒëŒ€ ęē―로가 되ė–ī ė—ëŸŽę°€ ë°œėƒí•œë‹Ī.

div.profile-outer-box
  div.profile-inner-box
    if (loggedInUser.avatarUrl).startsWith("h") 
      img(src=loggedInUser.avatarUrl,crossorigin).profile-img
    else
      img(src="/" + loggedInUser.avatarUrl).profile-img

âœĻ ë‚īėž 할 ęēƒ

  1. Thumbnail
profile
dev log

0ę°œė˜ 댓ęļ€