怎么用 JavaScript 構(gòu)建自定義的 HTML5 視頻播放器
在網(wǎng)頁(yè)中觀看和分享視頻內(nèi)容是一個(gè)很常見(jiàn)的功能,多年來(lái),視頻嵌入網(wǎng)頁(yè)的方式發(fā)生了變化?,F(xiàn)在,我們?cè)诂F(xiàn)代瀏覽器中使用 <video> 標(biāo)簽就可以添加視頻文件到網(wǎng)頁(yè)上,該標(biāo)簽支持多個(gè)視頻格式。
video_element.webp當(dāng)使用 <video> 標(biāo)簽時(shí)的主要警告是渲染的視頻播放器會(huì)因?yàn)g覽器而異,如果你想提供一致的用戶體驗(yàn),使用原生操作并不理想。這就是為什么構(gòu)建自定義控件而不是使用瀏覽器默認(rèn)界面很有用的原因。
在這個(gè)教程中,我將會(huì)帶你使用 JavaScript 構(gòu)建一個(gè)自定義的視頻播放器。目標(biāo)是如何利用瀏覽器 HTML5 Media API 來(lái)提升默認(rèn)設(shè)置的體驗(yàn)。
我們將在本教程中構(gòu)建一個(gè)看起來(lái)像 YouTube 視頻播放器,因?yàn)槲艺J(rèn)為復(fù)制大多數(shù)人已經(jīng)熟悉的一些功能是個(gè)好主意。
當(dāng)然,我們并不會(huì)實(shí)現(xiàn) YouTube 播放器上的所有功能,因?yàn)檫@會(huì)讓教程更長(zhǎng)、更復(fù)雜。然而,一旦你完成了本教程,我相信你能夠很輕松地加入新的功能。
你可以查看我們將構(gòu)建的線上案例,或者在 GitHub 上查看源碼。
準(zhǔn)備條件
你需要對(duì) JavaScript 和 DOM 有基本的了解,才能繼續(xù)學(xué)習(xí)本教程。我推薦你使用最新版本的谷歌瀏覽器,因?yàn)樵诒疚木帉?xiě)時(shí),我們將添加的一些功能(比如畫(huà)中畫(huà)功能)僅適用于谷歌(Webkit 內(nèi)核)瀏覽器。
開(kāi)始
我在 GitHub 中為本教程準(zhǔn)備了開(kāi)始文件。有需要的話,你可以克隆到自己的機(jī)器上,并在編輯器中打開(kāi)。你將分別在 index.html 和 style.css 中找到播放器的標(biāo)記文檔文件及其樣式,以及我們用來(lái)測(cè)試播放器的視頻文件。index.js 將是我們添加播放器工作所需的所有 JavaScript 代碼的地方。
在終端中運(yùn)行 npm install 來(lái)安裝 browser-sync 作為啟動(dòng) Web 服務(wù)器的開(kāi)發(fā)依賴(lài)項(xiàng),其在任何文件更改時(shí)自動(dòng)刷新頁(yè)面。然后 npm start 啟動(dòng)項(xiàng)目,監(jiān)聽(tīng)瀏覽器 http://localhost:3000。
目前都做了些什么
現(xiàn)在,視頻播放器保留本機(jī)瀏覽器控件,正如你所期待那樣工作。自定義控件已經(jīng)被定義在 #video-controls 元素,但是它們被隱藏了。
<!-- index.html -->
. . .
<div class="video-controls hidden" id="video-controls">
<!-- Custom controls are defined here -->
</div>
. . .
即使我們要為控件實(shí)現(xiàn)自定義界面,保留 <video> 元素上的 controls 屬性是個(gè)很好的主意,這樣用戶不管出于什么原因禁用 JavaScript,瀏覽器本機(jī)的控件依舊可使用。對(duì)于其他人,本機(jī)空間可以輕松隱藏并替換成自定義控件,這稍后進(jìn)行演示。
海報(bào)圖像已經(jīng)添加到視頻中,設(shè)置 preload屬性值為 metadata,這指示瀏覽器僅獲取視頻元數(shù)據(jù)(比如 duration)。為了讓事情簡(jiǎn)單點(diǎn),我們只添加 MP4 類(lèi)型的視頻源文件,因?yàn)樵擃?lèi)型的視頻被所有主流瀏覽器兼容,是一個(gè)非常安全的默認(rèn)值。有關(guān)視頻格式和瀏覽器兼容性的更多信息,可參考該文檔。
<!-- index.html -->
. . .
<video controls class="video" id="video" preload="metadata" poster="poster.jpg">
<source src="video.mp4" type="video/mp4"></source>
</video>
. . .
隱藏自帶控件
我們首先需要做的事情是在確認(rèn)瀏覽器支持 HTML5 視頻后,隱藏默認(rèn)視頻控件并提供我們自己的界面。在你的 index.js 文件中輸入下面代碼片段來(lái)實(shí)現(xiàn)上面的功能:
// index.js
// Select elements here
const video = document.getElementById('video');
const videoControls = document.getElementById('video-controls');
const videoWorks = !!document.createElement('video').canPlayType;
if (videoWorks) {
video.controls = false;
videoControls.classList.remove('hidden');
}
canPlayType 屬性是我們檢查瀏覽器對(duì)視頻格式支持的方式。要使用它,我們需要?jiǎng)?chuàng)建 <video> 元素的實(shí)例并將檢查是否支持 canPlayType。如果支持,則可以安全地假設(shè)其支持 HTML 視頻,然后禁用默認(rèn)控件,啟用我們自定義的控件。
canPlayType_support.webp默認(rèn)控件已經(jīng)被替換成自定義控件
切換播放狀態(tài)
讓我們從基礎(chǔ)開(kāi)始。我們需要通過(guò)點(diǎn)擊播放按鈕來(lái)播放或者暫停視頻,并且更改應(yīng)該匹配視頻狀態(tài)的圖標(biāo)。我們從獲取視頻和播放按鈕開(kāi)始,代碼在 index.js 頂部,如下:
// index.js
const playButton = document.getElementById('play');
然后,我們創(chuàng)建一個(gè)函數(shù)來(lái)切換視頻播放狀態(tài):
// index.js
// Add functions here
// togglePlay toggles the playback state of the video.
// If the video playback is paused or ended, the video is played
// 如果視頻播放是暫?;蛘咭曨l結(jié)尾狀態(tài),視頻播放
// otherwise, the video is paused
// 否則,視頻暫停
function togglePlay() {
if (video.paused || video.ended) {
video.play();
} else {
video.pause();
}
}
最后,我們創(chuàng)建一個(gè)時(shí)間監(jiān)聽(tīng)器,當(dāng) playButton 按鈕被點(diǎn)擊后執(zhí)行 togglePlay 方法。
// index.js
// Add eventlisteners here
playButton.addEventListener('click', togglePlay);
夠簡(jiǎn)單吧?通過(guò)點(diǎn)擊瀏覽器中的播放按鈕對(duì)其測(cè)試。它應(yīng)該正確地播放和暫停視頻。
appropriately_play_and_stop_video.webm.gif這實(shí)際上為本教程的其他部分定下了基調(diào)。我們通常會(huì)選擇一個(gè)視頻控件,創(chuàng)建一個(gè)實(shí)現(xiàn)特定功能的函數(shù),通過(guò)事件監(jiān)聽(tīng)器將其連接起來(lái)。
我們繼續(xù),根據(jù)視頻狀態(tài)更新播放按鈕。下面是 playButton 的 HTML 文件:
<!-- index.html -->
. . .
<button data-title="Play (k)" id="play">
<svg class="playback-icons">
<use href="#play-icon"></use>
<use class="hidden" href="#pause"></use>
</svg>
</button>
. . .
在 <svg> 元素中,我們有播放和暫停按鈕,但是一次我們只能展示其中一個(gè),另一個(gè)則隱藏?,F(xiàn)在我們要做的就是切換每個(gè)圖標(biāo)的 hidden 類(lèi),以便根據(jù)視頻的狀態(tài)展示正確的圖標(biāo)。
首先,在 index.js 文件頂部選擇圖標(biāo):
// index.js
const playbackIcons = document.querySelectorAll('.playback-icons use');
接著,在 togglePlay 函數(shù)下創(chuàng)建一個(gè)函數(shù),用來(lái)更新播放按鈕:
// index.js
// updatePlayButton updates the playback icon and tooltip
// depending on the playback state
// 根據(jù)播放狀態(tài),updatePlayButton 函數(shù)更新播放圖標(biāo)和提示
function updatePlayButton() {
playbackIcons.forEach(icon => icon.classList.toggle('hidden'));
}
最后,在文件底部添加如下事件監(jiān)聽(tīng)器:
// index.js
video.addEventListener('play', updatePlayButton);
video.addEventListener('pause', updatePlayButton);
當(dāng)視頻播放或者暫停時(shí),updatePlayButton 函數(shù)都會(huì)被執(zhí)行,切換每個(gè)按鈕中的 hidden 類(lèi)。因?yàn)闀和0粹o元素默認(rèn)值是 hidden 類(lèi),一旦視頻被播放,這個(gè)暫停圖標(biāo)出現(xiàn),播放圖標(biāo)將會(huì)隱藏。如果視頻被暫停,則會(huì)發(fā)生相反的情況。你可以在自己瀏覽器上測(cè)試。
額外要做的事情是,當(dāng)鼠標(biāo)移動(dòng)到播放按鈕上,需要更新展示的提示文本。默認(rèn)提示是 play(k),但是當(dāng)視頻正在播放,需要更新提示信息為 pause(k)。k 是我們將在本教程后面添加播放或者暫停視頻的鍵盤(pán)快捷鍵。
如下,更新 updatePlayButton 函數(shù):
// index.js
function updatePlayButton() {
playbackIcons.forEach(icon => icon.classList.toggle('hidden'));
if (video.paused) {
playButton.setAttribute('data-title', 'Play (k)')
} else {
playButton.setAttribute('data-title', 'Pause (k)')
}
}
當(dāng)視頻正在播放或者暫停時(shí),鼠標(biāo)移動(dòng)到按鈕上,應(yīng)該設(shè)置正確的提示文本。
如果你想知道提示信息是怎么展示的,可以看下相關(guān)的 CSS:
// style.css
. . .
button::before {
content: attr(data-title);
position: absolute;
display: none;
right: 0;
top: -50px;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
font-weight: bold;
padding: 4px 6px;
word-break: keep-all;
white-space: pre;
}
button:hover::before {
display: inline-block;
}
. . .
video-tooltip.gif展示視頻持續(xù)時(shí)間和經(jīng)過(guò)時(shí)間
展示視頻時(shí)長(zhǎng)很必要,因?yàn)檫@是用戶首先想看到的,所以我們接下來(lái)將講解。
下面是持續(xù)時(shí)長(zhǎng)和經(jīng)過(guò)時(shí)間的元素標(biāo)記:
<!-- index.html -->
<div class="time">
<time id="time-elapsed">00:00</time>
<span> / </span>
<time id="duration">00:00</time>
</div>
通過(guò) index.js 選擇這兩個(gè)控件(元素),如下:
// index.js
const timeElapsed = document.getElementById('time-elapsed');
const duration = document.getElementById('duration');
一旦頁(yè)面加載完成后,我們將使用 duration 屬性展示視頻的總時(shí)長(zhǎng)。這個(gè)屬性表示的是視頻的總秒數(shù),所以在展示之前,我們需要將其轉(zhuǎn)換成分秒。我們創(chuàng)建一個(gè) formatTime 函數(shù),將時(shí)間轉(zhuǎn)換成分秒:
// index.js
// formatTime takes a time length in seconds and returns the time in
// minutes and seconds
function formatTime(timeInSeconds) {
const result = new Date(timeInSeconds * 1000).toISOString().substr(11, 8);
return {
minutes: result.substr(3, 2),
seconds: result.substr(6, 2),
};
};
接著,我們?cè)?formatTime 函數(shù)下創(chuàng)建 initializeVideo 函數(shù):
// index.js
// initializeVideo sets the video duration, and maximum value of the
// progressBar
function initializeVideo() {
const videoDuration = Math.round(video.duration);
const time = formatTime(videoDuration);
duration.innerText = `${time.minutes}:${time.seconds}`;
duration.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`)
}
如上所示,視頻持續(xù)時(shí)長(zhǎng)被四舍五入,格式化為分秒,然后在屏幕上更新。datetime 同步更新為時(shí)間字符串,表示視頻持續(xù)時(shí)長(zhǎng)。
接著,如下所示,讓我們將 initializeVideo 函數(shù)連接到 loadedmetadata 監(jiān)聽(tīng)器上。當(dāng)元數(shù)據(jù)被加載之后,將會(huì)更新視頻的持續(xù)時(shí)長(zhǎng)。
// index.js
video.addEventListener('loadedmetadata', initializeVideo);
video-duration.webp同理,當(dāng)視頻播放過(guò)程中,我們更新播放經(jīng)過(guò)的時(shí)間。下面的函數(shù)能幫我們實(shí)現(xiàn)這個(gè)功能:
// index.js
// updateTimeElapsed indicates how far through the video
// the current playback is
function updateTimeElapsed() {
const time = formatTime(Math.round(video.currentTime));
timeElapsed.innerText = `${time.minutes}:${time.seconds}`;
timeElapsed.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`)
}
我們需要 timeupdate 事件監(jiān)聽(tīng)視頻。無(wú)論什么時(shí)候,視頻的 currentTime 屬性值更新了,事件就會(huì)觸發(fā)。
// index.js
video.addEventListener('timeupdate', updateTimeElapsed);
上面的代碼確保視頻的 currentTime 更新,經(jīng)過(guò)時(shí)間也會(huì)適當(dāng)更新。
current-time-update.gif更新進(jìn)度條
接下來(lái)我們要做的事情是當(dāng)視頻播放,更新進(jìn)度條。下面是進(jìn)度條的元素標(biāo)志:
<!-- index.html -->
. . .
<div class="video-progress">
<progress id="progress-bar" value="0" min="0"></progress>
<input class="seek" id="seek" value="0" min="0" type="range" step="1">
<div class="seek-tooltip" id="seek-tooltip">00:00</div>
</div>
. . .
上面,我們有 progress 元素,用于顯示任務(wù)的進(jìn)度條,而 range 類(lèi)型的 input 允許我們快速無(wú)縫瀏覽視頻。兩個(gè)元素我都用同個(gè)樣式修飾,所以它們有一樣的寬高,但是 input 是透明色(除了與進(jìn)度條內(nèi)相同的顏色的指示點(diǎn))。
如果你很好奇,你可以仔細(xì)看 CSS 的內(nèi)容,看看我是怎么做的。讓進(jìn)度條看起來(lái)像一個(gè)單一的元素是一種 hack,但是我覺(jué)得對(duì)我們的用例來(lái)說(shuō)很合理。
兩者的 min 屬性被設(shè)置為 0,兩者的 value 屬性指向當(dāng)前時(shí)間值。它們還需要一個(gè) max 屬性,該屬性將設(shè)置為視頻的持續(xù)時(shí)間(以秒為單位),該屬性值來(lái)自 video.duration,如上所示。我們可以在 initializeVideo 函數(shù)中實(shí)現(xiàn),但是我們得先選擇元素:
// index.js
const progressBar = document.getElementById('progress-bar');
const seek = document.getElementById('seek');
然后如下更新 initializeVideo 函數(shù):
// index.js
function initializeVideo() {
const videoDuration = Math.round(video.duration);
seek.setAttribute('max', videoDuration);
progressBar.setAttribute('max', videoDuration);
const time = formatTime(videoDuration);
duration.innerText = `${time.minutes}:${time.seconds}`;
duration.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`)
}
現(xiàn)在,進(jìn)度條元素的范圍輸入在 0 和以秒為單位的視頻持續(xù)時(shí)長(zhǎng)之間,如屬性 min 和 max 屬性。正如你將看到的,這使得我們能夠在任何時(shí)間點(diǎn)輕松地將進(jìn)度條和時(shí)間范圍同步。
繼續(xù),當(dāng)視頻被播放我們就更新上述元素的值,以便進(jìn)度條發(fā)揮作用。如下,創(chuàng)建 updateProgress 函數(shù):
// index.js
// updateProgress indicates how far through the video
// the current playback is by updating the progress bar
function updateProgress() {
seek.value = Math.floor(video.currentTime);
progressBar.value = Math.floor(video.currentTime);
}
然后,在第一個(gè)事件監(jiān)聽(tīng)器下,為 video 添加一個(gè)新的名為 timeupdate 事件監(jiān)聽(tīng)器:
// index.js
video.addEventListener('timeupdate', updateProgress);
刷新你的瀏覽器,然后嘗試。當(dāng)視頻被播放,你應(yīng)該看到進(jìn)度條更新。
progress-bar-update.gif預(yù)先跳轉(zhuǎn)
大多數(shù)的播放器都允許你點(diǎn)擊進(jìn)度條跳轉(zhuǎn)到視頻指定的點(diǎn),我們的視頻播放器也將一樣。首先,我們需要選擇提示信息元素:
// index.js
const seekTooltip = document.getElementById('seek-tooltip');
然后,添加一個(gè)函數(shù),用來(lái)當(dāng)光標(biāo)移動(dòng)到進(jìn)度條上在信息元素里展示時(shí)間戳:
// index.js
// updateSeekTooltip uses the position of the mouse on the progress bar to
// roughly work out what point in the video the user will skip to if
// the progress bar is clicked at that point
function updateSeekTooltip(event) {
const skipTo = Math.round((event.offsetX / event.target.clientWidth) * parseInt(event.target.getAttribute('max'), 10));
seek.setAttribute('data-seek', skipTo)
const t = formatTime(skipTo);
seekTooltip.textContent = `${t.minutes}:${t.seconds}`;
const rect = video.getBoundingClientRect();
seekTooltip.style.left = `${event.pageX - rect.left}px`;
}
此函數(shù)在 seek 元素中,使用光標(biāo)位置粗略計(jì)算用戶懸停范圍輸入框的地方,然后將位置信息存放在 data-seek 屬性中,同時(shí)更新提示信息以反映該位置的時(shí)間戳。
在 seek 控制器中關(guān)聯(lián) updateSeekTooltip 函數(shù)和 mousemove 來(lái)查看效果:
// index.js
seek.addEventListener('mousemove', updateSeekTooltip);
mousemove-tooltip.gif不管是點(diǎn)擊或者拖拽指示點(diǎn),一旦 seek 元素值發(fā)生更改,我們希望跳轉(zhuǎn)到 data-seek 屬性設(shè)置的時(shí)間點(diǎn)。
在 updateSeekTooltip 函數(shù)下,創(chuàng)建一個(gè)新的名為 skipAhead 的函數(shù):
// index.js
// skipAhead jumps to a different point in the video when
// the progress bar is clicked
function skipAhead(event) {
const skipTo = event.target.dataset.seek ? event.target.dataset.seek : event.target.value;
video.currentTime = skipTo;
progressBar.value = skipTo;
seek.value = skipTo;
}
使用 input 事件監(jiān)控 seek 元素發(fā)生更改時(shí),將執(zhí)行此函數(shù)。然后,我們獲取 data-seek 的值并檢查其是否有效。如果有效,我們獲取該值并更新視頻播放過(guò)的時(shí)間和進(jìn)度條的位置。如果 data-seek 屬性不存在(比如在手機(jī)端),改為使用 seek 元素的值。
這產(chǎn)生跳轉(zhuǎn)到視頻指定位置的效果。
// index.js
seek.addEventListener('input', skipAhead);
jump-ahead-demo.gif音頻控制
<!-- index.html -->
. . .
<div class="volume-controls">
<button data-title="Mute (m)" class="volume-button" id="volume-button">
<svg>
<use class="hidden" href="#volume-mute"></use>
<use class="hidden" href="#volume-low"></use>
<use href="#volume-high"></use>
</svg>
</button>
<input class="volume" id="volume" value="1" type="range" max="1" min="0" step="0.01">
</div>
. . .
在上面代碼片段中,你可以找到所有相關(guān)音頻控件的標(biāo)記。我們有一個(gè)按鈕,根據(jù)視頻音頻的狀態(tài)展示,和一個(gè)控制音頻范圍的 input 元素。
首先,當(dāng) #volume 元素的值發(fā)生更改,我們要做的就是更改視頻的音頻大小。我們也要更新視頻當(dāng)前的圖標(biāo)。
正如你所見(jiàn),音頻的輸入范圍是 0 到 1,并以 0.01 的值遞增。以這種方式設(shè)置它是為了使其與視頻的音量屬性值保持一致,該屬性值的范圍也是從 0 到 1,其中 0 是最低音量,1 是最高音量。
繼續(xù),我們選擇按鈕,圖標(biāo)和輸入框,如下 index.js 所示:
// index.js
const volumeButton = document.getElementById('volume-button');
const volumeIcons = document.querySelectorAll('.volume-button use');
const volumeMute = document.querySelector('use[href="#volume-mute"]');
const volumeLow = document.querySelector('use[href="#volume-low"]');
const volumeHigh = document.querySelector('use[href="#volume-high"]');
const volume = document.getElementById('volume');
接著,創(chuàng)建一個(gè)新的名為 updateVolume 函數(shù),當(dāng)音頻輸入框值發(fā)生更改,該函數(shù)更新視頻音頻值:
// index.js
// updateVolume updates the video's volume
// and disables the muted state if active
function updateVolume() {
if (video.muted) {
video.muted = false;
}
video.volume = volume.value;
}
然后,將其和 volume 元素關(guān)聯(lián)起來(lái),如下:
// index.js
volume.addEventListener('input', updateVolume);
到這里,你將意識(shí)到當(dāng)你左滑輸入框時(shí),音量減少,反之音量增加。我們需要添加另一個(gè)函數(shù)來(lái)在音量變化時(shí)更新圖標(biāo):
// index.js
// updateVolumeIcon updates the volume icon so that it correctly reflects
// the volume of the video
function updateVolumeIcon() {
volumeIcons.forEach(icon => {
icon.classList.add('hidden');
});
volumeButton.setAttribute('data-title', 'Mute (m)')
if (video.muted || video.volume === 0) {
volumeMute.classList.remove('hidden');
volumeButton.setAttribute('data-title', 'Unmute (m)')
} else if (video.volume > 0 && video.volume <= 0.5) {
volumeLow.classList.remove('hidden');
} else {
volumeHigh.classList.remove('hidden');
}
}
當(dāng)這函數(shù)執(zhí)行,所有的圖標(biāo)都會(huì)隱藏,然后會(huì)根據(jù)條件顯示其中一個(gè)圖標(biāo)。
我們可以通過(guò)監(jiān)聽(tīng)視頻 volumechange 事件,在每次音量發(fā)生變化時(shí)運(yùn)行 updateVolumeIcon 函數(shù),如下:
// index.js
video.addEventListener('volumechange', updateVolumeIcon);
添加上面的更改后,在你瀏覽上你可以看到下面的效果:
volume-change.gif我們需要添加的另一個(gè)事件是能夠通過(guò)單擊音量圖標(biāo)使得視頻靜音和取消靜音。我們將創(chuàng)建一個(gè)名為 toggleMute 函數(shù):
// index.js
// toggleMute mutes or unmutes the video when executed
// When the video is unmuted, the volume is returned to the value
// it was set to before the video was muted
function toggleMute() {
video.muted = !video.muted;
if (video.muted) {
volume.setAttribute('data-volume', volume.value);
volume.value = 0;
} else {
volume.value = volume.dataset.volume;
}
}
當(dāng) volumeButton 被點(diǎn)擊后運(yùn)行該函數(shù):
// index.js
volumeButton.addEventListener('click', toggleMute);
該函數(shù)切換視頻 muted 屬性的狀態(tài)為真或者假。當(dāng)視頻被靜音,音頻值就會(huì)存放在 volume 元素 data-volume 屬性上,以便當(dāng)視頻取消靜音時(shí),我們可以恢復(fù)音頻狀態(tài)之前的值。
這里是實(shí)操效果:
volume-muted-unmuted.gif點(diǎn)擊視頻播放或者暫停
在很多視頻播放器應(yīng)用中,點(diǎn)擊視頻本身能夠快速進(jìn)行播放或者暫停,所以,在我們的播放器中也實(shí)現(xiàn)它。
我們要做的就是監(jiān)聽(tīng) video 上的 click 事件,當(dāng)事件觸發(fā)就運(yùn)行 togglePlay 函數(shù):
// index.js
video.addEventListener('click', togglePlay);
雖然這可行,但是讓我們通過(guò)在播放或者暫停視頻時(shí)添加一些反饋?zhàn)屵@更有趣,就像 YouTube 或者 Netflix 上一樣。
這是我們動(dòng)畫(huà)的 HTML:
<!-- index.html -->
. . .
<div class="playback-animation" id="playback-animation">
<svg class="playback-icons">
<use class="hidden" href="#play-icon"></use>
<use href="#pause"></use>
</svg>
</div>
. . .
下面是相關(guān)的 CSS:
// style.css
.playback-animation {
pointer-events: none;
position: absolute;
top: 50%;
left: 50%;
margin-left: -40px;
margin-top: -40px;
width: 80px;
height: 80px;
border-radius: 80px;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
}
.playback-animation 元素通過(guò) opacity 屬性,設(shè)置默認(rèn)值是透明色。為了復(fù)制 YouTube 中的動(dòng)效,我們將會(huì)使用 Web Animations API 來(lái)實(shí)現(xiàn)該元素透明度和縮放效果。
在 index.js 文件頂部先選中該元素:
// index.js
const playbackAnimation = document.getElementById('playback-animation');
然后在其他函數(shù)下創(chuàng)建下面的函數(shù):
// index.js
// animatePlayback displays an animation when
// the video is played or paused
function animatePlayback() {
playbackAnimation.animate([
{
opacity: 1,
transform: "scale(1)",
},
{
opacity: 0,
transform: "scale(1.3)",
}], {
duration: 500,
});
}
animate 函數(shù)接受一個(gè)關(guān)鍵幀對(duì)象數(shù)組和一個(gè)控制動(dòng)畫(huà)時(shí)間等的可選對(duì)象。
現(xiàn)在,為 video 元素添加第二個(gè) click 事件:
// index.js
video.addEventListener('click', animatePlayback);
現(xiàn)在當(dāng)你點(diǎn)擊播放或者暫停視頻,可以看到簡(jiǎn)短的動(dòng)畫(huà)效果。
play-pause-video.gif視頻全屏
接下來(lái),我們實(shí)現(xiàn)全屏功能按鈕。為了讓視頻全屏(包括控制器),我們需要選擇 .video-container 元素,然后詢問(wèn)瀏覽器去全屏放置它(及其子元素)。
在 index.js 文件中選擇按鈕和視頻容器:
// index.js
const fullscreenButton = document.getElementById('fullscreen-button');
const videoContainer = document.getElementById('video-container');
然后創(chuàng)建一個(gè)新的名為 toggleFullScreen 函數(shù):
// index.js
// toggleFullScreen toggles the full screen state of the video
// If the browser is currently in fullscreen mode,
// then it should exit and vice versa.
function toggleFullScreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (document.webkitFullscreenElement) {
// Need this to support Safari
document.webkitExitFullscreen();
} else if (videoContainer.webkitRequestFullscreen) {
// Need this to support Safari
videoContainer.webkitRequestFullscreen();
} else {
videoContainer.requestFullscreen();
}
}
然后,為 fullScreenButton 元素添加一個(gè) click 事件,如下:
// index.js
fullscreenButton.onclick = toggleFullScreen;
toggleFullScreen 函數(shù)會(huì)先檢查 document 是否是全屏模式,如果是則退出到瀏覽器模式。否則,則將 videoContainer 元素放置在全屏。
在該章節(jié),我們還要做的是當(dāng)鼠標(biāo)懸停在按鈕上更新全屏圖片和提示文本。首先,選擇圖標(biāo):
// index.js
const fullscreenIcons = fullscreenButton.querySelectorAll('use');
然后創(chuàng)建一個(gè)函數(shù),當(dāng) videoContainer 進(jìn)行全屏或者退出全屏模式時(shí)候更新按鈕:
// index.js
// updateFullscreenButton changes the icon of the full screen button
// and tooltip to reflect the current full screen state of the video
function updateFullscreenButton() {
fullscreenIcons.forEach(icon => icon.classList.toggle('hidden'));
if (document.fullscreenElement) {
fullscreenButton.setAttribute('data-title', 'Exit full screen (f)')
} else {
fullscreenButton.setAttribute('data-title', 'Full screen (f)')
}
}
最后,為 videoContainer 元素分配 updateFullscreenButton 函數(shù)到 onfullscreenchange 事件處理器:
// index.js
videoContainer.addEventListener('fullscreenchange', updateFullscreenButton);
嗯,它按預(yù)期工作!你可以在自己瀏覽器上測(cè)試或者看下面的 GIF 圖。
full-screen-video.gif添加畫(huà)中畫(huà)支持
Picture-in-Picture(PiP) API 允許用戶在浮動(dòng)窗口(其中位于其他窗口之上) 中觀看視頻,這樣他們就可以在觀看視頻的同時(shí)將注意力放在其他站點(diǎn)或者應(yīng)用上。
到目前為止,這個(gè) API 只被少數(shù)瀏覽器支持,所以我們需要對(duì)不支持的瀏覽器隱藏該 PiP 按鈕,以便他們看不到使用不了的功能。
picture-in-picture-ability.webp請(qǐng)參考 caniuse.com 獲取最新的表格信息。
下面的代碼能幫我們實(shí)現(xiàn)該功能。在其他事件監(jiān)聽(tīng)器下添加此代碼。
// index.js
document.addEventListener('DOMContentLoaded', () => {
if (!('pictureInPictureEnabled' in document)) {
pipButton.classList.add('hidden');
}
});
正如本教程我們要做的那樣,我們先需要選中相關(guān)的控制器:
// index.js
const pipButton = document.getElementById('pip-button')
然后創(chuàng)建切換 Picture-in-Picture 模式的函數(shù):
// index.js
// togglePip toggles Picture-in-Picture mode on the video
async function togglePip() {
try {
if (video !== document.pictureInPictureElement) {
pipButton.disabled = true;
await video.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
} catch (error) {
console.error(error)
} finally {
pipButton.disabled = false;
}
}
我創(chuàng)建了一個(gè)名為 togglePip 的異步函數(shù),以便我們可以在 requestPictureInPicture() 方法拒絕時(shí)捕獲到錯(cuò)誤,這可能由于多種原因?qū)е?。在真?shí)的應(yīng)用中,你可能想向用戶展示錯(cuò)誤信息,而不是打印到控制臺(tái)上。
接著,在 pipButton 元素上添加 click 事件,然后添加 togglePip 函數(shù)到該事件處理器中。
// index.js
pipButton.addEventListener('click', togglePip);
現(xiàn)在,添加 pipButton 應(yīng)該進(jìn)入或者退出畫(huà)中畫(huà)模式。你也可以通過(guò)點(diǎn)擊(畫(huà)中畫(huà)模式)右上角的關(guān)閉按鈕關(guān)閉 PiP 窗口。
PiP-mode.gif切換視頻控件
視頻控件會(huì)占用一些空間并阻擋用戶查看一些內(nèi)容。當(dāng)它們不被使用的時(shí)候?qū)⑵潆[藏起來(lái)比較好,然后當(dāng)鼠標(biāo)移動(dòng)到視頻上方再顯示它們。
為了實(shí)現(xiàn)這個(gè)目標(biāo),我們編寫(xiě)兩個(gè)函數(shù),如下:
// index.js
// hideControls hides the video controls when not in use
// if the video is paused, the controls must remain visible
function hideControls() {
if (video.paused) {
return;
}
videoControls.classList.add('hide');
}
// showControls displays the video controls
function showControls() {
videoControls.classList.remove('hide');
}
這里我們想做的就是,當(dāng)鼠標(biāo)離開(kāi)視頻上方就隱藏控件。但是當(dāng)視頻停止播放的時(shí)候,我們確保控件總是展示的,所以在 hideControls() 函數(shù)中添加條件判斷。
為了實(shí)現(xiàn)這個(gè),我們將在 video 元素和 videoControls 元素上使用 onmouseenter 和 onmouseleave 事件處理器,如下:
// index.js
video.addEventListener('mouseenter', showControls);
video.addEventListener('mouseleave', hideControls);
videoControls.addEventListener('mouseenter', showControls);
videoControls.addEventListener('mouseleave', hideControls);
添加鍵盤(pán)快捷鍵
我們將添加到播放器的最后一個(gè)特性是使用快捷鍵控制視頻播放。實(shí)際上,就是當(dāng)我們按下特定的鍵時(shí),運(yùn)行我們指定函數(shù)的事情。我們將實(shí)現(xiàn)的快捷鍵如下:
-
k:播放或者暫停視頻 -
m:視頻靜音或者取消靜音 -
f:切換全屏 -
p:切換畫(huà)中畫(huà)模式
這里我們要做的就是監(jiān)聽(tīng) document 中 keyup 事件,檢測(cè)按下的快捷鍵并返回相關(guān)的函數(shù)。
// index.js
// keyboardShortcuts executes the relevant functions for
// each supported shortcut key
function keyboardShortcuts(event) {
const { key } = event;
switch(key) {
case 'k':
togglePlay();
animatePlayback();
if (video.paused) {
showControls();
} else {
setTimeout(() => {
hideControls();
}, 2000);
}
break;
case 'm':
toggleMute();
break;
case 'f':
toggleFullScreen();
break;
case 'p':
togglePip();
break;
}
}
如上,一個(gè) switch 聲明被用來(lái)檢測(cè)哪個(gè)快捷鍵被按下,然后執(zhí)行相關(guān)的代碼。兩秒后調(diào)用 hideControl 函數(shù)的原因是模仿 YouTube 上的行為,當(dāng)使用快捷鍵播放視頻時(shí)候,控件不會(huì)立馬消失,而是有一個(gè)短暫的延時(shí)。
// index.js
document.addEventListener('keyup', keyboardShortcuts);
總結(jié)
改進(jìn)視頻播放器的方法還有很多,但是本教程篇幅已經(jīng)很長(zhǎng)了,所以我不得不在這里停下來(lái)。如果你對(duì)額外的功能感興趣,下面是些想法:
- 添加對(duì)字幕的支持
- 添加對(duì)播放速度的支持
- 添加快速前進(jìn)或者倒放視頻的功能
-
添加選擇視頻分辨率(
720p, 480p, 360p, 240p)的功能
我希望本教程對(duì)你有幫助。相關(guān)代碼 GitHub。
Thanks for reading, and happy coding!
參考
原文地址 - https://freshman.tech/custom-html5-video/
