video recorder - getUserMedia / MediaRecorder / a.download
webassembly video transcode - FFmpeg.wasm / webm â mp4 / file â blob â url
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"));
upload íėīė§ėė userę° ë đí ëēížė ëëŽ videoëĨž ë đíí ė ėëëĄ íë Īęģ íëĪ.
â ėđīëĐëžė ėĪëėĪė ëí ė ęķ ęķí ę°ė ļėĪęļ°: userę° ë đí ëēížė ëëĨīëĐī userė ėđīëĐëžė ėĪëėĪė ëí ė ę·ž ęķíė ėŧëëĪ.
⥠ëđëėĪ ėĪėę° ëģīęļ° & ëŊļëĶŽëģīęļ°: ë đíë videoëĨž ëĪėīëĄëíęļ° ė ė userę° ëŊļëĶŽ ëģž ė ėëëĄ íëĪ.
âĒ ëđëėĪ ë đí ë° ëĪėīëĄë: videoëĨž ë đíí í ëĪėīëĄë í ė ėëëĄ íëĪ.
block content
div
button#startBtn Start Recording
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);
ėīė ë
đí ëēížė ëëĨīëĐī ėđīëĐëžę° íėąíëėī ëī ėķë í ëŠĻėĩėī íëĐīė ëĻë ęēė íėļí ė ėëĪ.
íėŽë ë
đí ëēížė ëëŽėž ëŊļëĶŽëģīęļ° íëĐīėī ëŽëĪ.
ėīëĨž â 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);
MDN - MediaRecorder ė°ļęģ
âĒ MediaRecorderëĨž ėīėĐíī ëđëėĪ íđė ėĪëėĪëĨž ë đííęģ ëĪėīëĄë í ė ėëĪ.
ðĄ ëđëėĪ ë đí
MediaRecorderę° streamė ë°ėėĻëĪ.
ėĪė ë
đíę° ėėëëëĄ ë
đí ëēížė ëëŽ MediaRecorderëĨž íėąííëĪ.
ë
đíë videoëĨž ëŊļëĶŽ ëģž ė ėëĪ.
ë đí ëēížė ëĪė ëëĨīëĐī MediaRecorderę° ëđíėąíëėī ë đíę° ėĪëĻëëĪ.
ë
đíę° ėĪëĻëëĐī ė ėĨë ë°ėīí°ė ėĩėĒ
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 ëēížė ëë ė ë ë§íŽę° ę°ëĶŽíĪë íėžė ė ėĨí ė ėëĪ.
ę·ļë°ë° íėŽ ëĪėīëĄëí ëđëėĪë ėŽėė ëė§ë§ ęļļėī(duration)ëĨž ę°ė§ ëŠŧíęļ° ëëŽļė ėī ė ė íīęē°íīėž íëĪ.
ëí ëŠĻë ęļ°ęļ°ëĪėī webmė ėīíīíė§ë ëŠŧíęļ° ëëŽļė webm íėžė mp4 íėžëĄ ë°ęŋėž íęģ , ëđëėĪėė ėļëĪėžë ėķėķíīėž íëĪ.
ėīëĨž ėíī ffmpeg.wasmė ėīėĐí ė ėëĪ.
ffmpeg
ë ëđëėĪë ėĪëėĪė ę°ė ëŊļëėī íėžė ëĪėí ėĒ
ëĨė ííëĄ ęļ°ëĄíęģ ëģííīėĢžë ėŧīíĻí° íëĄę·ļëĻė ë§íëĪ.
ėëĨž ëĪėī, FFmpegëĨž ėīėĐíī ëđëėĪëĨž ėėķíęą°ë ëđëėĪėė ėĪëėĪëĨž ėķėķí ė ėëĪ.
ëđëėĪėė ėĪíŽëĶ°ė·ė ė°ęą°ë ëđëėĪëĨž gif íėžëĄ ë§ëĪęą°ë ëđëėĪė ėë§ė ėķę°í ėë ėëĪ.
ëí, ė íëļėēëž ę°ė ëđëėĪëĨž ėŽëŽ ę°ė íŽë§·ęģž íė§ëĄ ėļė―ëĐí ėë ėëĪ.
ę·ļë°ë° FFmpegë ë°ąėëėė ėĪííīėž íęļ° ëëŽļė ėëë ėëēëĨž ėīėĐí ë§íž ëđėĐė ė§ëķíīėž íëĪ.
ėī ëŽļė ëĨž íīęē°íęļ° ėíī WebAssemblyëĨž ėīėĐíëĪ.
ėđ ėīė ëļëĶŽë ėë°ėĪíŽëĶ―íļė ëŽīëĪėžęđ? & ėīëģī ę°ë°ėëĨž ėí ėđ ė ęļ°ė WebAssembly ėĪëŠ ė°ļęģ
WebAssembly
ë íëĄ íļėëėė ë§Īė° ëđ ëĨīęē ė―ëëĨž ėĪíí ė ėëëĄ íë ę°ë°Đí íėĪėīëĪ.
ëļëžė°ė ėė ėĪí ę°ëĨí ėļėīë ęļ°ëģļė ėžëĄ HTML, CSS, JavaScriptėīė§ë§, ėë°ėĪíŽëĶ―íļę° ėë ëĪëĨļ ėĒ
ëĨė ėļėī ëí WebAssemblyëĄ ėŧīíėžíĻėžëĄėĻ ėë°ėĪíŽëĶ―íļę° ėë ėļėīë ëļëžė°ė ėė ėĪíí ė ėëĪ.
ėëĨž ëĪėī, ęēė ëąė ėë°ėĪíŽëĶ―íļëĄ ë§ëĪęļ°ė ėë°ėĪíŽëĶ―íļ ėëę° ëëŽī ëëĶŽęą°ë ėë°ėĪíŽëĶ―íļëĄ ęĩŽíí ė ėë ëķëķëĪėī ėėī ëĪëĨļ ėļėīëĨž ėŽėĐíī ë§ë í ėĪė ëĄ ėĪííęļ° ėíīėë ęēė ėĪėđ ęģžė ė ęą°ėģėž íëĪ.
ę·ļëŽë WebAssemblyëĄ ėŧīíėžíëĐī ėĪėđíė§ ėęģ ë ëļëžė°ė ėė ęēėė ėĪíí ė ėëĪ.
ėīëĨž íĩíī ėĪí ëđėĐėī í° íëĄę·ļëĻëĪë ëđėĐė ė ė―íęģ ëļëžė°ė ėė ėĪíí ė ėëĪ.
ííļ, ė°ëĶŽë WebAssemblyëĄ ėŧīíėžë ėļėīëĨž ėīėĐíī íëĄę·ļëĻė ë§ëĪ ëŋ, ëģīíĩ WebAssemblyëĨž ė§ė ėėąíė§ë ėëëĪ.
ėė ë§íëŊ ëđëėĪëĨž ëĪėí ííëĄ ëģííë ë° FFmpegëĨž ėŽėĐíëĪ.
ėīë ė íëļė ęē―ė°ėë ėŽėĐėę° ė
ëĄëí ëđëėĪëĨž ę·ļëĪė ëđėž ėëēėė ëģííė§ë§
Fmpeg.wasmëĨž ėīėĐíëĐī ėŽėĐėę° ė
ëĄëí ëđëėĪëĨž ėŽėĐėė ëļëžė°ė ėė ëģííëëĄ í ė ėëĪ.
FFmpegë ėë ėë°ėĪíŽëĶ―íļę° ėë C ėļėīëĄ ë§ëĪėīė§ íëĄę·ļëĻėīė§ë§, WebAssemblyëĨž ėīėĐíëĐī ëļëžė°ė ėė FFmpegëĨž ėĪíėíŽ ė ėęļ° ëëŽļėīëĪ.
ėŽęļ°ėë handleDownload íĻė ėėė userę° videoëĨž ëĪėīëĄë íęļ° ė ė mp4 íėžëĄ ë°ęŋëģīë Īęģ íëĪ.
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();
// ėĪëĩ
};
ëĻžė , FFmpeg ę°ėė ėļęģė íėžė ë§ëĪėīėž íëĪ.
ėīë ęē ë§ëĪėīė§ íėžė ėĪėĄīíė§ ėė§ë§ ëļëžė°ė ė ëĐëŠĻëĶŽė ė ėĨëëĪ.
ėīëĨž ėíīėë 0ęģž 1ė ė ëģī(binaryData)ëĨž ė íīėĪėž íëĪ.
ë
đíë videoė ëí ė ëģīëĨž ëīęģ ėë url, ėĶ videoFileėī ë°ëĄ ę·ļęēėīëĪ.
ë°ëžė, fetchFile()ė ėīėĐíī videoFileė ė íīėĢžëëĄ íëĪ.
ffmpeg.FS("writeFile", "íėž ėīëĶ.íėĨė", await fetchFile(binaryData));
ëĪėėžëĄ, FFmpeg ëŠ
ë đėīëĨž ėŽėĐíī ėėė ë§ë ę°ėė íėžė ëģííë Īęģ íëĪ.
FFmpegëĨž ėŽėĐėė ëļëžė°ė ėė ëĄëĐíęļ° ëëŽļė FFmpeg ëŠ
ë đėī ëí ėŽėĐėė ëļëžė°ė ėė ėĪíëëëĄ í ė ėëĪ.
ffmpeg.run()
ė ėė ë§ë ę°ėė íėž(recording.webm)ė ę°ėė ėŧīíĻí°ėė inputėžëĄ ë°ė í ėīëĨž ė§ė í ęē°ęģžëŽž(output.mp4)ëĄ ëģííëëĄ íëĪ.
ėīë videoëĨž ėīëđ 60 íë ėėžëĄ ėļė―ëĐíęļ° ėíī ėëė ę°ėī ė―ëëĨž ėėąíėëĪ.
await ffmpeg.run("-i", "recording.webm", "-r", "60", "output.mp4");
ėīė ę°ė íėž ėėĪí (ëļëžė°ė ė ëĐëŠĻëĶŽ)ėë output.mp4 íėžėī ėëĪ.
ėĪė ëĄ íėžė ë§ëĪęļ° ėíī ę°ėė ėļęģė ėĄīėŽíë 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);
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ėļ ęēė íėļí ė ėëĪ.
ėĩė ëēė ė 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", // ėķę° â
});
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