更新:
關於第二點,也就是說計算進度條拖放按鈕定位的問題。
很感謝 batsing 同學提供了更好的方案: 滑塊左偏量 = (進度條長 - 滑塊長) * (已播時間/總時長)
嘗試過之后發現除了拖曳滑片的時候會拋錨外,其它暫時沒發現什么問題,並且較之前的算法省了很多不必要的步驟,所以如今除了拖曳操作的時候使用舊方法外,已經進行修改。如果還有其它建議歡迎告訴我~~(づ ̄ 3 ̄)づ
正文:
本以為寫一個video播放器不難,可寫着寫着坑就越挖越大了。
先看一下播放器大概是長這樣的:
在開發過程中這幾個問題深深地困擾着我:
1、各機器兼容問題
2、關於視頻播放過程中進度條顯示情況
3、關於改變播放進度和音量大小的操作問題
1、各機器兼容問題
除了chrome很乖支持良好外,其中UC獲取不到duration屬性,也就獲取不到視頻的總長度了,但這部分可以由后台傳參數解決,但是不能禁用系統播放器這個就不太好了。。。。
拋開其它瀏覽器,來看看騰訊X5內核的瀏覽器的支持情況:http://x5.tencent.com/guide?id=2009
解決剩下問題前先了解一下video中常用的一些方法、屬性和事件:
duration:返回當前音頻/視頻的長度(以秒計)
currentTime:設置或返回音頻/視頻中的當前播放位置(以秒計)
play(): 開始播放音頻/視頻
pause(): 暫停當前播放的音頻/視頻
監聽timeupdate事件,獲得當前視頻播放的進度。
嗯,了解了這些之后也大概明白播放器是怎樣工作的了,看着API擼代碼就行了:http://www.w3school.com.cn/tags/html_ref_audio_video_dom.asp
2、關於視頻播放過程中進度條顯示情況 (主要指的是:進度條和拖放按鈕需要按照視頻的播放情況而進行調整的問題。)
本來進度條的計算方法是顯而易見的:
已播放進度 = 進度條長度 / (總時長 / 已播放時間)
但是因為進度條多了一個拖放按鈕,而且拖放按鈕的定位是跟隨視頻的播放而改變的。
如果按照上面的算法,很有可能在視頻播放完成的時候,拖放按鈕會超出進度條的范圍,變成了這樣:
也考慮過當按鈕的寬度 + 按鈕的position.left
>= 進度條長度的時候,拖放按鈕不再移動。但這樣就又產生了一個問題了:視頻並未播放完,但拖放按鈕已經到了盡頭,很容易給用戶造成以為視頻已經播放完畢的錯覺:
( 在已播放時間 < 總時長的情況下,拖放按鈕已經到盡頭了)
花了好多時間,地思來想去才靈光一閃想到解決方案:
為啥不在視頻播放過程中把正在向前移動的按鈕一點點地往后退呢:
是時候展示我高超的畫技了(*^__^*) 嘻嘻……
如上圖所示,塗黑的部分根據百分比慢慢地增加(為方便起見給上圖中塗黑的部分設一個變量: iconDetract;)根據視頻已播放的時間占總時長的百分比,計算出占按鈕寬度的百分比iconDetract。然后再用按鈕原來的position.left - iconDetract,得出按鈕的實際定位。
由於一點點地減去iconDetract,直到iconDetract == 按鈕的總寬度為止。(這時候視頻也播放完畢了):
先計算出 按鈕原來的定位(activeTol) = 進度條長度 / (總時長 / 已播放時間)
再根據視頻的播放進度計算出 iconDetract = 按鈕寬度 / (總時長 / 已播放時間)
最后得出 按鈕的實際定位 = activeTol - iconDetract
(嗯,第一個問題就這樣妥妥地解決掉了,目前只想到了這么一個解決方法。如果你有更好的方法,請告訴我。 )
3、關於改變播放時間點和音量大小的操作問題
改變視頻的時間點有兩種方法:
1:拖動拖放按鈕,慢慢改變視頻的播放時間點(慢)
2:點擊進度條的某一個位置,讓視頻直接跳到相對應的時間點(快)
改變音量的兩種方法:
1:拖動拖放按鈕,一點點慢慢改變音量的大小(慢)
2:點擊音量條的某一個位置,讓音量直接跳到相對應的位置上(快)
(它們的共同的地方都有拖動按鈕操作,以及點擊進度條,按鈕直接到相對於的位置上。唯一不同的是,操作的是視頻的進度條抑或是音量條。)
怎樣可以各自調用一套代碼實現各自不同的功能咧,這是目前所要解決的。
實現方法並不難,只是繁瑣。
因為拖放按鈕的時候需要先點擊拖放按鈕,但由於冒泡機制,會同時點擊進度條。這時候是就會把上面列出來的1和2的操作都一起做了,並不符合現實所需。
所以,調用的方法的時候傳一個參數moveing,當moveing == true的時候,當前對象是按鈕,而不是它的父級進度條,並且阻止冒泡 e.stopPropagation();
但由於拖放按鈕,和點擊進度條使得改變視頻的播放時間點以及改變音量,需要獲取到焦點的X、Y坐標,這時候除了區分當前操作的是視頻的進度抑或是調整音量的大小外還需要區分this的指向到底是當前操作對象還是它的父級。(代碼中liveEvent部分)
demo完整代碼如下
css:

