[TIL] 211214

Lee SyongΒ·2021λ…„ 12μ›” 14일
0

TIL

λͺ©λ‘ 보기
118/204
post-thumbnail

πŸ“ 였늘 ν•œ 것

  1. 썸넀일 μΆ”μΆœ 및 λ‹€μš΄λ‘œλ“œ / 썸넀일 μ—…λ‘œλ“œ 및 보여주기

  2. flexbox / gridμ—μ„œ λ§ˆμ§€λ§‰ 행을 μ •λ ¬ν•˜λŠ” 방법


πŸ“š 배운 것

1. WebAssembly video transcode

1) 썸넀일 μΆ”μΆœ 및 λ‹€μš΄λ‘œλ“œ

(1) -ss / -frames:v

// recorder.js
const handleDownload = () => {
  // μ€‘λž΅
  
  // recording.webm을 input으둜 λ°›μ•„ output.mp4으둜 λ³€ν™˜
  await ffmpeg.run("-i", "recording.webm", "-r", "60", "output.mp4");
  
  // recording.webm을 input으둜 λ°›μ•„ thumbnail.jpgλ₯Ό 생성 (μΆ”κ°€ ❗)
  await ffmpeg.run(
    "-i",
    "recording.webm",
    // μ˜μƒμ˜ νŠΉμ • μ‹œκ°„λŒ€(μ—¬κΈ°μ„œλŠ” 1초)둜 μ΄λ™ν•œλ‹€.
    "-ss",
    "00:00:01",
    // μ΄λ™ν•œ μ‹œκ°„λŒ€μ˜ 첫 ν”„λ ˆμž„μ„ μŠ€ν¬λ¦°μƒ·μœΌλ‘œ 1μž₯ μ°λŠ”λ‹€.
    "-frames:v",
    "1",
    // κ·Έ μŠ€ν¬λ¦°μƒ·μ„ μΈλ„€μΌλ‘œ λ§Œλ“ λ‹€.
    "thumbnail.jpg"
  );
  
  const mp4File = await ffmpeg.FS("readFile", "output.mp4");
  
  // μ€‘λž΅
};

가상 파일 μ‹œμŠ€ν…œ(FS)의 λ©”λͺ¨λ¦¬μ— thumbnail.jpg이 λ§Œλ“€μ–΄μ§„λ‹€.

(2) file β†’ blob β†’ url / a.download

μ•žμ„œ webm νŒŒμΌμ„ mp4 파일둜 λ³€ν™˜ν–ˆλ˜ κ²ƒμ²˜λŸΌ file β†’ blob β†’ url 과정을 거쳐 thumbnail.jpg에 μ ‘κ·Όν•  수 μžˆλ„λ‘ λ§Œλ“€μ–΄μ€€λ‹€.

// recorder.js
const handleDownload = () => {
  // μ€‘λž΅ 
  const thumbFile = await ffmpeg.FS("readFile", "thumbnail.jpg");
  const thumbBlob = new Blob([thumbFile.buffer], { type: "image/jpg" });
  const thumbUrl = URL.createObjectURL(thumbBlob);
};

thumbUrl둜 μ ‘κ·Όν•  수 μžˆλŠ” a νƒœκ·Έλ₯Ό λ§Œλ“€μ–΄ thumbnail.jpgλ₯Ό λ‹€μš΄λ‘œλ“œ ν•  수 μžˆλ„λ‘ ν•œλ‹€.

// recorder.js
const handleDownload = () => {
  // μ€‘λž΅  
  const thumbA = document.createElement("a");
  thumbA.href = thumbUrl;
  thumbA.download = "myThumbnail.jpg";
  document.body.appendChild(thumbA);
  thumbA.click();
};

