auth : corner
date : 05-29
준비 파일 : 한 개의 video 영상
[Video Player HTML/CSS](##Video Player HTML/CSS)
💡 HTML, Javascript로 구성하는 Youtube-Player를 Nuxt.js로 개발하는 방법을 제시하고 있습니다.
글쓴이의 운영체제는 Mac OS이므로 이를 감안하시어 읽어주시길 바랍니다.
맥에서 Homebrew를 이용하여 ffmpeg 설치를 합니다.
💡 Homebrew가 설치되어있지 않다면 Mac M1 Homebrew InstallGuide의 문서를 참조하세요.
다음 명령어를 실행하면 바로 FFmpeg 설치가 됩니다. 의존 패키지가 많아 시간이 많이 소요됩니다.
intel brew install ffmpeg
m1 arch -arm64 brew install ffmpeg
$ brew install ffmpeg
or ||
$ arch -arm64 brew install ffmpeg
...
==> Auto-updated Homebrew!
...
==> New Formulae
...
FFmpeg을 설치하고 ffmpeg을 인자없이 실행하면 버전, 빌드 옵션, 기본적인 사용 방법이 출력됩니다.
$ ffmpeg
ffmpeg version 5.0 Copyright ....
프로젝트 폴더에서 assets/
경로를 생성하고 Video영상과 previewImgs/
경로에 영상 중간중간을 캡처해서 미리보기가 될 스크린샷을 넣어둡니다.
영상을 넣어두고 ffmpeg 명령어를 이용하여 해당 비디오의 preview 이미지를 생성합니다.
영상 길이의 프레임 1/10 만큼 썸네일을 찍는다는 것인데 저의 영상은 6초짜리라서 1장의 썸네일만 생성되었습니다.
$ ffmpeg -i assets/my_video.mp4 -vf fps=1/10,scale=120:-1 assets/previewImgs/preview%d.jpg
명령어를 수행하고 난 뒤 터미널에서 작업에 대한 내용이 출력됩니다.
[swscaler @ 0x109030000] [swscaler @ 0x148d68000] deprecated pixel format used, make sure you did set range correctly
...
최종구조
Nuxt에서 HTML과 CSS를 작업하기 위해 index.vue로 이동합니다.
<template>
<div>
<h1> YOUTUBE - VIDEO - PLAYER </h1>
<video src="@/assets/my_video.mp4"></video>
</div>
</template>
<script>
export default {
name: 'IndexPage'
}
</script>
video 태그에 assets 경로로 넣은 비디오를 넣고 서버를 실행해서 영상이 뜨는지 확인해봅니다.
우리는 간단한 style을 이용해서 video를 youtube처럼 화면 사이즈에따라 줄어들고 커지는 것 처럼 반응하도록 작업하겠습니다.
<template>
<div>
<h1> YOUTUBE - VIDEO - PLAYER </h1>
<div class="video-container">
<video src="@/assets/my_video.mp4"></video>
</div>
</div>
</template>
<script>
export default {
name: 'IndexPage'
}
</script>
<style scoped>
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
}
.video-container {
width: 90%;
max-width: 1000px;
display: flex;
justify-content: center;
margin-inline: auto;
}
video {
width: 100%;
height: auto;
}
</style>
video 태그를 <div class='video-container'>
로 감싸고 , 위 코드처럼 스타일을 적용했을 때 사이즈가 적절히 변하는지 확인해봅니다.
저는 <video src="..." controls>
태그에 controls 속성을 추가했습니다.
하지만 우리는 유튜브 같은 컨트롤을 이용할 것이기 때문에 수정을 해야합니다.
우선 template 코드를 아래와 같이 수정합니다.
<div class="video-container">
<div class="video-controls-container">
<div class="timeline-container"></div>
<div class="controls">
<button class="play-pause-btn">
Play
</button>
</div>
</div>
<video src="@/assets/my_iu.mp4"></video>
</div>
css를 아래와 같이 수정합니다.
.video-container {
width: 90%;
max-width: 1000px;
display: flex;
justify-content: center;
margin-inline: auto;
position: relative;
}
video {
width: 100%;
}
.video-controls-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
color: white;
z-index: 100;
opacity: 0;
transition: opacity 150ms ease-in-out;
}
.video-container:hover .video-controls-container,
.video-container:focus-within .video-controls-container,
.video-container.paused .video-controls-container {
opacity: 1;
}
.video-controls-container .controls {
display: flex;
gap: .5rem;
padding: .25rem;
align-items: center;
}
.video-controls-container .controls button {
background: none;
border: none;
color: inherit;
padding: 0;
/*height: 30px;*/
width: 30px;
font-size: 1.1rem;
}
.video-controls-container .controls {
display: flex;
gap: .5rem;
padding: .25rem;
align-items: center;
}
영상과 컨트롤러에 마우스가 올려지거나, 재생중일 때, 클릭했을 때 컨트롤이 자연스럽게 보여지도록 하는 transition 애니메이션 처리도 해줍니다.
이제 컨트롤바에서 Play버튼을 유튜브 아이콘 처럼 변경해야합니다. 저는 유튜브에서 개발자모드로 소스를 찍어서 svg 코드를 가져왔습니다.
임의 아이콘을 사용하여도 좋지만, 어렵거나 큰 문제도 아니고 간편하게 하기 위해 유튜브를 모방하는 것이 목적이기 때문에 그렇게 했습니다.
<button class="play-pause-btn">
<svg class="play-icon" height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><use class="ytp-svg-shadow" xlink:href="#ytp-id-935"></use><path class="ytp-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-935"></path></svg>
<svg class="pause-icon" height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><use class="ytp-svg-shadow" xlink:href="#ytp-id-974"></use><path class="ytp-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="ytp-id-974"></path></svg>
</button>
<button class="play-pause-btn">
<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><use class="ytp-svg-shadow" xlink:href="#ytp-id-12"></use><path class="ytp-svg-fill" d="M 12,24 20.5,18 12,12 V 24 z M 22,12 v 12 h 2 V 12 h -2 z" id="ytp-id-12"></path></svg>
</button>
컨트롤 클래스 태그 아래 버튼 태그에 svg를 가져오거나, 아이콘을 넣습니다.
.video-controls-container .controls button {
background: none;
border: none;
color: inherit;
padding: 0;
/*height: 30px;*/
width: 30px;
font-size: 1.1rem;
}
.video-container.paused .pause-icon {
display: none;
}
.video-container:not(.paused) .play-icon {
display: none;
}
스타일을 적용해줍니다.
영상이 재생중이면 일시정지 버튼을, 일시정지 상태면 재생버튼이 뜨도록 처리할 것입니다.
현재는 <div class="video-container">
이렇게 paused 클래스가 없을 땐 일시정지를, paused 클래스를 넣으면 재생 버튼이 뜨는 상태가 되도록 css 처리만 하였습니다.
.video-controls-container .contorls button {}
css 코드에 추가합니다.
cursor: pointer;
opacity: .85;
transition: opacity 150ms ease-in-out;
그리고 위 코드 아래에 컨트롤 버튼위에 올려질 경우 버튼이 보여지도록 새 css를 넣습니다.
.video-controls-container .controls button:hover {
opacity: 1;
}
이제 css 처리는 다 끝났으니 nuxt(vue)를 이용해서 아이콘을 변경하고, 플레이어를 재생과 일시정지를 시키는 작업입니다.
바닐라 javascript로 짜야한다면 document.querySelect 등을 이용해야겠지만, Vue/Nuxt에서 사용하는 ref를 써서 이용합니다.
html태그 video 태그에 ref="video" 라는 참조변수를 작성합니다.
<video src="@/assets/my_iu.mp4" ref="video"></video>
재생과 일시정지를 반복하는 버튼태그에 on.click 함수를 적습니다.
<button class="play-pause-btn" @click="togglePlay()">
최상위 video-container 태그에는 재생과 일시정지 토글에 맞춰서 아이콘이 변경될 클래스 바인딩을 합니다.
<div class="video-container" :class="{'paused' : paused}">
data() {
return {
paused: true,
}
},
methods: {
togglePlay() {
if (this.$refs.video.paused) {
this.paused = false;
this.$refs.video.play();
} else {
this.paused = true;
this.$refs.video.pause();
}
},
}
먼저 paused 라는 데이터를 선언하고, 영상은 항상 처음엔 일시정지 상태이기 때문에 true
값으로 정합니다.
테스트해보면 결과는 재생과 일시정지에 맞춰서 비디오가 재생/일시정지 되며, 아이콘도 상황에 적절히 맞춰서 변합니다.
하지만, 유튜브는 첫 진입시 바로 재생이기 때문에 autoplay를 걸어야합니다.
<video src="@/assets/my_iu.mp4" ref="video" autoplay></video>
그리고 데이터 선언한 paused
값은 false
로 변경합니다.
이제 영상을 스페이스바로 재생 / 일시정지 컨트롤을 해야합니다.
template 코드에서 @keydown.space
, @keydown.space.native
, 등 많은 방법을 사용해봤지만 가능한 방법은 DOM 접근뿐이었습니다.
더 좋은 방법이 있다면 솔루션을 제안해주세요.
mounted() {
window.addEventListener('keyup', ((ev) => {
let key = ev.key.toLowerCase()
switch (key) {
case " ": break;
case "t" : this.toggleTheater(); break;
case "p": this.togglePlay(); break;
default: break;
}
if (ev.keyCode === 32) {
this.togglePlay();
}
}));
},
그리고 Video 화면을 클릭해서도 재생/일시정지가 컨트롤 되도록 합니다.
<video src="@/assets/my_iu.mp4" ref="video" @click="togglePlay" autoplay></video>
<button class="mini-player-btn">
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zm-10-7h9v6h-9z"/>
</svg>
</button>
<button class="theater-btn">
<svg class="tall" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 6H5c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 10H5V8h14v8z"/>
</svg>
<svg class="wide" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 7H5c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm0 8H5V9h14v6z"/>
</svg>
</button>
<button class="full-screen-btn">
<svg class="open" viewBox="0 0 24 24">
<path fill="currentColor" d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>
<svg class="close" viewBox="0 0 24 24">
<path fill="currentColor" d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
</svg>
</button>
버튼을 추가하고, style을 변경합니다.
.video-container {
width: 90%;
max-width: 1000px;
display: flex;
justify-content: center;
margin-inline: auto;
position: relative;
background-color: black; /* 추가 */
}
/* 추가 */
.video-container.theater,
.video-container.full-screen{
max-width: initial;
width: 100%;
}
.video-container.theater {
max-height: 90vh;
}
.video-container.full-screen {
max-height: 100vh;
}
methods안에 함수를 생성합니다.
data() {
return {
paused: false,
theaterMode: false,
fullscreen : false,
miniMode : false,
}
},
mounted() {
window.addEventListener('keyup', ((ev) => {
const key = ev.key.toLowerCase()
const tagName = window.document.activeElement.tagName.toLowerCase();
if (key === 'input') return;
switch (key) {
case " ": break;
case "t" : this.toggleTheater(); break;
case "p": this.togglePlay(); break;
case "i": this.toggleMiniMode(); break;
case "f": this.toggleFullScreen(); break;
default: break;
}
if (ev.keyCode === 32) {
this.togglePlay();
}
}));
document.addEventListener("fullscreenchange", () => {
this.$refs.video_container.classList.toggle('full-screen');
});
this.$refs.video.addEventListener('enterpictureinpicture', () => {
this.$refs.video_container.classList.add("mini-player");
});
this.$refs.video.addEventListener('leavepictureinpicture', () => {
this.$refs.video_container.classList.remove("mini-player");
});
},
methods: {
togglePlay() {
if (this.$refs.video.paused) {
this.paused = false;
this.$refs.video.play();
} else {
this.paused = true;
this.$refs.video.pause();
}
},
// view Mode
toggleTheater() {
this.$refs.video_container.classList.toggle('theater');
},
toggleFullScreen() {
if (document.fullscreenElement == null) {
this.$refs.video_container.requestFullscreen();
} else {
document.exitFullscreen();
}
},
toggleMiniMode() {
if (this.$refs.video_container.classList.contains('mini-player')) {
document.exitPictureInPicture();
} else {
this.$refs.video.requestPictureInPicture();
}
},
},
template 코드에서 각자 버튼에 맞게 이벤트 클릭 함수를 추가해주고, video-container에 클래스 바인딩합니다.
<div class="video-container" :class="[{'paused' : paused}, {'theater' : theaterMode}, {'full-screen': fullscreen}]" ref="video_container">
이후 아이콘 작업들은 모두 깃허브 소스를 확인해주세요.