μΈλ€μΌ μΆμΆ λ° λ€μ΄λ‘λ / μΈλ€μΌ μ λ‘λ λ° λ³΄μ¬μ£ΌκΈ°
flexbox / gridμμ λ§μ§λ§ νμ μ λ ¬νλ λ°©λ²
// 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μ΄ λ§λ€μ΄μ§λ€.
μμ 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);
};
upload.pug νμΌμμ λ²νΌμ id μ νμλ₯Ό μμ νλ€.
recorder.js νμΌμμ λ³μ μ΄λ¦μ μμ νλ€.
λ¬Έμμ΄μΈ νμΌ μ΄λ¦λ€μ λ³μλ‘ μ§μ ν΄ λͺ¨λ λ°κΏμ€¬λ€.
// recorder.js
const files = {
input: "recording.webm",
output: "output.mp4",
thumb: "thumbnail.jpg",
};
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");
// μ€λ΅
};
λ€μ΄λ‘λκ° μλ£λκΈ° μ μ λ€μ΄λ‘λ λ²νΌμ΄ μ°μμΌλ‘ νμ±νλλ κ²μ λ§λλ€.
λ€μ΄λ‘λκ° μλ£λλ©΄ λ€μ νμ±νλλλ‘ νλ€.
// 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);
};
// 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);
thumbUrl: { type: String, required: true }
label(for="thumb") Thumbnail File
input(name="thumb", type="file", accept="image/*", id="thumb", required)
videoRouter
.route("/upload")
.all(protectorMiddleware)
.get(getUpload)
.post(uploadVideo.fields([{ name: "video" }, { name: "thumb" }]), postUpload);
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("/");
};
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;
}
}
Some ways to align the last row in a flexbox grid μ°Έκ³
μΈλ€μΌμ κ°μ Έμλλ κΈ°μ‘΄ CSS μ€νμΌμ λ¬Έμ μ μ΄ λ³΄μ¬μ λ€μ μμ νλ€.
μ 체 μμ΄ν
μ μ λΉν κ°κ²©μ μ μ§ν μ±λ‘ μ€μ μ λ ¬νλ©΄μλ, λ§μ§λ§ νμ μμ΄ν
μ μΌμͺ½μ μ λ ¬
νκ³ μΆμλ€.
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 νλͺ©λ€μ΄ λμ΄λμ μ£ΌμΆ λ°©ν₯ μ¬μ 곡κ°μ μ±μ°λ' μ λ§ λ€λ¦ λλ€.
κ·Έλ°λ° μμ κ°μ΄ κ°μ μμλ₯Ό μΆκ°νλ©΄ λ§μ§λ§ νμ μ μΈν λλ¨Έμ§ νλ€μ space-betweenμ μν΄ κ°κ²©μ΄ μΌμ ν΄μ§μ§λ§, λ§μ§λ§ νμ κ°κ²©λ§ λ°λ‘ λκ² λλ€.
μ΄λ₯Ό ν΄κ²°νκΈ° μν΄ λ§μ§λ§ νμ λ¨λ λΆλΆμ divλ₯Ό μΆκ°ν ν μ΄λ₯Ό 보μ΄μ§ μλλ‘ ν¨μΌλ‘μ¨ κ·Έλ¦¬λκ° μ μ¬κ°νμ΄ λλλ‘ ν μ μλ€.
κ·Έλ¬λ, μ΄λ flex itemμ΄ μ΄ λͺ κ°μΈμ§ μκ³ μκ±°λ
μ΄λ₯Ό κ³μ°ν΄μ divλ₯Ό λμ μΌλ‘ μΆκ°ν μ μμ λλ§ μΈ μ μλ λ°©λ²μ΄λ€.
[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 λλ©΄μλ, λ§μ§λ§ νμ μμ΄ν μ μΌμͺ½ μ λ ¬λλ€.
thumbnail λ§λ¬΄λ¦¬ λ° λ³΅μ΅
flash messages