iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 11
0
Modern Web

JS30 錄系列 第 11

Day 11 - Custom Video Player

任務目標

自製一個影片播放器的介面, 內容將包含:

  • 影片進度條: 進度條要能夠顯示影片播放進度, 進度條會自動跳轉到滑鼠點擊位置, 進度條會隨著滑鼠拖曳改變進度位置
  • 播放鍵: 能夠切換播放/暫停, 另外, 點擊影片也會切換播放/暫停
  • 音量滑軌: 拖曳改變音量大小
  • 播放速度滑軌: 拖曳改變播放速度
  • 跳轉鈕: 往前/往後跳轉固定時間的按鈕
  • 全屏鈕: 按下切換全屏

成品像這樣

以下為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>原生的控制器.

接下來看看如何實踐各功能鈕. 播放器介面會接收使用者的操作並進行回饋, 因此
大多遵守以下模型:

  1. 利用DOM存取該元素
  2. 在按鈕上設置監聽器, 若觸發事件, 則以自訂函式回應
  3. 自訂函式透過操作<video>的物件屬性與方法, 決定如何改變影片狀態.

來吧!

播放/暫停

首先, 讓點擊影片本身就能觸發播放/暫停. 程式碼如下:

// 存取<video>
const video = player.querySelector('.viewer');

// 切換播放, 暫停
function togglePlay(e) {
	video[video.paused ? 'play' : 'pause' ]();
}

// 加上監聽器
video.addEventListener('click', togglePlay);

videoaudio 都是 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);

playpause 是影音媒體的事件. 從暫停到播放的瞬間 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));

videocurrentTime 屬性代表目前的播放時間, 要讓時間跳轉, 只要在按下跳轉鈕時讓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 第十一篇!

Reference

HTMLMediaElement
parseFloat
Fullscreen API


上一篇
Day 10 - Hold Shift and Check Checkboxes
下一篇
Day 12 - Key Sequence Detection
系列文
JS30 錄30

1 則留言

0
penguinrun
iT邦新手 5 級 ‧ 2017-12-30 19:55:05

請問怎麼沒有小劇場了? /images/emoticon/emoticon31.gif

Arel iT邦新手 5 級 ‧ 2017-12-30 21:01:05 檢舉

最近風平浪靜, 一夜無話

我要留言

立即登入留言