파일 λ‹€μš΄λ‘œλ“œκ°€ λͺ¨λ‘ 끝났닀면 이듀을 FFmpeg의 가상 파일 μ‹œμŠ€ν…œ(FS)μ—μ„œ unlink ν•΄μ•Ό ν•œλ‹€.
λΈŒλΌμš°μ €μ˜ λ©”λͺ¨λ¦¬μ— 계속 λ‘”λ‹€λ©΄ 쓸데없이 μ›Ή μ‚¬μ΄νŠΈ 속도가 느렀질 수 있기 λ•Œλ¬Έμ΄λ‹€.
μ†ŒμŠ€ 파일인 recording.webm 파일과 이λ₯Ό 톡해 λ§Œλ“  output.mp4 그리고 thumbnail.jpg νŒŒμΌκΉŒμ§€ unlink ν•˜λ„λ‘ ν•œλ‹€.

// recorder.js
const handleDownload = () => {
  // μ€‘λž΅
  ffmpeg.FS("unlink", "recording.webm");
  ffmpeg.FS("unlink", "output.mp4");
  ffmpeg.FS("unlink", "thumbnail.jpg");
};

λ˜ν•œ, ν•΄λ‹Ή νŒŒμΌμ— μ ‘κ·Όν•  수 μžˆλŠ” url도 μ‚­μ œν•΄μ•Ό ν•œλ‹€.
URL.revokeObjectURL()을 μ΄μš©ν•΄ webm blob url인 videoFile, mp4 blob url인 mp4Url, jpg blob url인 thumbUrl을 μ‚­μ œλ‹€.

// recorder.js
const handleDownload = () => {
  // μ€‘λž΅  
  URL.revokeObjectURL(videoFile);
  URL.revokeObjectURL(mp4Url);
  URL.revokeObjectURL(thumbUrl);
};

2) handleDownload ν•¨μˆ˜ μ΅œμ’… μˆ˜μ •

(1) startBtn β†’ actionBtn

upload.pug νŒŒμΌμ—μ„œ λ²„νŠΌμ˜ id μ„ νƒμžλ₯Ό μˆ˜μ •ν•œλ‹€.
recorder.js νŒŒμΌμ—μ„œ λ³€μˆ˜ 이름을 μˆ˜μ •ν•œλ‹€.

(2) fileName을 λ³€μˆ˜ μ΄λ¦„μœΌλ‘œ λ³€ν™˜

λ¬Έμžμ—΄μΈ 파일 이름듀을 λ³€μˆ˜λ‘œ 지정해 λͺ¨λ‘ 바꿔쀬닀.

// recorder.js
const files = {
  input: "recording.webm",
  output: "output.mp4",
  thumb: "thumbnail.jpg",
};

(3) downloadFile ν•¨μˆ˜ 생성

handleDonwload ν•¨μˆ˜ μ•ˆμ—μ„œ a νƒœκ·Έλ₯Ό λ§Œλ“€μ–΄ νŒŒμΌμ„ λ‹€μš΄λ‘œλ“œ ν•˜λŠ” 뢀뢄이 λ°˜λ³΅λœλ‹€.
이λ₯Ό λ”°λ‘œ ν•¨μˆ˜λ‘œ λ§Œλ“€μ–΄ λΉΌλƒˆλ‹€.

// recorder.js
const downloadFile = (fileUrl, fileName) => {
  const a = document.createElement("a");
  a.href = fileUrl;
  a.download = fileName;
  document.body.appendChild(a);
  a.click();
};

const handleDownload = () => {
  // μ€‘λž΅
  downloadFile(mp4Url, "myRecording.mp4");
  downloadFile(thumbUrl, "myThumbnail.mp4");
  // μ€‘λž΅
};

(4) λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 연속 클릭 방지

λ‹€μš΄λ‘œλ“œκ°€ μ™„λ£Œλ˜κΈ° 전에 λ‹€μš΄λ‘œλ“œ λ²„νŠΌμ΄ μ—°μ†μœΌλ‘œ ν™œμ„±ν™”λ˜λŠ” 것을 λ§‰λŠ”λ‹€.
λ‹€μš΄λ‘œλ“œκ°€ μ™„λ£Œλ˜λ©΄ λ‹€μ‹œ ν™œμ„±ν™”λ˜λ„λ‘ ν•œλ‹€.

