JS30 day11 Custom HTML5 Video Player

Posted by Anthony Chao on 2019-11-11

JS30 day11 - Custom HTML5 Video Player

作業內容

今天要做出客制化的 video player,包括播放鍵 / 可以拉動影片進度條等等…

學到什麼

  • CSS

    1. .player:fullscreen 這只有在全螢幕的時候會有用
  • JS

    1. 影片有個屬性叫做 paused,影片播放或暫停的方法叫做 play()pause(),很直觀
    1
    2
    3
    4
    5
    6
    7
    function togglePlay(){
    if(video.paused){
    video.play();
    }else{
    video.pause();
    }
    }
    1. 影片有個屬性叫做現在播放的時間 currentTime,JS 裡面把字串轉數字使用 parseFloat,稍微查了一下發現還可以使用 Number(),但是他有時候會出現 NaN,所以用 parse 比較安全
      參考資料:https://gomakethings.com/converting-strings-to-numbers-with-vanilla-javascript/
    1
    2
    3
    function skip(){
    video.currentTime += parseFloat(this.dataset.skip)
    }
    1. 如果要改變影片屬性,可以很直觀地像下面這樣使用,在這次例子中,可以用同一行使用在音量或者播放速度上面
    1
    2
    3
    function handleRangeUpdate(){
    video[this.name] = this.value
    }
    1. 這個例子中的進度條,使用 flexbasis 這個 CSS 屬性改變鄉端寬度,日後可以參考,另外影片的 duration 屬性代表這個影片的全長
    1
    2
    3
    4
    function handleProgress(){
    const percent = (video.currentTime / video.duration) * 100;
    progressBar.style.flexBasis = `${percent}%`;
    }
    1. 這裏的 progress.offsetWidth 代表這個物件的總寬度
    1
    2
    3
    4
    function scrub(e){
    const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration
    video.currentTime = scrubTime
    }
    1. 在 JS 裡面的物件都是 hash 的形式,因此下面兩個方法是一樣的, video.play 拿到的屬性等於 video[play] 拿到的屬性,只是這個屬性是一個方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function togglePlay(){
    if(video.paused){
    video.play();
    }else{
    video.pause();
    }
    }
    // 效果等同於下面的
    function togglePlay(){
    const method = video.paused ? 'play' : 'pause';
    video[method]();
    }
    1. 因為想做出可以拉動影片長度的效果,所以除了監聽 mousemove 事件之外,還要有 mousedown 才會觸動事件,因此寫成下面這樣
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)
    1. 同時監聽 mousedownmousemove 又可以寫成下面那種樣子,如果 mousedown 是 true 就會觸發 scrub() function
    1
    2
    3
    4
    5
    6
    7
    progress.addEventListener('mousemove', (e)=>{
    if(mousedown){
    scrub(e);
    }
    })
    // 效果等同於下面的
    progress.addEventListener('mousemove', (e) => mousedown && scrub(e))

參考資料:
https://github.com/wesbos/JavaScript30

code 內容:
HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 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)




prevent_hack