自製一個影片播放器的介面, 內容將包含:
以下為HTML格式:
<div class="player">
<video class="player__video viewer" src="https://player.vimeo.com/external/194837908.sd.mp4?s=c350076905b78c67f74d7ee39fdb4fef01d12420&profile_id=164"></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">FS</button>
</div>
</div>
先稍微了解一下HTML檔案內容的含義. <video>
標籤帶有原生的播放器介面, 在標籤內加上controls
屬性, 就能使用該介面. 另外, 對video
標籤本身進行全屏顯示時, 該介面也會自動出現.
因此若要自訂播放器介面, video
內不能放controls
, 自訂的播放器介面可以放在video
標籤下方, 並將兩者包裝在新的<div>
下. 該<div>
就是我們切換全屏時要作用的<div>
, 如此一來, 在全屏時就不會跑出<video>
原生的控制器.
接下來看看如何實踐各功能鈕. 播放器介面會接收使用者的操作並進行回饋, 因此
大多遵守以下模型:
<video>
的物件屬性與方法, 決定如何改變影片狀態.來吧!
首先, 讓點擊影片本身就能觸發播放/暫停. 程式碼如下:
// 存取<video>
const video = player.querySelector('.viewer');
// 切換播放, 暫停
function togglePlay(e) {
video[video.paused ? 'play' : 'pause' ]();
}
// 加上監聽器
video.addEventListener('click', togglePlay);
video
和 audio
都是 HTMLMediaElement
, 比起一般的 HTMLElemnt
, 多出了支援影音處理相關的屬性與方法. 其中呼叫 play()
方法會讓多媒體播放, pause()
方法會讓多媒體暫停播放.
另外 video.paused
屬性代表影片播放的狀態, 若為true
, 表示影片目前為暫停的, 反之為播放狀態.
使用物件方法的方式除了 物件.方法
外還有物件[方法]
一途, 後者的好處是方法可以用變數表示.
上述程式碼用三元運算子 (Ternary Operators) 來決定要操控video
的哪個方法. 意思為, 若為暫停狀態, 則呼叫 video['play']()
,反之則呼叫 video['pause']()
. 以此達到切換的效果.
接著需要讓播放鍵也有相同效果.
const toggle = player.querySelector('.toggle');
toggle.addEventListener('click', togglePlay);
一般播放鍵處在播放或暫停狀態時, 圖示也會改變吧! 因此還需要下列程式碼操控播放鍵圖示.
function updateButton() {
const icon = this.paused ? '►' : '❚ ❚';
toggle.textContent = icon;
}
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
play
和 pause
是影音媒體的事件. 從暫停到播放的瞬間 play
會被觸發. 從播放到暫停的瞬間, pause
會被觸發. 切換播放或暫停一定會觸發這兩個事件, 因此我們只需要監聽這兩個事件來改變圖示就好.
在HTML標籤中, 有兩個具有 data-skip
屬性的 <button>
標籤分別代表向前與向後跳轉固定秒數的跳轉鈕.
要讓跳轉鈕發揮效用, 程式碼如下:
const skipButtons = player.querySelectorAll('[data-skip]');
function skip() {
video.currentTime += parseFloat(this.dataset.skip);
}
skipButtons.forEach(button => button.addEventListener('click', skip));
video
的 currentTime
屬性代表目前的播放時間, 要讓時間跳轉, 只要在按下跳轉鈕時讓currentTime
增加或減少特定的時間就好.
我們將要跳轉的時間值放在data-skip
屬性中, 因此該值的預設型別為字串. 在 Javascript 中, 將字串與數字做運算, 運算結果會以字串相加的形式呈現, 得到值並不會是我們想要的.
parseFloat()
是一個全域函式, 用來將值轉換為浮點數, 若值能夠被轉換為浮點數, 其型別也將自動被強制轉型為 Number
. 利用該特性我們得以將data-skip
的值轉換為數字. 與currentTime
做運算才會得到正確的值.
補充一下 this
指向呼喚 skip()
函式的物件, 如果是向前跳轉鈕觸發點擊, 就會由向前跳轉鈕呼喚該函式, data-skip
的值就會是 "25"
.
兩者的作用原理是相同的, 差在要改變屬性不同而已. 滑動桿是 type
屬性為 range
的<input>
. 其最大與最小值預設為 0 和 100, 實際值 value
預設為中心值, 若要自訂, 可透過 max
, min
, value
屬性調整.
程式碼如下:
const ranges = player.querySelectorAll('.player__slider');
function handleRangeUpdate() {
video[this.name] = this.value;
}
ranges.forEach(range => {
range.addEventListener('input', handleRangeUpdate);
})
我們希望拖曳滑動桿時屬性會跟著改變, 此事可以監聽 input
事件, input
事件好用的地方在於, 只要 value
值一發生改動的瞬間就會觸發, 不像 change
事件必須等到元素失焦後才會觸發.
video
存取音量的屬性名稱為 volume
, 控制播放速度的屬性名稱為 playbackRate
, 和跳轉鈕的寫法相同, 我們利用 this
個別存取該元素的此二屬性並讓其與目前的 value
值相同.
如此一來, 以 volume
屬性為例: 我們拖曳了滑動桿, value
的值變了, input
事件觸發, handleRangeUpdate
執行, <video>
的 volume
屬性值與目前 <input>
的 value
值同步.
注意到進度條的部分, 用了兩層 <div>
來表示, 外層 <div class="progress">
表示進度條的框, 內層 <div class="progress_filled>
代表實際的進度條, 才是我們該存取的元素.
程式碼如下:
const progressBar = player.querySelector('.progress__filled');
function handleProgress() {
const percent = (video.currentTime) / (video.duration) * 100;
progressBar.style.flexBasis = `${percent}%`;
}
video.addEventListener('timeupdate', handleProgress);
duration
屬性代表 video
物件的總時間, 用 currentTime / duration
能夠得到目前進度佔總時間長度的比例, 轉換為百分比後, 指定給 progressBar
的長度就好.
接著只要在每次 currentTime
更新時, 更新這個長度即可. 剛好, timeudpate
事件就是用來監聽 currentTime
更新的瞬間, 如此就完成了!
這裡要實現兩件事. 第一, 點擊進度條上的任何位置, 時間軸會自動更新到該處. 第二, 拖曳進度條也會跟著改變時間軸.
程式碼如下:
const progress = player.querySelector('.progress');
function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}
let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);
程式邏輯在 scrub()
自訂函式中.
首先得確認點擊位置佔總體進度條的百分比.
因為我們只需考慮橫向的座標位置, 這裡用到滑鼠事件的 offsetX
屬性. 當滑鼠在某個元素的內部時, offsetX
記錄著滑鼠在該元素內部的X軸位置. 當滑鼠在該元素的最左邊時, offsetX
值為0, 在最右邊時, offsetX
值為該元素的寬度.
將 offsetX
與進度條外框的寬度相除, 再乘上 duration
, 便能得知該點所代表的影片進度. 接著將 currentTime
更新到該時間即可.
若要讓拖曳的同時也能改變時間軸, 只要在「滑鼠移動」並且「滑鼠右鍵是按下」的情況執行scrub()
函式即可.
這裡我們是針對外層的 <div class="player">
做全屏切換, 如此一來在全屏的情況下, 我們使用的控制器才會是我們自訂的控制器.
程式碼如下:
const fullscreen = player.querySelector('.fullscreen');
function toggleScreen() {
/* I only integrate Chrome version... */
if(!document.webkitFullscreenElement) {
player.webkitRequestFullScreen();
} else {
document.webkitExitFullscreen();
}
}
fullscreen.addEventListener('click', toggleScreen);
全屏切換會用到一個尚未標準化的API, Fullscreen API
. 該API可以讓幾乎各種元素切換為全屏模式. 前提是 API 有支援該種元素.
由於未標準化, 因此各家瀏覽器的使用名稱可能會有所出入. 礙於篇幅, 這裡只示範 Chrome 瀏覽器的版本, 有興趣的可以在文末參考連結看看其他家瀏覽器的實踐方式.
document.fullscreenElement
為唯讀屬性, 若瀏覽器內存在任何正在全屏顯示的元素, 該屬性值為該元素, 否則為 null
.
requestFullscreen()
則可以將呼叫它的元素全屏化. exitFullscreen
只能對document
使用, 是將所有全屏的元素解除全屏.
因此上述程式碼意思為, 如果沒有元素正在全屏, 就把 player
元素全屏化, 否則就關掉全部正在全屏的元素. 以此邏輯來切換全屏. 會這樣設計是因為同時有很多元素在全屏是沒有意義的.
因為 Chrome 瀏覽器支援 webkit
標頭的版本, 因此會看到API方法前面多加了個 webkit
.
大功告成!!
以上為 JS30 第十一篇!
HTMLMediaElement
parseFloat
Fullscreen API