// recorder.js
const handleDownload = () => {
  actionBtn.removeEventListener("click", handleDownload);
  actionBtn.innerText = "Transcoding...";
  actionBtn.disabled = true;
  
  // μ€‘λž΅

  actionBtn.disabled = false;
  actionBtn.innerText = "Record Again";
  init(); // λ‹€μ‹œ λΉ„λ””μ˜€λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ λ³Ό 수 μžˆλ„λ‘ 함 ❗ 이걸 μ•ˆ μ“°λ©΄ 이전에 λ…Ήν™”λœ λΉ„λ””μ˜€κ°€ 계속 반볡 μž¬μƒλ¨
  actionBtn.addEventListener("click", handleDownload);
};

(5) recorder.js μ΅œμ’… μ½”λ“œ

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

const actionBtn = document.getElementById("actionBtn");
const video = document.getElementById("preview");

let stream;
let recorder;
let videoFile;

const files = {
  input: "recording.webm",
  output: "output.mp4",
  thumb: "thumbnail.jpg",
};

const downloadFile = (fileUrl, fileName) => {
  const a = document.createElement("a");
  a.href = fileUrl;
  a.download = fileName;
  document.body.appendChild(a);
  a.click();
};

const handleDownload = async () => {
  // (λ‹€μš΄λ‘œλ“œ μ™„λ£Œ μ „κΉŒμ§€) λ²„νŠΌ λΉ„ν™œμ„±ν™”
  actionBtn.removeEventListener("click", handleDownload);
  actionBtn.innerText = "Transcoding...";
  actionBtn.disabled = true;

  // FFmpegλ₯Ό λ‘œλ“œν•œλ‹€
  const ffmpeg = createFFmpeg({
    log: true,
    corePath: "https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js",
  });
  await ffmpeg.load();

  // FFmpeg 세계(가상 파일 μ‹œμŠ€ν…œ FS)에 νŒŒμΌμ„ λ§Œλ“ λ‹€ - μ‹€μ‘΄ν•˜μ§€λŠ” μ•Šμ§€λ§Œ λΈŒλΌμš°μ € λ©”λͺ¨λ¦¬μ— μ €μž₯λœλ‹€
  ffmpeg.FS("writeFile", files.input, await fetchFile(videoFile));

  // (1) νŒŒμΌμ„ λ³€ν™˜ν•œλ‹€ (webm β†’ mp4)
  await ffmpeg.run("-i", files.input, "-r", "60", files.output);

  // (2) 썸넀일을 λ§Œλ“ λ‹€
  await ffmpeg.run(
    "-i",
    files.input,
    "-ss",
    "00:00:01",
    "-frames:v",
    "1",
    files.thumb
  );

  // (1)(2) 파일 β†’ blob β†’ url
  const mp4File = await ffmpeg.FS("readFile", files.output);
  const thumbFile = await ffmpeg.FS("readFile", files.thumb);

  const mp4Blob = new Blob([mp4File.buffer], { type: "video/mp4" });
  const thumbBlob = new Blob([thumbFile.buffer], { type: "image/jpg" });

  const mp4Url = URL.createObjectURL(mp4Blob);
  const thumbUrl = URL.createObjectURL(thumbBlob);

  // (1)(2) a.download μ΄μš©ν•΄ λΉ„λ””μ˜€ 파일 & 썸넀일 λ‹€μš΄λ‘œλ“œ
  downloadFile(mp4Url, "myRecording.mp4");
  downloadFile(thumbUrl, "myThumbnail.jpg");

  // 가상 파일 μ‹œμŠ€ν…œ(FS)μ—μ„œ 파일 unlink
  ffmpeg.FS("unlink", files.input);
  ffmpeg.FS("unlink", files.output);
  ffmpeg.FS("unlink", files.thumb);

  // 파일 url μ‚­μ œ
  URL.revokeObjectURL(videoFile);
  URL.revokeObjectURL(mp4Url);
  URL.revokeObjectURL(thumbUrl);

  // λ‹€μš΄λ‘œλ“œ μ™„λ£Œ ν›„ λ²„νŠΌ ν™œμ„±ν™”
  actionBtn.disabled = false;
  actionBtn.innerText = "Record Again";
  init();
  actionBtn.addEventListener("click", handleStart);
};

