๐Ÿ“–Day 11 - Custom HTML5 Video Player

JavaScript 30์˜ Day 11 ํ”„๋กœ์ ํŠธ๋Š” HTML, CSS, JavaScript๋งŒ์„ ์ด์šฉํ•ด์„œ ๋‚˜๋งŒ์˜ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋Š” ํ”„๋กœ์ ํŠธ์ด๋‹ค.

๐Ÿค“๐Ÿ“„์ฝ”๋“œ ๋ชจ์•„๋ณด๊ธฐ

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>HTML Video Player</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="player">
      <video class="player__video viewer" src="652333414.mp4"></video>

      <div class="player__controls">
        <div class="progress">
          <div class="progress__filled"></div>
        </div>
        <button class="player__button toggle" title="Toggle Play">โ–บ</button>
        <button class="player__button volume" title="Toggle Volume">๐Ÿ”Š</button>
        <input
          type="range"
          name="volume"
          class="player__slider"
          min="0"
          max="1"
          step="0.05"
          value="1"
        />
        <input
          type="range"
          name="playbackRate"
          class="player__slider"
          min="0.5"
          max="2"
          step="0.1"
          value="1"
        />
        <button data-skip="-10" class="player__button">ยซ 10s</button>
        <button data-skip="10" class="player__button">10s ยป</button>
        <button class="player__button full-screen">๐Ÿ”ณ</button>
      </div>
    </div>

    <script src="main.js"></script>
  </body>
</html>

CSS

