JS30 day11 Custom HTML5 Video Player
JS30 day11 - Custom HTML5 Video Player
作業內容
今天要做出客制化的 video player,包括播放鍵 / 可以拉動影片進度條等等…

學到什麼
-
CSS
.player:fullscreen這只有在全螢幕的時候會有用
-
JS
- 影片有個屬性叫做
paused,影片播放或暫停的方法叫做play()跟pause(),很直觀
function togglePlay(){ if(video.paused){ video.play(); }else{ video.pause(); } }- 影片有個屬性叫做現在播放的時間
currentTime,JS 裡面把字串轉數字使用parseFloat,稍微查了一下發現還可以使用Number(),但是他有時候會出現 NaN,所以用 parse 比較安全 參考資料:https://gomakethings.com/converting-strings-to-numbers-with-vanilla-javascript/
function skip(){ video.currentTime += parseFloat(this.dataset.skip) }- 如果要改變影片屬性,可以很直觀地像下面這樣使用,在這次例子中,可以用同一行使用在音量或者播放速度上面
function handleRangeUpdate(){ video[this.name] = this.value }- 這個例子中的進度條,使用 flexbasis 這個 CSS 屬性改變鄉端寬度,日後可以參考,另外影片的 duration 屬性代表這個影片的全長
function handleProgress(){ const percent = (video.currentTime / video.duration) * 100; progressBar.style.flexBasis = `${percent}%`; }- 這裏的
progress.offsetWidth代表這個物件的總寬度
function scrub(e){ const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration video.currentTime = scrubTime }- 在 JS 裡面的物件都是 hash 的形式,因此下面兩個方法是一樣的,
video.play拿到的屬性等於video[play]拿到的屬性,只是這個屬性是一個方法
function togglePlay(){ if(video.paused){ video.play(); }else{ video.pause(); } } // 效果等同於下面的 function togglePlay(){ const method = video.paused ? 'play' : 'pause'; video[method](); }- 因為想做出可以拉動影片長度的效果,所以除了監聽
mousemove事件之外,還要有mousedown才會觸動事件,因此寫成下面這樣
let mousedown = false; progress.addEventListener('click', scrub) progress.addEventListener('mousemove', (e)=>{ if(mousedown){ scrub(e); } }) progress.addEventListener('mousedown', () => mousedown = true) progress.addEventListener('mouseup', () => mousedown = false)- 同時監聽
mousedown跟mousemove又可以寫成下面那種樣子,如果 mousedown 是 true 就會觸發scrub()function
progress.addEventListener('mousemove', (e)=>{ if(mousedown){ scrub(e); } }) // 效果等同於下面的 progress.addEventListener('mousemove', (e) => mousedown && scrub(e)) - 影片有個屬性叫做
參考資料: https://github.com/wesbos/JavaScript30
code 內容: HTML:
<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>
</div>
</div>
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: 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;
}
JS:
// get element
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 skipButtons = player.querySelectorAll('[data-skip]');
const ranges = player.querySelectorAll('.player__slider');
// build functions
function togglePlay(){
if(video.paused){
video.play();
}else{
video.pause();
}
}
// 另一種方法
// function togglePlay(){
// const method = video.paused ? 'play' : 'pause';
// video[method]();
// }
function updateButton(){
const icon = this.paused ? '►' : '❚ ❚'
toggle.textContent = icon
}
function skip(){
video.currentTime += parseFloat(this.dataset.skip)
}
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
}
// 綁定事件
video.addEventListener('click', togglePlay);
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
toggle.addEventListener('click', togglePlay);
skipButtons.forEach(function(button){
button.addEventListener('click', skip)
});
ranges.forEach(function(range){
range.addEventListener('change', handleRangeUpdate)
})
ranges.forEach(function(range){
range.addEventListener('mousemove', handleRangeUpdate)
})
video.addEventListener('timeupdate', handleProgress)
let mousedown = false;
progress.addEventListener('click', scrub)
progress.addEventListener('mousemove', (e)=>{
if(mousedown){
scrub(e);
}
})
// progress.addEventListener('mousemove', (e) => mousedown && scrub(e))
progress.addEventListener('mousedown', () => mousedown = true)
progress.addEventListener('mouseup', () => mousedown = false)