[Javascript] 11. Custom Video Player
1. 배경
간단한 비디오 플레이어를 만들었다. 화면이나 플레이 버튼을 누르면 재생되거나 일시정지되고, 이에 따라 버튼의 모양이 바뀐다. 풀 스크린 버튼, 10초 전, 25초 뒤로 이동하는 스킵 버튼, 재생 상황에 따라 이를 나타내는 바를 만들었으며, 재생 바를 클릭하면 이에 비례하여 재생 상황이 이동되도록 하였다.
2. 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>
<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="25" class="player__button">25s »</button>
<button class="player__button fullscreen">🔲</button>
</div>
</div>
<script src="scripts.js"></script>
</body>
</html>
3. 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 .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: 0%;
}
/* 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;
}
4. Javascript
간단한 기능이 여러 가지 사용되었으므로 각각 풀어서 작성하면 다음과 같다.
const player = document.querySelector('.player');
const video = player.querySelector('.viewer');
function togglePlay() {
const method = video.paused ? 'play' : 'pause';
video[method]();
}
video.addEventListener('click', togglePlay);
가장 먼저 사용자의 클릭에 따라 플레이 상태를 재생과 정지로 바꿔 주는 기능이다. 조상 컨테이너를 player로, 비디오 태그인 viewer를 video로 지정하고, togglePlay 함수를 정의한다. togglePlay 함수는 video가 paused 상태인지 확인하여 true인 경우 play, false인 경우 pause 문자열을 반환하고 이를 method 변수에 할당한다. video[method]();는 반환되는 method 값에 따라서 play나 pause를 실행하게 된다.
const toggle = player.querySelector('.toggle');
function updateButton() {
const icon = this.paused ? '►' : '❚ ❚';
console.log(icon);
toggle.textContent = icon;
}
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
toggle.addEventListener('click', togglePlay);
다음으로는 toggle 변수에 재생/일시정지 버튼을 할당하고, updateButton 함수를 정의했다. 재생/일시정지 버튼의 모양을, 정지 상태일 때는 '►', 플레이 상태일 때는 '❚ ❚' 로 바꿔 주는 함수이다. video에 play나 pause 이벤트가 전달되면, 이 함수를 실행하는데, 이 함수는 비디오가 paused 상태일 때는 ►, 플레이 상태일 때는 ❚ ❚를 icon 변수에 할당한다. 그리고 textContent를 통해 토글 버튼에 이 icon 변수를 입력한다. textContent는 노드의 텍스트 콘텐츠를 반환한다. 즉, toggle의 텍스트 콘텐츠를 ►, ❚ ❚로 바꿔 주는 것이다.
const skipButtons = player.querySelectorAll('[data-skip]');
function skip() {
console.log(this.dataset.skip);
video.currentTime += parseFloat(this.dataset.skip);
}
skipButtons.forEach(button => button.addEventListener('click', skip));
다음은 동영상의 스킵 기능이다. dataset의 skip 속성을 가진 버튼을 skipButtons에 할당하고, 이에 대해 이벤트 리스너를 걸어서, 클릭 이벤트가 전달되면 스킵 함수를 실행한다. 스킵 함수는, 비디오의 현재 재생 시간인 currentTime에, 전달되는 이벤트의 dataset의 skip 속성의 값을 더한다. dataset의 skip 속성은 각각 -10과 25가 주어져 있으므로, -10초, 25초씩 건너뛰게 된다. parseFloat는 문자열을 실수로 반환한다.
const ranges = player.querySelectorAll('.player__slider');
function handleRangeUpdate() {
console.log(this.value);
console.log(this.name);
video[this.name] = this.value;
}
ranges.forEach(range => range.addEventListener('change', handleRangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate));
다음은 음량과 재생 속도를 컨트롤하는 바를 만든다. player__slider 클래스를 가진 range 인풋을 모두 선택하여 ranges 변수에 할당하고, 모든 ranges에 대해 change나 mousemove 이벤트가 전달되면 handleRangeUpdate 함수를 실행하도록 하였다. 두 개의 range에는 html에서 각각 volume, playbackRate라는 name을 주었다. 전달되는 이벤트, 즉 change와 mousemove에 대해 handleRangeUpdate가 시행되는데, mousemove는 요소 위에서 마우스가 움직일 때마다 값을 전달하는데, 값이 변하지 않는 상태에서는 동일한 value를 전달한다. change는 요소의 값이 변할 때마다 값을 전달한다. 전달되는 이벤트의 name은 사용자가 클릭한 range가 어느 쪽인지에 따라 html에서 설정해둔 volume, playbackRate과 그 value를 전달하여 음량이나 재생 속도를 변화시키게 된다.
const progressBar = player.querySelector('.progress__filled');
function handleProgress() {
const percent = (video.currentTime / video.duration) * 100;
progressBar.style.flexBasis = `${percent}%`;
}
video.addEventListener('timeupdate', handleProgress);
다음은 재생 상황을 표시해 주는 바 기능이다. 재생이 진행됨에 따라 노란색 바를 채워 준다. 해당 div를 선택하여 progressBar에 할당하고, video의 timeupdate 이벤트가 일어날 때마다 handleProgress 함수를 시행한다. timeupdate 이벤트는 currentTime 속성이 업데이트 될 때마다 발생한다. percent 변수를 정의하여, video의 currentTime 속성을 duration 속성으로 나누고 100을 곱하여 %로 나타낸 값을 할당한다. duration은 HTML의 미디어 요소의 길이를 초 단위로 나타내 주는 속성이다. 이렇게 정의한 변수 percent의 값을 앞서 선택한 div인 progressBar의 style의 flexBasis 속성에 입력해 줌으로써 재생 상황을 표시하게 된다. flexBasis는 플렉스 아이템의 초기 길이를 지정한다. 앞서 css에서 지정해 둔 flex-basis 값(0%)에 percent변수를 입력함으로써, currentTime이 변할 때마다 %가 변하게 되어 재생 상황을 표시하게 되는 것이다.
const progress = player.querySelector('.progress');
function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
console.log(e);
}
let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);
재생 상황 바를 클릭하면 해당 부분이 재생되도록 하는 기능이다. 재생 바 전체를 progress로 선택하고, 클릭 이벤트가 전달되면 scrub 함수가 실행되도록 하였다. scrub 함수는 이벤트가 전달되면 video의 currentTime 값을 바꿔 준다. 이 값은 scrubTime으로 정의하여, 전달된 이벤트의 offsetX값을 progress의 offsetWidth값(전체 너비 값)으로 나누고, video의 재생 시간을 곱해 줌으로써 시간의 형태로 나타낸다. 즉, 동영상 재생 상황 바의 전체 너비 분의 클릭된 곳의 너비만큼 동영상 재생 시간을 곱해 주는 것이다. 위에서 음량과 재생 속도를 컨트롤하는 바와 같이, mousemove 이벤트에 반응하도록 하여 마우스를 끄는 이벤트에도 대응하도록 하였다. 마우스 무브 이벤트가 전달되면, mousedown && scrub를 콜백한다. mousedown은 기본 false로 설정되어 있으니 scrub는 실행되지 않는다. mousedown 이벤트가 전달되면 mousedown이 true가 되므로, mousemove에 대한 scrub는 시행될 것이고, mouseup 이벤트가 전달되면 mousedown이 다시 false가 되므로 scrub가 중단된다.
const fullscreen = player.querySelector('.fullscreen');
function handleScreen() {
video.requestFullscreen();
}
fullscreen.addEventListener('click', handleScreen);
마지막으로 전체 화면 기능을 추가하였다. fullscreen 버튼을 fullscreen 변수에 할당하고, 클릭될 때마다 handleScreen 함수를 시행시킨다. 그리고 video의 requestFullscreen 함수를 시행시키면 미리 css에서 지정해둔 fullscreen 설정에 따라 전체 화면이 시행된다.