html {
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  padding: 0;
  display: flex;
  background: #7a419b;
  min-height: 100vh;
  background: linear-gradient(135deg, #7c1599 0%, #921099 48%, #7e4ae8 100%);
  background-size: cover;
  align-items: center;
  justify-content: center;
}

.player {
  max-width: 750px;
  border: 5px solid rgba(0, 0, 0, 0.2);
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
  position: relative;
  font-size: 0;
  overflow: hidden;
}

/* This css is only applied when fullscreen is active. */
.player:fullscreen {
  max-width: none;
  width: 100%;
}

.player:-webkit-full-screen {
  max-width: none;
  width: 100%;
}

.player__video {
  width: 100%;
}

.player__button {
  background: none;
  border: 0;
  line-height: 1;
  color: white;
  text-align: center;
  outline: 0;
  padding: 0;
  cursor: pointer;
  max-width: 50px;
}

.player__button:focus {
  border-color: #ffc600;
}

.player__slider {
  width: 10px;
  height: 30px;
}

.player__controls {
  display: flex;
  position: absolute;
  bottom: 0;
  width: 100%;
  transform: translateY(100%) translateY(-5px);
  transition: all 0.3s;
  flex-wrap: wrap;
  background: rgba(0, 0, 0, 0.1);
}

.player:hover .player__controls {
  transform: translateY(0);
}

.player:hover .progress {
  height: 15px;
}

.player__controls > * {
  flex: 1;
}

.progress {
  flex: 10;
  position: relative;
  display: flex;
  flex-basis: 100%;
  height: 5px;
  transition: height 0.3s;
  background: rgba(0, 0, 0, 0.5);
  cursor: ew-resize;
}

.progress__filled {
  width: 50%;
  background: #ffc600;
  flex: 0;
  flex-basis: 50%;
}

/* unholy css to style input type="range" */

input[type="range"] {
  -webkit-appearance: none;
  background: transparent;
  width: 100%;
  margin: 0 5px;
}

input[type="range"]:focus {
  outline: none;
}

input[type="range"]::-webkit-slider-runnable-track {
  width: 100%;
  height: 8.4px;
  cursor: pointer;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
  background: rgba(255, 255, 255, 0.8);
  border-radius: 1.3px;
  border: 0.2px solid rgba(1, 1, 1, 0);
}

input[type="range"]::-webkit-slider-thumb {
  height: 15px;
  width: 15px;
  border-radius: 50px;
  background: #ffc600;
  cursor: pointer;
  -webkit-appearance: none;
  margin-top: -3.5px;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
}

input[type="range"]:focus::-webkit-slider-runnable-track {
  background: #bada55;
}

input[type="range"]::-moz-range-track {
  width: 100%;
  height: 8.4px;
  cursor: pointer;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
  background: #ffffff;
  border-radius: 1.3px;
  border: 0.2px solid rgba(1, 1, 1, 0);
}

input[type="range"]::-moz-range-thumb {
  box-shadow: 0 0 0 rgba(0, 0, 0, 0), 0 0 0 rgba(13, 13, 13, 0);
  height: 15px;
  width: 15px;
  border-radius: 50px;
  background: #ffc600;
  cursor: pointer;
}

JavaScript

/* Get our Elements */

const player = document.querySelector(".player");
const video = player.querySelector(".viewer");
const progress = player.querySelector(".progress");
const progressBar = player.querySelector(".progress__filled");
const toggle = player.querySelector(".toggle");
const volume = player.querySelector(".volume");
const skipButtons = player.querySelectorAll("[data-skip]");
const ranges = player.querySelectorAll(".player__slider");
const screen = player.querySelector(".full-screen");

/* Build out functions */

function togglePlay() {
  const method = video.paused ? "play" : "pause";
  video[method]();
}

function toggleVolume() {
  const mute = video.muted ? "๐Ÿ”Š" : "๐Ÿ”‡";
  video.muted = !video.muted;
  volume.textContent = mute;
}

function updateToggle() {
  const icon = this.paused ? "โ–ถ๏ธ" : "โšโš";
  toggle.textContent = icon;
}

function skip(e) {
  video.currentTime += parseInt(e.dataset.skip);
}

function handleKeyboard(e) {
  if (e.key === "ArrowRight") {
    skip(skipButtons[1]);
  } else if (e.key === "ArrowLeft") {
    skip(skipButtons[0]);
  } else if (e.key === " ") {
    togglePlay();
  } else if (e.key === "m") {
    toggleVolume();
  }
}

function handleRangeUpdate() {
  video[this.name] = this.value;
}

function handleProgress() {
  const percent = (video.currentTime / video.duration) * 100;
  progressBar.style.flexBasis = `${percent}%`;
}

function scrub(e) {
  const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
  video.currentTime = scrubTime;
}

function handleScreen() {
  video.requestFullscreen();
}

/* Hook up the event listners */

video.addEventListener("click", togglePlay);
video.addEventListener("play", updateToggle);
video.addEventListener("pause", updateToggle);
video.addEventListener("timeupdate", handleProgress);

toggle.addEventListener("click", togglePlay);
volume.addEventListener("click", toggleVolume);

window.addEventListener("keydown", handleKeyboard);
skipButtons.forEach((button) => button.addEventListener("click", skip));

ranges.forEach((range) => range.addEventListener("change", handleRangeUpdate));
ranges.forEach((range) =>
  range.addEventListener("mousemove", handleRangeUpdate)
);

let mousedown = false;
progress.addEventListener("click", scrub);
progress.addEventListener("mousedown", (e) => mousedown && scrub(e));
progress.addEventListener("mousedown", () => (mousedown = true));
progress.addEventListener("mouseup", () => (mousedown = false));

screen.addEventListener("click", handleScreen);

๐Ÿ”Ž์ฝ”๋“œ ์„ค๋ช…

  1. ๊ฐ€์žฅ ๋จผ์ €, querySelector๋ฅผ ์ด์šฉํ•ด์„œ ํ•„์š”ํ•œ element๋“ค์„ ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ์„ธํŒ…ํ•ด์ค€๋‹ค. ์ด์ œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ํ•˜๋‚˜์”ฉ ๊ตฌํ˜„ํ•  ์ฐจ๋ก€๋‹ค. ์ฒซ๋ฒˆ์งธ๋กœ, ์žฌ์ƒ ๋ฐ ์ผ์‹œ์ •์ง€ ๊ธฐ๋Šฅ์ด๋‹ค. togglePlay๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค๊ณ  <video>์˜ paused ์†์„ฑ์„ ์ด์šฉํ•ด ํ˜„์žฌ ์ผ์‹œ์ •์ง€ ๋˜์–ด์žˆ๋‹ค๋ฉด 'play' ์žฌ์ƒ์ค‘์ด๋ผ๋ฉด 'pause'๋ฅผ method๋ผ๋Š” ๋ณ€์ˆ˜์— ๋„ฃ์–ด์ค€๋‹ค. ์ดํ›„์— play() ํ˜น์€ pause() ๋ฉ”์„œ๋“œ๋กœ ์˜์ƒ์„ ์žฌ์ƒ / ์ผ์‹œ์ •์ง€ ์‹œ์ผœ์ค€๋‹ค. ์‹œ๊ฐ์  ํšจ๊ณผ๋ฅผ ์œ„ํ•ด ์šฐ๋ฆฌ๊ฐ€ ์•„๋Š” ์žฌ์ƒ ๋ฐ ์ผ์‹œ์ •์ง€๋ฒ„ํŠผ์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๋Š” updateToggle ์ด๋ผ๋Š” ํ•จ์ˆ˜๋„ ๊ฐ™์ด ๋งŒ๋“ค์–ด์ฃผ๋Š”๋ฐ video์˜ play, pause ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ ๋ฒ„ํŠผ์„ ์—…๋ฐ์ดํŠธ ํ•ด์ค€๋‹ค.

  2. ๋‘๋ฒˆ์งธ๋กœ, ์˜์ƒ์˜ ์Šคํ‚ต ๊ธฐ๋Šฅ์ด๋‹ค. ์˜์ƒ์„ ๋„˜๊ธฐ๊ธฐ ์œ„ํ•ด์„œ currentTime์ด๋ผ๋Š” ์†์„ฑ์„ ์ด์šฉํ•˜๋Š”๋ฐ skipButtons์—์„œ forEach๋ฌธ์„ ์ด์šฉํ•ด click์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด skip์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค. skip์ด๋ผ๋Š” ํ•จ์ˆ˜๋Š” video.currentTime์— ํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ๋ฒŒ์–ด์ง„ ๋ฒ„ํŠผ์˜ ๋ฐ์ดํ„ฐ ์†์„ฑ์„ ์ด์šฉํ•œ๋‹ค. ์˜ˆ์ „์— ์•Œ์•„๋ดค๋“ฏ์ด JavaScript์—์„œ ์ ‘๊ทผํ• ๋•Œ๋Š” data-* ํ˜•ํƒœ์—์„œ ๋’ท๋ถ€๋ถ„์ธ * ๋งŒ์„ ์ด์šฉํ•ด ์ ‘๊ทผํ•œ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ์ด๋ฒคํŠธ ๊ฐ์ฒด e์˜ dataset์†์„ฑ์„ ํ†ตํ•ด ๋ฌธ์ž์—ด์„ ๊ฐ€์ ธ์˜ค๊ณ  parseInt๋กœ type์„ ๋ฐ”๊ฟ”์ค€๋‹ค.

  3. ์„ธ๋ฒˆ์งธ๋กœ, ๋ณผ๋ฅจ๊ณผ ๋ฐฐ์†์„ ์กฐ์ ˆํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ranges๋ผ๋Š” ๋ณ€์ˆ˜์— player__slider๋ผ๋Š” ํด๋ž˜์Šค์˜ ์š”์†Œ๋“ค์„ ๋ฐ›์•„์คฌ๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๊ฑธ ์ด์šฉํ•œ๋‹ค. change์™€ mousemove์ด๋ฒคํŠธ์— handleRangeUpdate๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š”๋ฐ ์ด ํ•จ์ˆ˜๋Š” html์˜ name element๋ฅผ ์ด์šฉํ•ด value๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค.

  4. ๋งˆ์ง€๋ง‰์œผ๋กœ, ๊ฐ€์žฅ ๊นŒ๋‹ค๋กœ์šด ์žฌ์ƒ ์ง„ํ–‰ ๋ฐ” ๊ธฐ๋Šฅ์ด๋‹ค. ํ˜„์žฌ ์žฌ์ƒ ์ง„ํ–‰๋ฅ ์— ๋”ฐ๋ผ ์žฌ์ƒ ๋ฐ”๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” handleProgress ํ•จ์ˆ˜์™€ ํŠน์ • ์ง€์ ์— ๋งˆ์šฐ์Šค๋ฅผ ๋†“์•˜์„ ๋•Œ ์˜์ƒ์˜ ํŠน์ • ์‹œ๊ฐ„์œผ๋กœ ์ด๋™์‹œํ‚ค๋Š” scrubํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ๋จผ์ €, handleProgress ํ•จ์ˆ˜๋Š” <video>์˜ currentTime๊ณผ duration์ด๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•œ๋‹ค. ๋น„๋””์˜ค์˜ ํ˜„์žฌ ์žฌ์ƒ ์‹œ๊ฐ„ / ์ „์ฒด ์˜์ƒ ์‹œ๊ฐ„์˜ ํผ์„ผํŠธ๋ฅผ percent๋ผ๋Š” ๋ณ€์ˆ˜์— ๋„ฃ์–ด์ฃผ๊ณ  felx-basis๋ฅผ percent๋งŒํผ ๋ฐ”๊ฟ”์ฃผ๋ฉด ์›ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์‹คํ–‰๋œ๋‹ค. scrubํ•จ์ˆ˜๋Š” progress์˜ ์ „์ฒด ๊ธธ์ด(progress.offsetWidth)์—์„œ ์ด๋ฒคํŠธ ๊ฐ์ฒด e์˜ x์ขŒํ‘œ(e.offsetX)๊ฐ€ ์–ด๋Š ์‹œ๊ฐ„์„ ๋‚˜ํƒ€๋‚ด๋Š”์ง€ ๊ตฌํ•˜๋ฉด ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด, 1๋ถ„ ์งœ๋ฆฌ ์˜์ƒ์—์„œ ์ • ๊ฐ€์šด๋ฐ(50%)๋ฅผ ํด๋ฆญํ•˜๋ฉด ์˜์ƒ์˜ ํ˜„์žฌ ์žฌ์ƒ์‹œ๊ฐ„์ด 30์ดˆ๋กœ ๋ฐ”๋€Œ๋ฉด ๋œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ ํŠน์ • ์‹œ์ ์˜ x์ขŒํ‘œ์—์„œ ์ „์ฒด ๊ธธ์ด๋ฅผ ๋‚˜๋ˆˆ ๊ฐ’์— ์ „์ฒด ์žฌ์ƒ์‹œ๊ฐ„์„ ๊ณฑํ•ด์ฃผ๋ฉด ์›ํ•˜๋Š” ์‹œ๊ฐ„์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ์–ป์€ ์‹œ๊ฐ„์„ video.currentTime์— ๋„ฃ์–ด์ฃผ๋ฉด ๋์ด๋‹ค.

  5. ์ถ”๊ฐ€๋กœ, ์˜์ƒ์„ ์ „์ฒด ํ™”๋ฉด์œผ๋กœ ์žฌ์ƒํ•˜๋Š” ๊ธฐ๋Šฅ๊ณผ ํ‚ค๋ณด๋“œ์˜ ์ŠคํŽ˜์ด์Šค ๋ฐ”๋ฅผ ํ†ตํ•œ ์žฌ์ƒ ๋ฐ ์ผ์‹œ์ •์ง€๊ธฐ๋Šฅ, ๋ฐฉํ–ฅํ‚ค๋กœ ์Šคํ‚ตํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ, m์„ ๋ˆ„๋ฅด๋ฉด ์Œ์†Œ๊ฑฐ๊ฐ€ ๊บผ์กŒ๋‹ค ์ผœ์ง€๋Š” ๊ธฐ๋Šฅ๋“ฑ์„ ์ถ”๊ฐ€๋กœ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค. ์ „์ฒดํ™”๋ฉด์œผ๋กœ ์ „ํ™˜ํ•ด์ฃผ๋Š” ๊ฒƒ์€ ๊ตฌ๊ธ€๋ง์„ ํ•ด๋ณด๋‹ˆ ๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ๋ฉ”์„œ๋“œ๋ช…์ด ๋‹ฌ๋ž๋Š”๋ฐ ๋‚˜๋Š” ํฌ๋กฌ์„ ์ด์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํฌ๋กฌ์—์„œ๋งŒ ๊ตฌํ˜„๋  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค. ํ‚ค๋ณด๋“œ๋ฅผ ์ด์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์€ handleKeyboard๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ํ•ด๋‹น ํ‚ค๊ฐ€ ์ž…๋ ฅ๋  ๊ฒฝ์šฐ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋ณด์•˜๋‹ค.

๐Ÿš€TIL(Today I Learned)

  • ๋‚˜์ค‘์— ๋ณด๋ฉด ์ •๋ง ์•ˆ์ข‹์€ ์ฝ”๋“œ์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ง€๊ธˆ๊นŒ์ง€ ๋‚ด๊ฐ€ ์•„๋Š” ๋ฐฉ๋ฒ• + ๊ตฌ๊ธ€๋ง์„ ํ†ตํ•ด ์ถ”๊ฐ€์ ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ customizing ํ•ด๋ดค๋Š”๋ฐ, ์•ž์œผ๋กœ ์žˆ์„ ํ”„๋กœ์ ํŠธ๋„ ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ํ†ตํ•ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์™€ ๋” ๊ฐ€๊นŒ์›Œ์ ธ์•ผ๊ฒ ๋‹ค.
  • dataset, offsetX, offsetWidth, video(HTML)

JavaScript 30 ํ”„๋กœ์ ํŠธ ๊ฒฐ๊ณผ๋ฌผ

'Language > JavaScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[JS] ๐Ÿ—“๏ธJavaScript 30 - Day 12  (0) 2021.11.29
[JS] ๐Ÿ—“๏ธJavaScript 30 - Day 10  (0) 2021.11.26
[JS] ๐Ÿ—“๏ธJavaScript 30 - Day 9  (0) 2021.11.23
[JS] ๐Ÿ—“๏ธJavaScript 30 - Day 8  (0) 2021.11.22
[JS] ๐Ÿ—“๏ธJavaScript 30 - Day 7  (0) 2021.11.21