[HTML/CSS] Video Controls 커스텀하기 - 1
태그에서 제공하는 기본 컨트롤 디자인 대신, 다른 디자인을 적용하고 싶었습니다.하지만 HTML5 에서는 이러한 커스텀 디자인을 기본적으로 지원하지 않기 때문에, 기본 컨트롤을 숨기고 HTML과 CS
hy-un.tistory.com
이전 글에 이어, 이번에는 HTML과 CSS로 커스텀한 비디오 컨트롤에 JavaScript를 사용하여 기능을 입히려고 합니다.
완성 페이지는 아래 링크에서 확인할 수 있습니다.
https://ganghyun95.github.io/video-controls-custom/
Video Player Custom
재생속도 보통
ganghyun95.github.io
구현 계획
JavaScript 모듈화:
- JavaScript 파일은 재사용성을 높이기 위해 클래스로 모듈화합니다.
모바일 볼륨 조절:
- 모바일에서는 비디오의 볼륨 조절 슬라이더(range)를 제거합니다. 이는 사용자가 range 대신 볼륨 버튼을 사용하기도 하고, iOS에서는 비디오 태그의 볼륨 조절이 제한적이기 때문입니다. (iOS에서는 음소거만 가능합니다.)
Firefox PIP 버튼 제거:
- Firefox 브라우저에서는 PIP 버튼을 제거합니다. Firefox에서는 비디오 태그 자체적으로 PIP 기능을 지원하기 때문입니다.
iOS 풀스크린 컨트롤:
- iOS에서는 requestFullscreen이 제대로 지원되지 않으므로, 대체로 webkitEnterFullscreen을 사용합니다. 이 메서드는 비디오 태그에만 적용할 수 있어서, 비디오를 전체 화면으로 전환할 때 커스텀 컨트롤 바가 숨겨지고 기본 컨트롤 바가 표시됩니다. 따라서, 비디오와 컨트롤 바가 포함된 부모 요소에 전체 화면을 적용할 수 없습니다.
초기 비디오 상태 설정
class VideoPlayer {
#VIDEO_STATE = {};
#ELEMENTS = {};
constructor(selector) {
const target = document.querySelector(selector);
this.#ELEMENTS.container = target;
this.#VIDEO_STATE = {
totalDuration: 0, // 총 재생 시간
pausedAt: 0, // 멈춘 시점
playbackRate: 1.0, // 재생 속도
volume: 0.5, // 볼륨
rangeColor: null, // 볼륨 범위 색상
convertTime: 0, // 시간 변환용 숫자 (예: 124초를 분/초로 변환)
isPaused: true, // 비디오가 일시정지될 때 정지 이전에 일시정지 상태였는지 여부
isDragging: false, // 프로그래스 바 이벤트 처리용
};
this.#ELEMENTS = {
wrapper: target.querySelector(".video-player-wrapper"),
playPauseBtn: target.querySelector(".play-pause-btn"),
progressContainer: target.querySelector(".progress-section"),
progressBar: target.querySelector(".progress-bar"),
totalTimeText: target.querySelector(".total-time"),
currentTimeText: target.querySelector(".current-time"),
video: target.querySelector(".video-player"),
volumeBtn: target.querySelector(".volume-btn"),
volumeRange: target.querySelector(".volume-control"),
fullScreenBtn: target.querySelector(".fs-btn"),
pipBtn: target.querySelector(".pip-btn"),
settingsBtn: target.querySelector(".settings-btn"),
...this.#ELEMENTS,
};
if (this.#ELEMENTS.volumeRange) {
this.updateSliderBackground();
}
this.init();
}
init() {}
}
비디오 플레이어의 상태 관리를 위한 속성과 제어에 필요한 DOM 요소들을 쉽게 관리할 수 있도록 하나의 전역 객체에 모았습니다.
총 재생 시간 설정
init() {
this.#ELEMENTS.video.addEventListener("loadedmetadata", (e) => {
this.#ELEMENTS.totalTimeText.textContent = this.formatTime(e.target.duration);
this.#VIDEO_STATE.totalDuration = e.target.duration;
});
this.registerEvent();
}
formatTime(seconds) {
let minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
minutes = ("0" + minutes).slice(-2);
seconds = ("0" + seconds).slice(-2);
return `${minutes}:${seconds}`;
}
registerEvent() {}
HTMLVideoElement의 loadedmetadata 이벤트를 사용하여 비디오의 총 재생 시간을 표시합니다.
이 이벤트는 비디오가 처음 로드되거나 src가 변경될 때 발생합니다.
loadedmetadata에서 e.target.duration은 비디오의 총 재생 시간을 초 단위로 나타냅니다.
formatTime 메서드는 초 단위를 인자로 받아 03:02와 같은 두 자리 숫자 형식의 시간 문자열을 반환합니다.
모바일 볼륨 조절 슬라이더 제거
if (this.isMobile()) {
this.#ELEMENTS.volumeRange.classList.add("is-hidden");
}
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
모바일 기기에서는 isMobile 메서드를 사용하여 볼륨 범위를 숨깁니다. isMobile 메서드는 navigator.userAgent를 사용하여 현재 사용 중인 기기가 모바일 기기인지 확인하는 기능을 제공합니다.
비디오 재생
video.addEventListener("click", (e) => {
e.preventDefault();
video.paused ? video.play() : video.pause();
});
playPauseBtn.onclick = () => video.paused ? video.play() : video.pause();
비디오 재생은 HTMLVideoElement의 메서드인 play()와 pause()를 사용하여 구현했습니다.
video.paused 속성을 통해 비디오가 재생 중인지 일시정지 상태인지 여부를 boolean 값으로 확인할 수 있습니다.
비디오 요소에도 클릭 이벤트를 설정하여, 버튼이 아닌 비디오 자체를 클릭해서도 재생과 정지가 가능하도록 구현했습니다.
음소거/해제
volumeBtn?.addEventListener("click", () => {
video.muted = !video.muted;
if (volumeRange) {
volumeRange.value = video.muted ? 0 : this.#VIDEO_STATE.volume;
this.updateSliderBackground();
}
});
음소거 및 해제 기능은 HTMLVideoElement의 muted 속성을 사용하여 구현했습니다.
슬라이더 값 변경 시 배경색 업데이트(updateSliderBackground)
updateSliderBackground() {
const { volumeRange } = this.#ELEMENTS;
if (volumeRange) {
const percentage = volumeRange.value * 100;
volumeRange.style.background = `linear-gradient(to right, #fff ${percentage}%, #555 ${percentage}%)`;
}
}
volumeRange?.addEventListener("input", (e) => {
this.#VIDEO_STATE.volume = parseFloat(e.target.value);
video.volume = this.#VIDEO_STATE.volume;
video.muted = e.target.value <= 0;
this.updateSliderBackground();
});
슬라이더의 현재 값에 따라 백분율을 계산하고, linear-gradient를 사용하여 슬라이더의 배경색을 동적으로 변경합니다.
슬라이더의 왼쪽은 흰색(#fff)으로, 오른쪽은 회색(#555)으로 설정하여 사용자가 볼륨을 파악할 수 있도록 합니다.
재생/정지 및 볼륨 음소거/해제 아이콘 변경
video.addEventListener("play", (e) => {
playPauseBtn.classList.add("pause");
});
video.addEventListener("pause", (e) => {
playPauseBtn.classList.remove("pause");
});
video.addEventListener('volumechange', (e) => {
if (video.muted) {
volumeBtn.classList.add("mute");
} else {
volumeBtn.classList.remove("mute");
}
});
아이콘 변경은 단순히 클래스를 추가하거나 제거하는 방식으로 구현했습니다.
CSS에서 Font Awesome을 사용하여 :not(.pause)::before와 같은 선택자를 통해 각 상태에 맞는 content를 변경하기 때문입니다.
예를 들어, pause 클래스가 있을 때와 없을 때 각각 다른 아이콘이 표시되도록 설정했습니다. 자세한 내용은 이전 글을 참고해 주세요.
재생 속도 조절
<li data-playback-rate="0.25">0.25</li>
<li data-playback-rate="0.5">0.5</li>
<li data-playback-rate="0.75">0.75</li>
<li data-playback-rate="1.0">보통</li>
<li data-playback-rate="1.25">1.25</li>
<li data-playback-rate="1.5">1.5</li>
<li data-playback-rate="1.75">1.75</li>
<li data-playback-rate="2">2</li>
playbackSettings.addEventListener("click", (e) => {
const target = e.target.closest("li[data-playback-rate]");
if (target) {
const value = target.getAttribute("data-playback-rate");
if (value) {
this.#VIDEO_STATE.playbackRate = parseFloat(value);
video.playbackRate = parseFloat(value);
container.querySelector(".playback-text").textContent = target.textContent;
}
}
});
HTML에서 data-playback-rate 속성을 사용하여 재생 속도를 표시하고, JavaScript에서 이 값을 기반으로 HTMLVideoElement의 playbackRate 속성을 변경하여 재생 속도 변경을 구현했습니다.
PIP 기능
pipBtn?.addEventListener("click", () => {
if (document.pictureInPictureEnabled && video !== document.pictureInPictureElement) {
video.requestPictureInPicture();
} else {
document.exitPictureInPicture();
}
});
Picture-in-Picture(PiP) 기능은 HTMLVideoElement의 메서드인 requestPictureInPicture를 사용하여 구현했습니다.
- pictureInPictureEnabled: 브라우저가 PiP 기능을 지원하는지 여부를 나타냅니다.
- pictureInPictureElement: 현재 PiP 모드로 재생 중인 HTML 요소를 반환합니다. PiP 모드인 요소가 없으면 null을 반환합니다. 이를 통해 비디오가 이미 PiP 모드인지 확인합니다.
이 두 속성을 활용하여, PiP 기능이 활성화되지 않았을 때는 video.requestPictureInPicture()를 호출하여 비디오를 PiP 모드로 전환하고, 이미 PiP 모드인 경우 document.exitPictureInPicture()를 호출하여 PiP 모드를 종료합니다.
전체화면 기능
toggleFullscreen() {
const target = this.#ELEMENTS.wrapper;
this.#ELEMENTS.video.focus();
if (!document.fullscreenElement) {
this.#SCROLL_POS = window.scrollY;
if (target.requestFullscreen) {
target.requestFullscreen();
} else if (target.mozRequestFullScreen) {
target.mozRequestFullScreen();
} else if (this.#ELEMENTS.video.webkitEnterFullscreen) {
this.#ELEMENTS.video.webkitEnterFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozExitFullScreen) {
document.mozExitFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
fullScreenBtn.addEventListener("click", (e) => this.toggleFullscreen());
video.addEventListener("dblclick", () => this.toggleFullscreen());
fullscreenElement
- Web Api의 속성으로 현재 전체화면 모드에서 활성화된 요소를 나타냅니다. 이를 통해 전체화면 모드가 활성화되어 있는지 확인하고, 전체화면으로 전환할지 해제할지 구분할 수 있습니다.
this.#SCROLL_POS는 전체화면 모드로 전환할 때 현재 스크롤 위치를 기록합니다.
전체화면 모드에서 나갈 때, 페이지가 최상단으로 이동하지 않도록 스크롤 위치를 유지하기 위해 사용됩니다.
전체화면 기능은 Web API인 requestFullscreen()을 사용합니다. 브라우저별로 구현이 약간 다릅니다.
- iOS: iOS에서는 requestFullscreen()이 제대로 지원되지 않기 때문에, webkitEnterFullscreen()을 비디오 요소에 적용합니다.
- Firefox: Firefox에서는 mozRequestFullScreen()과 mozExitFullScreen() 메서드를 사용합니다. 특히, Firefox에서는 requestFullScreen의 대문자 'S'를 사용하는 점에 주의해야 합니다.
버튼뿐만 아니라 비디오 요소에도 더블 클릭 이벤트을 이용하여 전체화면 모드를 활성화하거나 해제할 수 있도록 설정했습니다.
전체화면 시 아이콘 변경
this.#ELEMENTS.container.addEventListener("fullscreenchange", (e) => {
if (document.fullscreenElement) {
wrapper.classList.add("full-screen");
} else {
window.scrollTo(0, this.#SCROLL_POS);
wrapper.classList.remove("full-screen");
}
});
전체 화면 시 아이콘 변경은 fullscreenchange 이벤트를 통해 구현했습니다. 이 이벤트는 전체 화면 모드가 변경될 때 발생합니다. fullscreenchange는 Web API의 일부로, 전체 화면 상태가 변할 때를 감지하는 데 사용됩니다.
이벤트를 사용하여 전체 화면 모드가 해제될 때 스크롤 위치를 원래 상태로 복원하도록 했습니다.
프로그래스 바 업데이트(updateProgressBar) 및 드래그 이벤트
updateProgressBar() {
const { video, currentTimeText, progressBar } = this.#ELEMENTS;
if (this.#VIDEO_STATE.isDragging) return;
const percentage = (this.#VIDEO_STATE.totalDuration > 0)
? (video.currentTime / this.#VIDEO_STATE.totalDuration) * 100
: 0;
progressBar.style.width = `${percentage}%`;
currentTimeText.textContent = this.formatTime(video.currentTime);
}
video.addEventListener("timeupdate", (e) => {
this.updateProgressBar();
});
updateProgressBar()는 HTMLVideoElement의 timeupdate 이벤트를 사용하여 구현했습니다.
currentTime은 HTMLVideoElement의 속성으로, 비디오의 현재 재생 시간을 초 단위로 나타냅니다. 이 속성을 통해 비디오의 재생 위치를 읽거나 조정할 수 있습니다.
이 이벤트는 비디오의 재생 시간이 업데이트될 때마다 호출됩니다.
percentage는 비디오의 현재 재생 시간과 총 재생 시간을 바탕으로 계산됩니다.
이를 통해 progressBar의 너비를 비율에 맞게 조정하고, currentTimeText에는 현재 시간을 표시합니다.
이 방식으로 비디오가 재생될 때 시간에 맞춰 프로그래스 바가 업데이트되도록 했습니다.
프로그래스 바 드래그 이벤트
updateProgressFromEvent(e) {
const { left, width } = this.#ELEMENTS.progressContainer.getBoundingClientRect();
const x = e.clientX - left;
const ratio = x / width;
const newTime = Math.min(Math.max(0, ratio * this.#VIDEO_STATE.totalDuration), this.#VIDEO_STATE.totalDuration);
this.updateProgressBarTo(newTime);
}
updateProgressBarTo(time) {
const { currentTimeText, progressBar } = this.#ELEMENTS;
const percentage = (time / this.#VIDEO_STATE.totalDuration) * 100;
currentTimeText.textContent = this.formatTime(time);
progressBar.style.width = percentage + '%';
}
const onPointerDown = (e) => {
e.preventDefault();
this.#VIDEO_STATE.isPaused = video.paused;
video.pause();
this.#VIDEO_STATE.isDragging = true;
this.updateProgressFromEvent(e.touches ? e.touches[0] : e);
};
const onPointerMove = (e) => {
if (!this.#VIDEO_STATE.isDragging) return false;
e.preventDefault();
this.updateProgressFromEvent(e.touches ? e.touches[0] : e);
};
const onPointerUp = (e) => {
if (this.#VIDEO_STATE.isDragging) {
this.#VIDEO_STATE.isDragging = false;
const { left, width } = this.#ELEMENTS.progressContainer.getBoundingClientRect();
const event = e.type === 'touchend' ? e.changedTouches[0] : e;
const x = event.clientX - left;
const ratio = x / width;
const newTime = Math.min(Math.max(0, ratio * this.#VIDEO_STATE.totalDuration), this.#VIDEO_STATE.totalDuration);
this.updateProgressBarTo(newTime);
video.currentTime = newTime;
}
};
progressContainer.addEventListener("mousedown", onPointerDown);
window.addEventListener("mousemove", onPointerMove);
window.addEventListener("mouseup", onPointerUp);
progressContainer.addEventListener("touchstart", onPointerDown, { passive: false });
window.addEventListener("touchmove", onPointerMove, { passive: false });
window.addEventListener("touchend", onPointerUp, { passive: false });
newTime은 마우스 위치와 프로그래스 바의 너비를 비율로 계산하여 구했습니다.
프로그래스 바를 이동하는 동안에는 currentTime이 실시간으로 업데이트되지 않도록 하고, 마우스를 떼면 currentTime이 업데이트되도록 구현했습니다.
isDragging 상태를 통해 드래그 중인지 여부를 판별하며, 프로그래스 바를 이동하는 동안 비디오가 일시 정지되도록 설정했습니다.
모바일 기기에서도 동작하도록 e.touches[0]를 사용하여 터치 이벤트를 처리했습니다. 이를 통해 사용자 입력에 따라 프로그래스 바를 드래그하여 비디오의 재생 시점을 조절할 수 있도록 구현했습니다.
passive: false는 모바일 브라우저에서 스크롤이나 터치 이벤트의 기본 동작을 방지할 수 있도록 설정합니다.
기본적으로, 이벤트 리스너는 passive 모드로 동작하며, 이는 브라우저가 스크롤 성능을 최적화하기 위해 이벤트 핸들러가 preventDefault()를 호출하지 않을 것이라고 가정합니다.
그러나 터치 이벤트나 스크롤 이벤트를 제어하려면 preventDefault()를 사용해야 할 경우가 있습니다.
이럴 때 passive: false를 설정하면 preventDefault() 호출이 정상적으로 작동하여 이벤트의 기본 동작을 막을 수 있습니다.
키보드 이벤트를 통한 Video 제어
video.addEventListener("keydown", (e) => {
if (["ArrowLeft", "ArrowRight", "Space"].includes(e.code)) {
e.preventDefault();
switch (e.code) {
case "ArrowLeft": {
video.currentTime -= 5;
break;
}
case "ArrowRight": {
video.currentTime += 5;
break;
}
case "Space": {
playPauseBtn.click();
break;
}
}
this.updateProgressBarTo(video.currentTime);
}
});
왼쪽 화살표 키를 누르면 비디오가 5초 뒤로 이동하고, 오른쪽 화살표 키를 누르면 5초 앞으로 이동합니다. 스페이스바를 누르면 비디오 재생/정지가 토글됩니다. 또한, 이러한 조작 후에는 프로그래스 바가 현재 비디오 시간에 맞게 업데이트됩니다.
로딩 스피너 표시
showLoadingSpinner(visible) {
const spinner = this.#ELEMENTS.container.querySelector(".video-player-loading");
if (!spinner) return false;
if (visible) {
spinner.classList.remove("is-hidden");
} else {
spinner.classList.add("is-hidden");
}
}
video.addEventListener("seeking", () => {
if (!this.#VIDEO_STATE.isDragging) {
this.showLoadingSpinner(true);
}
});
video.addEventListener("seeked", () => {
this.showLoadingSpinner(false);
if (!this.#VIDEO_STATE.isDragging && !this.#VIDEO_STATE.isPaused) {
video.play();
}
});
showLoadingSpinner(true): 로딩 스피너를 표시합니다.
showLoadingSpinner(false): 로딩 스피너를 숨깁니다.
seeking 이벤트: 사용자가 비디오의 currentTime을 변경할 때 발생합니다. 이 이벤트에서는 isDragging이 true일 때 로딩 스피너를 표시하지 않고 (showLoadingSpinner(false)), isDragging이 false일 때는 로딩 스피너를 표시합니다.
seeked 이벤트: currentTime 변경이 완료된 후 발생합니다. 이 이벤트에서는 로딩 스피너를 숨기고, isDragging이 false이고 비디오가 직전에 정지상태가 아니였던 경우 비디오를 재생합니다.
'JavaScript' 카테고리의 다른 글
Web API IntersectionObserver를 활용한 스크롤 관리 방법 (0) | 2024.08.31 |
---|---|
[JS/JavaScript] JavaScript로 쿠키(Cookie) 설정,읽기,삭제 구현 (0) | 2024.08.03 |
[HTML/CSS] Video Controls 커스텀하기 - 1 (0) | 2024.08.01 |
[JS/JavaScript] 드래그하여 요소 이동시키기 (0) | 2024.03.17 |
[JS/JavaScript] 드래그하여 체크박스 선택/해제하기 (0) | 2024.03.13 |
[HTML/CSS] Video Controls 커스텀하기 - 1
태그에서 제공하는 기본 컨트롤 디자인 대신, 다른 디자인을 적용하고 싶었습니다.하지만 HTML5 에서는 이러한 커스텀 디자인을 기본적으로 지원하지 않기 때문에, 기본 컨트롤을 숨기고 HTML과 CS
hy-un.tistory.com
이전 글에 이어, 이번에는 HTML과 CSS로 커스텀한 비디오 컨트롤에 JavaScript를 사용하여 기능을 입히려고 합니다.
완성 페이지는 아래 링크에서 확인할 수 있습니다.
https://ganghyun95.github.io/video-controls-custom/
Video Player Custom
재생속도 보통
ganghyun95.github.io
구현 계획
JavaScript 모듈화:
- JavaScript 파일은 재사용성을 높이기 위해 클래스로 모듈화합니다.
모바일 볼륨 조절:
- 모바일에서는 비디오의 볼륨 조절 슬라이더(range)를 제거합니다. 이는 사용자가 range 대신 볼륨 버튼을 사용하기도 하고, iOS에서는 비디오 태그의 볼륨 조절이 제한적이기 때문입니다. (iOS에서는 음소거만 가능합니다.)
Firefox PIP 버튼 제거:
- Firefox 브라우저에서는 PIP 버튼을 제거합니다. Firefox에서는 비디오 태그 자체적으로 PIP 기능을 지원하기 때문입니다.
iOS 풀스크린 컨트롤:
- iOS에서는 requestFullscreen이 제대로 지원되지 않으므로, 대체로 webkitEnterFullscreen을 사용합니다. 이 메서드는 비디오 태그에만 적용할 수 있어서, 비디오를 전체 화면으로 전환할 때 커스텀 컨트롤 바가 숨겨지고 기본 컨트롤 바가 표시됩니다. 따라서, 비디오와 컨트롤 바가 포함된 부모 요소에 전체 화면을 적용할 수 없습니다.
초기 비디오 상태 설정
JAVASCRIPT
class VideoPlayer {
#VIDEO_STATE = {};
#ELEMENTS = {};
constructor(selector) {
const target = document.querySelector(selector);
this.#ELEMENTS.container = target;
this.#VIDEO_STATE = {
totalDuration: 0, // 총 재생 시간
pausedAt: 0, // 멈춘 시점
playbackRate: 1.0, // 재생 속도
volume: 0.5, // 볼륨
rangeColor: null, // 볼륨 범위 색상
convertTime: 0, // 시간 변환용 숫자 (예: 124초를 분/초로 변환)
isPaused: true, // 비디오가 일시정지될 때 정지 이전에 일시정지 상태였는지 여부
isDragging: false, // 프로그래스 바 이벤트 처리용
};
this.#ELEMENTS = {
wrapper: target.querySelector(".video-player-wrapper"),
playPauseBtn: target.querySelector(".play-pause-btn"),
progressContainer: target.querySelector(".progress-section"),
progressBar: target.querySelector(".progress-bar"),
totalTimeText: target.querySelector(".total-time"),
currentTimeText: target.querySelector(".current-time"),
video: target.querySelector(".video-player"),
volumeBtn: target.querySelector(".volume-btn"),
volumeRange: target.querySelector(".volume-control"),
fullScreenBtn: target.querySelector(".fs-btn"),
pipBtn: target.querySelector(".pip-btn"),
settingsBtn: target.querySelector(".settings-btn"),
...this.#ELEMENTS,
};
if (this.#ELEMENTS.volumeRange) {
this.updateSliderBackground();
}
this.init();
}
init() {}
}
비디오 플레이어의 상태 관리를 위한 속성과 제어에 필요한 DOM 요소들을 쉽게 관리할 수 있도록 하나의 전역 객체에 모았습니다.
총 재생 시간 설정
JAVASCRIPT
init() {
this.#ELEMENTS.video.addEventListener("loadedmetadata", (e) => {
this.#ELEMENTS.totalTimeText.textContent = this.formatTime(e.target.duration);
this.#VIDEO_STATE.totalDuration = e.target.duration;
});
this.registerEvent();
}
formatTime(seconds) {
let minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
minutes = ("0" + minutes).slice(-2);
seconds = ("0" + seconds).slice(-2);
return `${minutes}:${seconds}`;
}
registerEvent() {}
HTMLVideoElement의 loadedmetadata 이벤트를 사용하여 비디오의 총 재생 시간을 표시합니다.
이 이벤트는 비디오가 처음 로드되거나 src가 변경될 때 발생합니다.
loadedmetadata에서 e.target.duration은 비디오의 총 재생 시간을 초 단위로 나타냅니다.
formatTime 메서드는 초 단위를 인자로 받아 03:02와 같은 두 자리 숫자 형식의 시간 문자열을 반환합니다.
모바일 볼륨 조절 슬라이더 제거
JAVASCRIPT
if (this.isMobile()) {
this.#ELEMENTS.volumeRange.classList.add("is-hidden");
}
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
모바일 기기에서는 isMobile 메서드를 사용하여 볼륨 범위를 숨깁니다. isMobile 메서드는 navigator.userAgent를 사용하여 현재 사용 중인 기기가 모바일 기기인지 확인하는 기능을 제공합니다.
비디오 재생
JAVASCRIPT
video.addEventListener("click", (e) => {
e.preventDefault();
video.paused ? video.play() : video.pause();
});
playPauseBtn.onclick = () => video.paused ? video.play() : video.pause();
비디오 재생은 HTMLVideoElement의 메서드인 play()와 pause()를 사용하여 구현했습니다.
video.paused 속성을 통해 비디오가 재생 중인지 일시정지 상태인지 여부를 boolean 값으로 확인할 수 있습니다.
비디오 요소에도 클릭 이벤트를 설정하여, 버튼이 아닌 비디오 자체를 클릭해서도 재생과 정지가 가능하도록 구현했습니다.
음소거/해제
JAVASCRIPT
volumeBtn?.addEventListener("click", () => {
video.muted = !video.muted;
if (volumeRange) {
volumeRange.value = video.muted ? 0 : this.#VIDEO_STATE.volume;
this.updateSliderBackground();
}
});
음소거 및 해제 기능은 HTMLVideoElement의 muted 속성을 사용하여 구현했습니다.
슬라이더 값 변경 시 배경색 업데이트(updateSliderBackground)
JAVASCRIPT
updateSliderBackground() {
const { volumeRange } = this.#ELEMENTS;
if (volumeRange) {
const percentage = volumeRange.value * 100;
volumeRange.style.background = `linear-gradient(to right, #fff ${percentage}%, #555 ${percentage}%)`;
}
}
JAVASCRIPT
volumeRange?.addEventListener("input", (e) => {
this.#VIDEO_STATE.volume = parseFloat(e.target.value);
video.volume = this.#VIDEO_STATE.volume;
video.muted = e.target.value <= 0;
this.updateSliderBackground();
});
슬라이더의 현재 값에 따라 백분율을 계산하고, linear-gradient를 사용하여 슬라이더의 배경색을 동적으로 변경합니다.
슬라이더의 왼쪽은 흰색(#fff)으로, 오른쪽은 회색(#555)으로 설정하여 사용자가 볼륨을 파악할 수 있도록 합니다.
재생/정지 및 볼륨 음소거/해제 아이콘 변경
JAVASCRIPT
video.addEventListener("play", (e) => {
playPauseBtn.classList.add("pause");
});
video.addEventListener("pause", (e) => {
playPauseBtn.classList.remove("pause");
});
video.addEventListener('volumechange', (e) => {
if (video.muted) {
volumeBtn.classList.add("mute");
} else {
volumeBtn.classList.remove("mute");
}
});
아이콘 변경은 단순히 클래스를 추가하거나 제거하는 방식으로 구현했습니다.
CSS에서 Font Awesome을 사용하여 :not(.pause)::before와 같은 선택자를 통해 각 상태에 맞는 content를 변경하기 때문입니다.
예를 들어, pause 클래스가 있을 때와 없을 때 각각 다른 아이콘이 표시되도록 설정했습니다. 자세한 내용은 이전 글을 참고해 주세요.
재생 속도 조절
HTML
<li data-playback-rate="0.25">0.25</li>
<li data-playback-rate="0.5">0.5</li>
<li data-playback-rate="0.75">0.75</li>
<li data-playback-rate="1.0">보통</li>
<li data-playback-rate="1.25">1.25</li>
<li data-playback-rate="1.5">1.5</li>
<li data-playback-rate="1.75">1.75</li>
<li data-playback-rate="2">2</li>
HTMLplaybackSettings.addEventListener("click", (e) => { const target = e.target.closest("li[data-playback-rate]"); if (target) { const value = target.getAttribute("data-playback-rate"); if (value) { this.#VIDEO_STATE.playbackRate = parseFloat(value); video.playbackRate = parseFloat(value); container.querySelector(".playback-text").textContent = target.textContent; } } });
HTML에서 data-playback-rate 속성을 사용하여 재생 속도를 표시하고, JavaScript에서 이 값을 기반으로 HTMLVideoElement의 playbackRate 속성을 변경하여 재생 속도 변경을 구현했습니다.
PIP 기능
HTMLpipBtn?.addEventListener("click", () => { if (document.pictureInPictureEnabled && video !== document.pictureInPictureElement) { video.requestPictureInPicture(); } else { document.exitPictureInPicture(); } });
Picture-in-Picture(PiP) 기능은 HTMLVideoElement의 메서드인 requestPictureInPicture를 사용하여 구현했습니다.
- pictureInPictureEnabled: 브라우저가 PiP 기능을 지원하는지 여부를 나타냅니다.
- pictureInPictureElement: 현재 PiP 모드로 재생 중인 HTML 요소를 반환합니다. PiP 모드인 요소가 없으면 null을 반환합니다. 이를 통해 비디오가 이미 PiP 모드인지 확인합니다.
이 두 속성을 활용하여, PiP 기능이 활성화되지 않았을 때는 video.requestPictureInPicture()를 호출하여 비디오를 PiP 모드로 전환하고, 이미 PiP 모드인 경우 document.exitPictureInPicture()를 호출하여 PiP 모드를 종료합니다.
전체화면 기능
JAVASCRIPT
toggleFullscreen() {
const target = this.#ELEMENTS.wrapper;
this.#ELEMENTS.video.focus();
if (!document.fullscreenElement) {
this.#SCROLL_POS = window.scrollY;
if (target.requestFullscreen) {
target.requestFullscreen();
} else if (target.mozRequestFullScreen) {
target.mozRequestFullScreen();
} else if (this.#ELEMENTS.video.webkitEnterFullscreen) {
this.#ELEMENTS.video.webkitEnterFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozExitFullScreen) {
document.mozExitFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
fullScreenBtn.addEventListener("click", (e) => this.toggleFullscreen());
video.addEventListener("dblclick", () => this.toggleFullscreen());
fullscreenElement
- Web Api의 속성으로 현재 전체화면 모드에서 활성화된 요소를 나타냅니다. 이를 통해 전체화면 모드가 활성화되어 있는지 확인하고, 전체화면으로 전환할지 해제할지 구분할 수 있습니다.
this.#SCROLL_POS는 전체화면 모드로 전환할 때 현재 스크롤 위치를 기록합니다.
전체화면 모드에서 나갈 때, 페이지가 최상단으로 이동하지 않도록 스크롤 위치를 유지하기 위해 사용됩니다.
전체화면 기능은 Web API인 requestFullscreen()을 사용합니다. 브라우저별로 구현이 약간 다릅니다.
- iOS: iOS에서는 requestFullscreen()이 제대로 지원되지 않기 때문에, webkitEnterFullscreen()을 비디오 요소에 적용합니다.
- Firefox: Firefox에서는 mozRequestFullScreen()과 mozExitFullScreen() 메서드를 사용합니다. 특히, Firefox에서는 requestFullScreen의 대문자 'S'를 사용하는 점에 주의해야 합니다.
버튼뿐만 아니라 비디오 요소에도 더블 클릭 이벤트을 이용하여 전체화면 모드를 활성화하거나 해제할 수 있도록 설정했습니다.
전체화면 시 아이콘 변경
JAVASCRIPT
this.#ELEMENTS.container.addEventListener("fullscreenchange", (e) => {
if (document.fullscreenElement) {
wrapper.classList.add("full-screen");
} else {
window.scrollTo(0, this.#SCROLL_POS);
wrapper.classList.remove("full-screen");
}
});
전체 화면 시 아이콘 변경은 fullscreenchange 이벤트를 통해 구현했습니다. 이 이벤트는 전체 화면 모드가 변경될 때 발생합니다. fullscreenchange는 Web API의 일부로, 전체 화면 상태가 변할 때를 감지하는 데 사용됩니다.
이벤트를 사용하여 전체 화면 모드가 해제될 때 스크롤 위치를 원래 상태로 복원하도록 했습니다.
프로그래스 바 업데이트(updateProgressBar) 및 드래그 이벤트
JAVASCRIPT
updateProgressBar() {
const { video, currentTimeText, progressBar } = this.#ELEMENTS;
if (this.#VIDEO_STATE.isDragging) return;
const percentage = (this.#VIDEO_STATE.totalDuration > 0)
? (video.currentTime / this.#VIDEO_STATE.totalDuration) * 100
: 0;
progressBar.style.width = `${percentage}%`;
currentTimeText.textContent = this.formatTime(video.currentTime);
}
video.addEventListener("timeupdate", (e) => {
this.updateProgressBar();
});
updateProgressBar()는 HTMLVideoElement의 timeupdate 이벤트를 사용하여 구현했습니다.
currentTime은 HTMLVideoElement의 속성으로, 비디오의 현재 재생 시간을 초 단위로 나타냅니다. 이 속성을 통해 비디오의 재생 위치를 읽거나 조정할 수 있습니다.
이 이벤트는 비디오의 재생 시간이 업데이트될 때마다 호출됩니다.
percentage는 비디오의 현재 재생 시간과 총 재생 시간을 바탕으로 계산됩니다.
이를 통해 progressBar의 너비를 비율에 맞게 조정하고, currentTimeText에는 현재 시간을 표시합니다.
이 방식으로 비디오가 재생될 때 시간에 맞춰 프로그래스 바가 업데이트되도록 했습니다.
프로그래스 바 드래그 이벤트
JAVASCRIPT
updateProgressFromEvent(e) {
const { left, width } = this.#ELEMENTS.progressContainer.getBoundingClientRect();
const x = e.clientX - left;
const ratio = x / width;
const newTime = Math.min(Math.max(0, ratio * this.#VIDEO_STATE.totalDuration), this.#VIDEO_STATE.totalDuration);
this.updateProgressBarTo(newTime);
}
updateProgressBarTo(time) {
const { currentTimeText, progressBar } = this.#ELEMENTS;
const percentage = (time / this.#VIDEO_STATE.totalDuration) * 100;
currentTimeText.textContent = this.formatTime(time);
progressBar.style.width = percentage + '%';
}
const onPointerDown = (e) => {
e.preventDefault();
this.#VIDEO_STATE.isPaused = video.paused;
video.pause();
this.#VIDEO_STATE.isDragging = true;
this.updateProgressFromEvent(e.touches ? e.touches[0] : e);
};
const onPointerMove = (e) => {
if (!this.#VIDEO_STATE.isDragging) return false;
e.preventDefault();
this.updateProgressFromEvent(e.touches ? e.touches[0] : e);
};
const onPointerUp = (e) => {
if (this.#VIDEO_STATE.isDragging) {
this.#VIDEO_STATE.isDragging = false;
const { left, width } = this.#ELEMENTS.progressContainer.getBoundingClientRect();
const event = e.type === 'touchend' ? e.changedTouches[0] : e;
const x = event.clientX - left;
const ratio = x / width;
const newTime = Math.min(Math.max(0, ratio * this.#VIDEO_STATE.totalDuration), this.#VIDEO_STATE.totalDuration);
this.updateProgressBarTo(newTime);
video.currentTime = newTime;
}
};
progressContainer.addEventListener("mousedown", onPointerDown);
window.addEventListener("mousemove", onPointerMove);
window.addEventListener("mouseup", onPointerUp);
progressContainer.addEventListener("touchstart", onPointerDown, { passive: false });
window.addEventListener("touchmove", onPointerMove, { passive: false });
window.addEventListener("touchend", onPointerUp, { passive: false });
newTime은 마우스 위치와 프로그래스 바의 너비를 비율로 계산하여 구했습니다.
프로그래스 바를 이동하는 동안에는 currentTime이 실시간으로 업데이트되지 않도록 하고, 마우스를 떼면 currentTime이 업데이트되도록 구현했습니다.
isDragging 상태를 통해 드래그 중인지 여부를 판별하며, 프로그래스 바를 이동하는 동안 비디오가 일시 정지되도록 설정했습니다.
모바일 기기에서도 동작하도록 e.touches[0]를 사용하여 터치 이벤트를 처리했습니다. 이를 통해 사용자 입력에 따라 프로그래스 바를 드래그하여 비디오의 재생 시점을 조절할 수 있도록 구현했습니다.
passive: false는 모바일 브라우저에서 스크롤이나 터치 이벤트의 기본 동작을 방지할 수 있도록 설정합니다.
기본적으로, 이벤트 리스너는 passive 모드로 동작하며, 이는 브라우저가 스크롤 성능을 최적화하기 위해 이벤트 핸들러가 preventDefault()를 호출하지 않을 것이라고 가정합니다.
그러나 터치 이벤트나 스크롤 이벤트를 제어하려면 preventDefault()를 사용해야 할 경우가 있습니다.
이럴 때 passive: false를 설정하면 preventDefault() 호출이 정상적으로 작동하여 이벤트의 기본 동작을 막을 수 있습니다.
키보드 이벤트를 통한 Video 제어
JAVASCRIPT
video.addEventListener("keydown", (e) => {
if (["ArrowLeft", "ArrowRight", "Space"].includes(e.code)) {
e.preventDefault();
switch (e.code) {
case "ArrowLeft": {
video.currentTime -= 5;
break;
}
case "ArrowRight": {
video.currentTime += 5;
break;
}
case "Space": {
playPauseBtn.click();
break;
}
}
this.updateProgressBarTo(video.currentTime);
}
});
왼쪽 화살표 키를 누르면 비디오가 5초 뒤로 이동하고, 오른쪽 화살표 키를 누르면 5초 앞으로 이동합니다. 스페이스바를 누르면 비디오 재생/정지가 토글됩니다. 또한, 이러한 조작 후에는 프로그래스 바가 현재 비디오 시간에 맞게 업데이트됩니다.
로딩 스피너 표시
JAVASCRIPT
showLoadingSpinner(visible) {
const spinner = this.#ELEMENTS.container.querySelector(".video-player-loading");
if (!spinner) return false;
if (visible) {
spinner.classList.remove("is-hidden");
} else {
spinner.classList.add("is-hidden");
}
}
video.addEventListener("seeking", () => {
if (!this.#VIDEO_STATE.isDragging) {
this.showLoadingSpinner(true);
}
});
video.addEventListener("seeked", () => {
this.showLoadingSpinner(false);
if (!this.#VIDEO_STATE.isDragging && !this.#VIDEO_STATE.isPaused) {
video.play();
}
});
showLoadingSpinner(true): 로딩 스피너를 표시합니다.
showLoadingSpinner(false): 로딩 스피너를 숨깁니다.
seeking 이벤트: 사용자가 비디오의 currentTime을 변경할 때 발생합니다. 이 이벤트에서는 isDragging이 true일 때 로딩 스피너를 표시하지 않고 (showLoadingSpinner(false)), isDragging이 false일 때는 로딩 스피너를 표시합니다.
seeked 이벤트: currentTime 변경이 완료된 후 발생합니다. 이 이벤트에서는 로딩 스피너를 숨기고, isDragging이 false이고 비디오가 직전에 정지상태가 아니였던 경우 비디오를 재생합니다.
'JavaScript' 카테고리의 다른 글
Web API IntersectionObserver를 활용한 스크롤 관리 방법 (0) | 2024.08.31 |
---|---|
[JS/JavaScript] JavaScript로 쿠키(Cookie) 설정,읽기,삭제 구현 (0) | 2024.08.03 |
[HTML/CSS] Video Controls 커스텀하기 - 1 (0) | 2024.08.01 |
[JS/JavaScript] 드래그하여 요소 이동시키기 (0) | 2024.03.17 |
[JS/JavaScript] 드래그하여 체크박스 선택/해제하기 (0) | 2024.03.13 |