const handleStop = () => {
  actionBtn.innerText = "Donwload Video";
  actionBtn.removeEventListener("click", handleStop);
  actionBtn.addEventListener("click", handleDownload);
  recorder.stop();
};

const handleStart = () => {
  actionBtn.innerText = "Stop Recording";
  actionBtn.removeEventListener("click", handleStart);
  actionBtn.addEventListener("click", handleStop);
  recorder = new MediaRecorder(stream);
  recorder.ondataavailable = (event) => {
    videoFile = URL.createObjectURL(event.data);
    video.srcObject = null;
    video.src = videoFile;
    video.loop = true;
    video.play();
  };
  recorder.start();
};

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

init();

actionBtn.addEventListener("click", handleStart);

3) 썸넀일 μ—…λ‘œλ“œ 및 보여주기

  • Video 데이터에 thumbUrl ν•„λ“œλ₯Ό λ§Œλ“ λ‹€.
thumbUrl: { type: String, required: true }
  • upload.pug νŒŒμΌμ— input[type="file"]을 μΆ”κ°€ν•œλ‹€.
label(for="thumb") Thumbnail File
input(name="thumb", type="file", accept="image/*", id="thumb", required)
  • videoRouter.js νŒŒμΌμ—μ„œ postUpload 컨트둀러 μ•žμ— μΆ”κ°€ν–ˆλ˜ uploadVideo 미듀웨어λ₯Ό μˆ˜μ •ν•œλ‹€. (single β†’ fields)
videoRouter
  .route("/upload")
  .all(protectorMiddleware)
  .get(getUpload)
  .post(uploadVideo.fields([{ name: "video" }, { name: "thumb" }]), postUpload);
  • videoController.js 파일의 postUpload μ»¨νŠΈλ‘€λŸ¬μ—μ„œ req.file λŒ€μ‹ μ— req.filesλ₯Ό λ³€μˆ˜λ‘œ μ§€μ •ν•œ ν›„ 파일의 경둜λ₯Ό λ°›λŠ”λ‹€.
export const postUpload = async (req, res) => {
  const {
    session: {
      user: { _id },
    },
    body: { title, description, hashtags },
    files: { video, thumb }, // μˆ˜μ • ❗
  } = req;
  try {
    const newVideo = await Video.create({
      fileUrl: video[0].path, // μˆ˜μ • ❗
      thumbUrl: thumb[0].path, // μΆ”κ°€ ❗
      title,
      description,
      hashtags: Video.formatHashtags(hashtags),
      owner: _id,
    });
    const user = await User.findById(_id);
    user.videos.push(newVideo._id);
    user.save();
  } catch (error) {
    return res.status(400).render("upload", {
      pageTitle: "Upload Video",
      errorMessage: error._message,
    });
  }
  return res.redirect("/");
};
  • 썸넀일을 μ‹€μ œλ‘œ homeμ΄λ‚˜ search, profile νŽ˜μ΄μ§€μ—μ„œ λ³Ό 수 μžˆλ„λ‘ video.pug νŒŒμΌμ„ μˆ˜μ •ν•œλ‹€.
mixin video(video)
  div.video-mixin__item
    div.video-mixin__outer
      div.video-mixin__inner
        a(href=`/videos/${video.id}`)
          img(src="/" + video.thumbUrl).video-mixin__thumb
    div.video-mixin__data
      a(href=`/videos/${video.id}`).video-mixin__title=video.title
      div.video-mixin__meta
        a(href=`/users/${video.owner._id}`) #{video.owner.name}
        br
        span 쑰회수 #{video.meta.views} 회 ㆍ #{video.createdAt.getFullYear()}/#{video.createdAt.getMonth()+1}/#{video.createdAt.getDate()}
    hr
