概論
本文將研究並驗證,在 Video Player 實現:當鼠標在進度條(timeline)移動時,隨著鼠標指向的時間區段,顯示對應的預覽縮圖。
研究
FFmpeg
FFmpeg 是一種開源、跨平台的錄影、轉檔和串流方案,提供了一套完整、強大且靈活的工具和庫,用於處理各種多媒體資料。它主要包括三個關鍵部分:libavcodec(音頻/影片編碼/解碼庫)、libavformat(音頻/影片封裝庫)和 ffmpeg 命令行工具。
利用 FFmpeg,使用者可以輕鬆地對視訊進行操作,如更改規模、轉換格式、裁剪、添加過濾器等等。此外,其豐富的API集合使開發者可以根據自己的需求進行客製化開發。
本研究將利用 FFmpeg,依照來源影片之 timeline,進行截圖。
Montage
Montage 是一款由 ImageMagick 開發的工具,用於將多個圖片合併成一個或多個圖片。此工具可以根據需求定義輸入圖像的佈局,調整邊框和間距,以及添加標籤等等。
本研究將利用 Montage,將複數截圖合併為一張等高的 timeline 預覽圖。
Video Player Progress Control
影片播放器進度控制是影片播放過程中一個重要的特性,它不僅讓使用者能直觀地了解當前播放進度,也能讓使用者自由地在影片中跳躍至指定的播放點。此功能的實現需要在播放器內部進行精確的時間與播放位置的映射,並且在使用者進行操作時提供流暢且快速的反應。
本研究將會嘗試在 Video Player 上,基於監聽 Progress Control 實現預覽圖功能。
Dplayer integrate timeline preview
Dplayer 已經提供了這個功能的接口,因此只需要將縮圖網址跟影片原址一起給 Player 便可使用。
要注意的事,Dplayer 要求縮圖為 160 寬度,並且會檢查此縮圖是否為當前影片所生,其檢查機制暫且不明。
Videojs integrate timeline preview
Videojs 並未提供該功能,不過我們可以透過對 Progress Control 的元件添加監聽器事件,透過 css 來顯示對應縮圖。
流程設計
實作及驗證
Thumbnails
使用以下指令建立 FFmpeg Docker Container:
1docker run -it --name app_ffmpeg -p 8080:8080 -v ~/Downloads/ffmpeg:/storage -w /storage --entrypoint='bash' jrottenberg/ffmpeg
以下在 Docker Container操作
首先擷取縮圖
由於 docker 環境並沒有預裝 bc,所以先安裝它:
1sudo apt install bc
將影片放置在 ~/Downloads/ffmpeg
,建立腳本檔 run.sh
包含以下內容:
1#!/bin/bash2
3input="swyg.mp4"4
5## Get duration in seconds6duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $input)7
8## Calculate interval9interval=$(echo "$duration/100" | bc -l)10
11for i in {0..99}; do12 ss=$(echo "$interval*$i/1" | bc)13 ss=$(printf "%02d:%02d:%02d" $((ss/3600)) $((ss%3600/60)) $((ss%60)))14 ffmpeg -ss $ss -i $input -vframes 1 -vf scale=160:-1 screenshot$(printf "%03d" $i).png15done
執行 run.sh
,ffmpeg 會依照 timeline 長度,均勻的在 100 個時間點擷取縮圖,並依照 screenshot*.png
格式命名。
接著,需要把 100 張縮圖整合成一個超長的縮圖
安裝 imagemagick
:
1sudo apt-get update2sudo apt-get install imagemagick
因為 docker 並沒有安裝字型檔,在執行 montage 會有問題,所以需要在 docker 安裝設定字體。
執行以下指令:
1sudo apt-get install fontconfig2sudo dpkg-reconfigure fontconfig3export FONTCONFIG_PATH=/etc/fonts
使用 montage 合併圖片:
1montage -geometry +0+0 -tile x1 screenshot*.png timeline.png2
3###4## -geometry +0+0 選項表示圖像之間的間距為 0。5## -tile x1 選項表示所有的圖像都排列在一行。6## screenshot*.png 是你的縮圖文件名模式。7## timeline.png 是輸出的圖片文件名。8###
到這裡,我們得到了最後的縮圖檔 timeline.png
。
Docker Container 內部操作結束
應用於 Dplayer
1<!doctype html>2<html lang="en">3
4<head>5 <meta charset="UTF-8">6 <meta name="viewport"7 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">8 <meta http-equiv="X-UA-Compatible" content="ie=edge">9 <script src="https://jsdelivr.fusioncdn.com/npm/dplayer@1.26.0/dist/DPlayer.min.js"></script>10 <!-- Configuration might be different along with Stream. -->11 <script src="https://vsp.mlytics.co/{TOKEN}.js"></script>12 <script src="https://jsdelivr.fusioncdn.com/npm/@mlytics/p2sp-sdk@latest/bundle/driver.min.js"></script>13 <script14 src="https://jsdelivr.fusioncdn.com/npm/@mlytics/p2sp-sdk@latest/bundle/peripheral/player/dplayer-hls.min.js"></script>15</head>24 collapsed lines
16
17<body>18 <div style="width: 800px; height: 640px">19 <div id="video" style="width: 800px; height: 640px; background-color: black;"></div>20 </div>21 <script>22 const driver = mlysdk.driver.initialize()23
24 const src = "PATH_TO_YOUR_M3U8"25 const video = document.getElementById('video')26 const adapter = driver.extensions.DPlayerHlsPlayerPlugin.create({27 container: video,28 autoplay: true,29 controls: true,30 video: {31 url: src,32 thumbnails: 'PATH_TO_YOUR_THUMBAILS',33 }34 })35 const dp = adapter.player36 </script>37</body>38
39</html>
應用於 Videojs
1<head>2 <link href="https://vjs.zencdn.net/8.3.0/video-js.css" rel="stylesheet" />3
4 <!-- If you'd like to support IE8 (for Video.js versions prior to v7) -->5 <!-- <script src="https://vjs.zencdn.net/ie8/1.1.2/videojs-ie8.min.js"></script> -->6</head>7
8<body>9 <video id="my-video" class="video-js" controls preload="auto" width="640" height="264" data-setup="{}">10 <source src="http://archive.org/download/thethreeagesbusterkeaton/Buster.Keaton.The.Three.Ages.ogv">11 <p class="vjs-no-js">12 To view this video please enable JavaScript, and consider upgrading to a13 web browser that14 <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>15 </p>65 collapsed lines
16 </video>17
18 <script src="https://vjs.zencdn.net/8.3.0/video.min.js"></script>19
20 <script>21 class ThumbnailTip extends videojs.getComponent('Component') {22 constructor(player, options) {23 super(player, options);24 }25
26 createEl() {27 return videojs.dom.createEl('div', {28 className: 'vjs-thumbnail-tip',29 innerHTML: '<div class="thumbnail" style="width:160px; height:90px; background:url(https://static.gordon.cf/p2sp-test/thumbnails-swyg.png) no-repeat;"></div>',30 style: 'position: absolute; z-index: 100;'31 });32 }33
34 showAt(time, totalDuration, totalThumbnails) {35 var backgroundPosition = getBackgroundPositionForTime(time, totalDuration, totalThumbnails);36 this.el().querySelector('.thumbnail').style.backgroundPosition = backgroundPosition;37 this.show();38 }39 }40
41 videojs.registerComponent('ThumbnailTip', ThumbnailTip);42
43 var player = videojs('my-video');44
45 let thumbnailTip = player.addChild('ThumbnailTip', {});46
47 var progressControl = player.controlBar.progressControl;48
49 progressControl.on('mousemove', function (event) {50 var mousePosition = videojs.dom.getPointerPosition(progressControl.el(), event);51 var time = mousePosition.x * player.duration();52
53 // Calculate the left offset in pixels.54 var leftOffsetPixels = mousePosition.x * progressControl.currentWidth();55
56 // Apply the left offset to the thumbnailTip57 thumbnailTip.el().style.left = leftOffsetPixels + 'px';58
59 // Calculate the top offset in pixels.60 // var topOffsetPixels = progressControl.el().getBoundingClientRect().top - thumbnailTip.el().offsetHeight;61 var topOffsetPixels = progressControl.el().getBoundingClientRect().top - 90 - 10;62
63 // Apply the top offset to the thumbnailTip64 thumbnailTip.el().style.top = topOffsetPixels + 'px';65
66 thumbnailTip.showAt(time, player.duration(), 100);67 });68
69
70 progressControl.on('mouseout', function () {71 thumbnailTip.hide();72 });73
74 function getBackgroundPositionForTime(time, totalDuration, totalThumbnails) {75 var thumbnailIndex = Math.floor((time / totalDuration) * totalThumbnails);76 return (-thumbnailIndex * 160) + 'px 0';77 }78 </script>79
80</body>
結論
Video Player 要實現縮圖預覽功能並不困難,不過考量到我們的產品並不介入 Player 的人機互動,因此建議在影片轉檔時,一併產生縮圖檔並提供 url 即可。