[TIL] 211213

Lee Syong·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
ëŠĨ동ė ėœžëĄœ ė‚īėž, 행ëģĩ하ęēŒðŸ˜

0개ė˜ 댓ęļ€