.video-mixin__item {
  .video-mixin__outer {
    width: 250px;
    margin: 0 auto;
    .video-mixin__inner {
      padding-top: calc(100% / 16 * 9);
      overflow: hidden;
      position: relative;
      border-radius: 8px;
      .video-mixin__thumb {
        width: 100%;
        height: 100%;
        object-fit: cover;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }
    }
  }
  .video-mixin__data {
    padding: 15px 0 10px 0;
  }
  .video-mixin__title {
    font-size: $font-regular;
  }
  .video-mixin__meta {
    padding-top: 10px;
    font-size: $font-micro;
    line-height: 1.4;
  }
}

2. flexbox / gridμ—μ„œ λ§ˆμ§€λ§‰ 행을 μ •λ ¬ν•˜λŠ” 방법 πŸ”₯

Some ways to align the last row in a flexbox grid μ°Έκ³ 

썸넀일을 κ°€μ Έμ™”λ”λ‹ˆ κΈ°μ‘΄ CSS μŠ€νƒ€μΌμ˜ 문제점이 λ³΄μ—¬μ„œ λ‹€μ‹œ μˆ˜μ •ν–ˆλ‹€.
전체 μ•„μ΄ν…œμ„ μ λ‹Ήν•œ 간격을 μœ μ§€ν•œ μ±„λ‘œ 쀑앙 μ •λ ¬ν•˜λ©΄μ„œλ„, λ§ˆμ§€λ§‰ ν–‰μ˜ μ•„μ΄ν…œμ€ μ™Όμͺ½μ— μ •λ ¬ν•˜κ³  μ‹Άμ—ˆλ‹€.

(1) flexbox: 가상 μš”μ†Œ μΆ”κ°€

Flex-box: Align last row to gridλ₯Ό μ°Έκ³ ν•΄ μ•„λž˜μ™€ 같이 μˆ˜μ •ν–ˆλ‹€.
flex container λ§ˆμ§€λ§‰ ν–‰μ˜ flex item은 μ™Όμͺ½ μ •λ ¬μ‹œν‚€κ³ , 이λ₯Ό μ œμ™Έν•œ λ‚˜λ¨Έμ§€ λΆ€λΆ„μ—λŠ” κ°€μƒμ˜ 빈 곡간λ₯Ό λ§Œλ“€μ–΄ μ±„μ›Œλ„£λŠ”λ‹€.

.video-mixin__container {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  margin: 80px auto;
  &::after {
    content: "";
    flex: auto;
  }
}
  • flex: autoλ₯Ό μ§€μ •ν•˜λ©΄, 'flex item이 flex container의 크기λ₯Ό λ„˜μ§€ μ•ŠκΈ° μœ„ν•΄ μ΅œμ†Œ 크기둜 μ€„μ–΄λ“€κ±°λ‚˜, 남은 곡간을 μ±„μš°κΈ° μœ„ν•΄ λŠ˜μ–΄λ‚œλ‹€.'
  • MDN - flexbox의 κΈ°λ³Έ κ°œλ… μ°Έκ³ 

    flex ν•­λͺ©μ„ flex: initial둜 μ§€μ •ν•˜λ©΄ flex: 0 1 auto 둜 μ§€μ •ν•œ 것과 λ™μΌν•˜κ²Œ λ™μž‘ν•©λ‹ˆλ‹€. 이 경우, flex ν•­λͺ©λ“€μ€ flex-growκ°€ 0μ΄λ―€λ‘œ flex-basis값보닀 컀지지 μ•Šκ³  flex-shrinkκ°€ 1μ΄λ―€λ‘œ flex μ»¨ν…Œμ΄λ„ˆ 곡간이 λͺ¨μžλΌλ©΄ 크기가 μ€„μ–΄λ“­λ‹ˆλ‹€. 또, flex-basisκ°€ autoμ΄λ―€λ‘œ flex ν•­λͺ©μ€ μ£ΌμΆ• λ°©ν–₯으둜 μ§€μ •λœ 크기 λ˜λŠ” 자기 λ‚΄λΆ€ μš”μ†Œ 크기 만큼 곡간을 μ°¨μ§€ν•©λ‹ˆλ‹€.

    flex: auto둜 μ§€μ •ν•˜λ©΄ flex: 1 1 auto둜 μ§€μ •ν•œ 것과 λ™μΌν•˜λ©°, flex:initial κ³ΌλŠ” 'μ£ΌμΆ• λ°©ν–₯ μ—¬μœ  곡간이 μžˆμ„ λ•Œ flex ν•­λͺ©λ“€μ΄ λŠ˜μ–΄λ‚˜μ„œ μ£ΌμΆ• λ°©ν–₯ μ—¬μœ  곡간을 μ±„μš°λŠ”' 점만 λ‹€λ¦…λ‹ˆλ‹€.