1 <style type="text/css"> 2 .control_bar{background-color:black;height:30px;line-height:30px;} 3 .control_bar span{ height:20px;line-height:20px; vertical-align:middle; display:inline-block;background-color:#fff;opacity:0.8;margin:0 10px;cursor:pointer;} 4 .control_bar #progress_bar,.control_bar #volume_bar{width:32%;border-radius:5px;height:10px; line-height:10px; position:relative;} 5 #progress_bar em,#volume_bar em{background-color:#5CA66A;height:100%; display:inline-block;} 6 .control_bar #time{color:#fff;font-size:14px;background:none;} 7 .control_bar #volume_bar{width:100px;} 8 #progress_bar_icon,#volume_bar_icon{position:absolute;top:-3px;left:0px; border-radius:50%;height:20px;width:20px;display:block;background:#964E4E;} 9 </style>
html:

1 <body> 2 <div style="width:600px;"> 3 <div class="videoPlayer" id="videoContainer"> 4 <video id="video" width="600" height="360"> 5 <source src="test.mp4" type='video/mp4'> 6 </video> 7 <div class="control_bar"> 8 <span id="pause">暫停</span> 9 <span id="progress_bar"><em></em><i id="progress_bar_icon"></i></span> 10 <span id="time"></span> 11 <span id="volume">音量</span> 12 <span id="volume_bar"><em></em><i id="volume_bar_icon"></i></span> 13 <span id="fullscreen">放大</span> 14 </div> 15 </div> 16 </div> 17 <script type="text/javascript" src="index.js"></script> 18 <script type="text/javascript"> 19 20 var videoSet = new VideoContainer({ 21 volumeTol: 0.5, //默認音量 22 video: document.getElementById("video"), 23 pause: document.getElementById("pause"), 24 timer: document.getElementById("time"), 25 progressBar: document.getElementById("progress_bar"), 26 progressBarIcon:document.getElementById("progress_bar_icon"), 27 volume: document.getElementById("volume"), 28 volumeBar: document.getElementById("volume_bar"), 29 volumeBarIcon:document.getElementById("volume_bar_icon"), 30 fullscreen: document.getElementById("fullscreen") 31 }); 32 33 </script> 34 </body>
js:

'use strict'; //檢測設備類型 var startWhen, endWhen, moveWhen; var u = navigator.userAgent; if ( u.match(/\b(Windows\sNT|Macintosh)\b/) ) { // 鼠標 startWhen = 'mousedown'; endWhen = 'mouseup'; moveWhen = 'mousemove'; } else { // 觸摸屏 startWhen = 'touchstart'; endWhen = 'touchend'; moveWhen = 'touchmove'; } // 原生的JavaScript事件綁定函數 function bindEvent(ele, eventName, func){ if(window.addEventListener){ ele.addEventListener(eventName, func); } else{ ele.attachEvent('on' + eventName, func); } } function VideoContainer(options) { var t = this; t.volumeTol = options.volumeTol; t.video = options.video; t.pauseBtn = options.pause; t.progressBar = options.progressBar; t.timer = options.timer; t.volume = options.volume; t.volumeBar = options.volumeBar; t.volumeBarIcon = options.volumeBarIcon; t.progressBarIcon = options.progressBarIcon; t.fullScreenBtn = options.fullscreen; t.activeTol = ''; //載入視頻 //獲取視頻總時長 t.video.addEventListener("canplaythrough", function(){ t.videoDuration = t.video.duration; t.tolTime = t.videoTime(t.videoDuration); t.timer.innerHTML = '00:00/' + t.tolTime.minutes_str + ':' + t.tolTime.seconds_str; }); //播放時間點更新 t.video.addEventListener("timeupdate", function(){ t.timeupdate = t.videoTime(t.video.currentTime); //更新時間 t.timer.innerHTML = t.timeupdate.minutes_str + ':' + t.timeupdate.seconds_str + '/' + t.tolTime.minutes_str + ':' + t.tolTime.seconds_str; t.activeTol = (t.progressBar.offsetWidth - t.progressBarIcon.offsetWidth) * ( t.video.currentTime / t.videoDuration ); t.progressBarIcon.style.left = t.activeTol + 'px'; t.progressBar.children[0].style.width = t.activeTol + 'px'; }); //設置默認音量 t.volumeTol = t.volumeTol ? t.volumeTol : 0.5; t.video.volume = t.volumeTol; t.volumeBar.children[0].style.width = t.volumeTol * 100 + '%'; t.volumeBarIcon.style.left = t.volumeTol * 100 + '%'; //各綁定事件 //暫停 t.pauseBtn.onclick = function() { t.videoPaused(); } //音量 t.volume.onclick = function() { t.videoMuted(); } //全屏 t.fullScreenBtn.onclick = function() { t.videoFullscreen(this); } t.liveEvent.call(t.progressBar, { _this: this, attribute: 'progressBar', moveing: false}); t.liveEvent.call(t.volumeBar, { _this: this, attribute: 'volumeBar', moveing: false}); t.liveEvent.call(t.progressBarIcon, { _this: this, attribute: 'progress_bar_icon', moveing: true}); t.liveEvent.call(t.volumeBarIcon, { _this: this, attribute: 'volume_bar_icon', moveing: true}); bindEvent(document, endWhen, function(e) { t._draging = false; }); } VideoContainer.prototype.liveEvent = function(options) { var t = options._this; //區分當前操作元素是拖動按鈕還是它的父級 var _this = options.moveing ? this.parentNode : this; var _parentWidth = _this.offsetWidth; //檢測設備類型 var _ua = function(e) { var Pos = null; if ( u.match(/\b(Windows\sNT|Macintosh)\b/) ) { e = e || window.event; Pos = { left : e.pageX, top: e.pageY } } else { var touch = e.targetTouches[0] || e.changedTouches[0] Pos = { left : touch.pageX , top: touch.pageY } } return Pos; }; //區分拖動的是進度條還是音量條 function playStep() { if (options.attribute == 'progress_bar_icon' || options.attribute == 'progressBar') { if(options.moveing === true) { //根據拖放的進度計算相對於占icon寬的多少 var init = t.progressBarIcon.offsetWidth / (_parentWidth / t._durCount.left); var progressBarIconWidth = t.progressBarIcon.offsetWidth; //拖放按鈕是否超出范圍 if ( t._durCount.left + progressBarIconWidth - init >= _parentWidth ){ t.video.currentTime = t.videoDuration; t.progressBar.children[0].style.width = _parentWidth + 'px'; t.progressBarIcon.style.left = _parentWidth - progressBarIconWidth + 'px'; } else{ t.video.currentTime = t.videoDuration / (_parentWidth / (t._durCount.left + init)); t.progressBar.children[0].style.width = t._durCount.left + 'px'; t.progressBarIcon.style.left = t._durCount.left + 'px'; } } else{ t.activeTol = (t.progressBar.offsetWidth - progressBarIconWidth) * ( t._durCount.left / _parentWidth ); t.progressBar.children[0].style.width = t._durCount.left + 'px'; t.video.currentTime = t.videoDuration / (_parentWidth / t._durCount.left); t.progressBarIcon.style.left = t.activeTol + 'px'; } } else{ if ( t._durCount.left + t.volumeBarIcon.offsetWidth >= _parentWidth ){ t.volumeTol = 1; } else if( t._durCount.left <= 0 ){ t.volumeTol = 0; } else { t.volumeTol = 1 / (_parentWidth / t._durCount.left); } //拖放按鈕是否超出范圍 if (t.volumeTol == 1) { t.volumeBarIcon.style.left = _parentWidth - t.volumeBarIcon.offsetWidth + 'px'; } else{ t.volumeBarIcon.style.left = t.volumeTol * 100 + '%'; } t.video.volume = t.volumeTol; t.volumeBar.children[0].style.width = t.volumeTol * 100 + '%'; } } //僅限拖動按鈕可移動 if ( options.moveing === true) { bindEvent(this, moveWhen, function(e) { if (t._draging) { //鼠標移動了多少距離 t._mousePos = { left: (_ua(e).left - _this.offsetLeft) - t._startPos.left, top: (_ua(e).top - _this.offsetTop) - t._startPos.top } t._motion = true; //鼠標是否在拖動范圍內 if (0 <= t._mousePos.left <= _parentWidth || 0 < t._mousePos.top <= _this.offsetHeight){ //移動后的坐標 = 上次記錄的定位 + 鼠標移動了的距離; t._durCount = { left: t._oldPos.left + t._mousePos.left, top: t._oldPos.top + t._mousePos.top, }; playStep(); } else { t._draging = false; } } }); } bindEvent(this, startWhen, function(e) { // 防止選擇文字、拖動頁面(觸摸屏)鼠標移動過快松開拋錨 e.preventDefault(); if ( this.setCapture ) { this.setCapture(); } //如果當前對象是拖動按鈕,阻止冒泡 if (options.moveing === true) { e.stopPropagation(); } t._draging = true; t._motion = false; //記錄按下鼠標到背景圖片的距離 t._startPos = { left: _ua(e).left - _this.offsetLeft, top: _ua(e).top - _this.offsetTop } //當前拖動按鈕的定位 t._oldPos = { left: this.offsetLeft, top: this.offsetTop }; //本次拖動的距離 t._durCount = t._startPos; }); bindEvent(this, endWhen, function(e) { t._draging = false; //如果進行的是拖動操作,則不必再更新視頻的進度 if( !t._motion) { playStep(); } // 防止選擇文字、拖動頁面(觸摸屏)鼠標移動過快松開拋錨 if (this.releaseCapture) { this.releaseCapture(); } delete t._draging; //是否拖動 delete t._startPos; //鼠標按下坐標 delete t._oldPos; //拖動按鈕的定位 delete t._mousePos; //鼠標移動的距離 }); } //轉換時間單位 VideoContainer.prototype.videoTime = function(time) { var timeId = {} var seconds = time > 0 ? parseInt(time) : 0; var minutes = parseInt(time / 60); seconds = seconds - minutes * 60; timeId.minutes_str = minutes < 10 ? '0' + minutes : minutes; timeId.seconds_str = seconds < 10 ? '0' + seconds : seconds; return timeId; } //是否全屏 VideoContainer.prototype.videoFullscreen = function() { var t = this; var element = t.video; //反射調用 var invokeFieldOrMethod = function(element, method) { var usablePrefixMethod; ["webkit", "moz", "ms", "o", ""].forEach(function(prefix) { if (usablePrefixMethod) return; if (prefix === "") { // 無前綴,方法首字母小寫 method = method.slice(0,1).toLowerCase() + method.slice(1); } var typePrefixMethod = typeof element[prefix + method]; if (typePrefixMethod + "" !== "undefined") { if (typePrefixMethod === "function") { usablePrefixMethod = element[prefix + method](); } else { usablePrefixMethod = element[prefix + method]; } } }); return usablePrefixMethod; }; if( invokeFieldOrMethod(document,'FullScreen') || invokeFieldOrMethod(document,'IsFullScreen') || document.IsFullScreen ){ //退出全屏 if ( document.exitFullscreen ) { document.exitFullscreen(); } else if ( document.msExitFullscreen ) { document.msExitFullscreen(); } else if ( document.mozCancelFullScreen ) { document.mozCancelFullScreen(); } else if( document.oRequestFullscreen ){ document.oCancelFullScreen(); } else if ( document.webkitExitFullscreen ){ document.webkitExitFullscreen(); } else{ var docHtml = document.documentElement; var docBody = document.body; var videobox = element.parentNode; docHtml.style.cssText = ""; docBody.style.cssText = ""; videobox.style.cssText = ""; document.IsFullScreen = false; } } else { //進入全屏 //此方法不可以在異步任務中執行,否則火狐無法全屏 if(element.requestFullscreen) { element.requestFullscreen(); } else if(element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if(element.msRequestFullscreen){ element.msRequestFullscreen(); } else if(element.oRequestFullscreen){ element.oRequestFullscreen(); } else if(element.webkitRequestFullscreen){ element.webkitRequestFullScreen(); }else{ var docHtml = document.documentElement; var docBody = document.body; var videobox = element.parentNode; var cssText = 'width:100%;height:100%;overflow:hidden;'; docHtml.style.cssText = cssText; docBody.style.cssText = cssText; videobox.style.cssText = cssText+';'+'margin:0px;padding:0px;'; document.IsFullScreen = true; } } } //是否暫停 VideoContainer.prototype.videoPaused = function() { var t = this; if(t.video.paused) { t.video.play(); } else{ t.video.pause(); } } //調整音量 VideoContainer.prototype.videoMuted = function() { var t = this; if ( !t.video.defaultMuted ){ t.video.volume = 0; t.video.defaultMuted = true; t.volumeBar.children[0].style.width = 0 + '%'; t.volumeBarIcon.style.left = 0 + '%'; } else { t.video.volume = t.volumeTol; t.video.defaultMuted = false; t.volumeBar.children[0].style.width = t.volumeTol * 100 + '%'; //拖放按鈕是否超出范圍 if (t.volumeTol == 1) { t.volumeBarIcon.style.left = t.volumeBar.offsetWidth - t.volumeBarIcon.offsetWidth + 'px'; } else{ t.volumeBarIcon.style.left = t.volumeTol * 100 + '%'; } } }