(2) div μΆ”κ°€

그런데 μœ„μ™€ 같이 가상 μš”μ†Œλ₯Ό μΆ”κ°€ν•˜λ©΄ λ§ˆμ§€λ§‰ 행을 μ œμ™Έν•œ λ‚˜λ¨Έμ§€ 행듀은 space-between에 μ˜ν•΄ 간격이 μΌμ •ν•΄μ§€μ§€λ§Œ, λ§ˆμ§€λ§‰ ν–‰μ˜ κ°„κ²©λ§Œ λ”°λ‘œ λ†€κ²Œ λœλ‹€.

이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ λ§ˆμ§€λ§‰ ν–‰μ˜ λ‚¨λŠ” 뢀뢄에 divλ₯Ό μΆ”κ°€ν•œ ν›„ 이λ₯Ό 보이지 μ•Šλ„λ‘ ν•¨μœΌλ‘œμ¨ κ·Έλ¦¬λ“œκ°€ μ •μ‚¬κ°ν˜•μ΄ λ˜λ„λ‘ ν•  수 μžˆλ‹€.

κ·ΈλŸ¬λ‚˜, μ΄λŠ” flex item이 총 λͺ‡ κ°œμΈμ§€ μ•Œκ³  μžˆκ±°λ‚˜ 이λ₯Ό κ³„μ‚°ν•΄μ„œ divλ₯Ό λ™μ μœΌλ‘œ μΆ”κ°€ν•  수 μžˆμ„ λ•Œλ§Œ μ“Έ 수 μžˆλŠ” 방법이닀.

(3) grid: auto-fill / space-evenly πŸ”₯

[responsive-web] CSS grid-layout repeat() (auto-fill, auto-fit)을 μ°Έκ³ ν•΄ μ΅œμ’…μ μœΌλ‘œ μ•„λž˜μ™€ 같이 μˆ˜μ •ν–ˆλ‹€.

.video-mixin__container {
  display: grid;
  grid-template-columns: repeat(auto-fill, 250px);
  justify-content: space-evenly;
  grid-gap: 40px;
  margin: 0 40px;
}
  • grid-gap에 μ˜ν•΄ grid item κ°„ 간격이 μœ μ§€λœλ‹€.
  • margin에 μ˜ν•΄ λΈŒλΌμš°μ € 창의 κ°€λ‘œ 길이가 쀄어듀어도 μ–‘ μ˜†μ— μ—¬μœ  곡간이 μ‘΄μž¬ν•œλ‹€.
  • space-evenly에 μ˜ν•΄ grid item κ°„ 간격이 λ„ˆλ¬΄ λ²Œμ–΄μ§€μ§€ μ•ŠμœΌλ©΄μ„œλ„ 쀑앙에 μ •λ ¬λœλ‹€.
  • repeat(auto-fill, 250px)에 μ˜ν•΄ ν™”λ©΄ λ„ˆλΉ„λ§ŒνΌ grid item이 λ‚˜μ—΄λ˜λ‹€κ°€, ν™”λ©΄ λ„ˆλΉ„μ— λΉ„ν•΄ overflow 된 grid item은 μžλ™μœΌλ‘œ wrap λ˜λ©΄μ„œλ„, λ§ˆμ§€λ§‰ ν–‰μ˜ μ•„μ΄ν…œμ€ μ™Όμͺ½ μ •λ ¬λœλ‹€.

✨ 내일 ν•  것

  1. thumbnail 마무리 및 볡슡

  2. flash messages

profile
λŠ₯λ™μ μœΌλ‘œ μ‚΄μž, ν–‰λ³΅ν•˜κ²ŒπŸ˜

0개의 λŒ“κΈ€