基于electron的音視頻播放器
前言
我是一個前端工程師,前一段時間想著搞一個屬于自己的作品,所以就突發(fā)奇想搞了一個基于electron的音視頻播放器桌面應(yīng)用程序。經(jīng)過幾個月的開發(fā),終于實現(xiàn)了大部分的功能,所以我想在這里總結(jié)一下前面一段時間的工作,以及在開發(fā)的時候遇見的各種坑坑洼洼,希望可以對想要從事electron桌面軟件開發(fā)的朋友有點幫助吧。
選擇做一個音視頻播放器桌面應(yīng)用程序原因
一開始我是打算做個網(wǎng)站或者webAPP那種的,但是作為一名前端工程師來說,網(wǎng)站或者webAPP并沒有什么可以吸引我的地方,因為那種東西都是司空見慣的,千篇一律,所以我就想著使用electron做一個音視頻播放器,搞點與眾不同的東西。雖然網(wǎng)上有很多視頻播放器,但是那些播放器基本都是使用C寫出來的,并沒有使用electron做出來的播放器。
技術(shù)的選型
我主要使用的技術(shù)是electron、node、vue、express、HTML5相關(guān)技術(shù)、DPlayer。Electron主要是用來構(gòu)建音視頻播放器所需要的環(huán)境,提供訪問系統(tǒng)資源的api(調(diào)用資源管理器,瀏覽器等等)以及打包成桌面應(yīng)用程序。其實說白了electron就是相當于一個瀏覽器和服務(wù)器后臺的結(jié)合,但是electron打破了傳統(tǒng)瀏覽器的界限,提供了調(diào)用系統(tǒng)底層資源的api,使得開發(fā)者可以使用系統(tǒng)的資源,比如攝像頭,麥克風(fēng)等等。同時開發(fā)者還可以在electron中調(diào)用node的模塊,搭建一個后臺等等。electron有2種進程,一種是渲染進程,另一種是主進程。主進程只能有一個,負責調(diào)用系統(tǒng)底層資源,管理窗口那些。渲染進程可以有多條,負責渲染頁面的。Node主要使用了fs和path這2個模塊,因為這個音視頻播放器涉及到了的文件讀取操作,所以這2個模塊是必不可少的。Vue是負責構(gòu)建界面的。Express是用來在應(yīng)用程序中構(gòu)建一個微型后臺,負責把視頻讀取出來變成流的形式,然后返回給前端界面。html5主要使用了拖拽api、全屏api、Notification消息通知等技術(shù)。DPlayer是整個音視頻播放器的核心組件,負責播放音視頻的。
已經(jīng)實現(xiàn)了的功能
- 視頻播放:目前已經(jīng)支持大多數(shù)視頻格式,比如 MP4、WebM、mkv、avi、WMV、FLV、rmvb 等,后續(xù)會添加更多的視頻格式
- 音頻播放:目前已經(jīng)支持大多是音頻格式,比如 MP3 等,后續(xù)會添加更多的音頻格式
- 換膚功能:該功能類似其他軟件的換膚功能,用戶可以根據(jù)自己的喜好選擇不同的主題皮膚
- 歷史記錄:音視頻播放器會自動記錄用戶播放已經(jīng)過的的視頻或音頻,比如音頻或視頻播放到那個時間
- 記憶功能:音視頻播放器會自動保存用戶的操作和修改的配置,比如用戶更換了主題皮膚,用戶關(guān)閉了應(yīng)用后再次打開,音視頻播放器會應(yīng)用用戶已經(jīng)修改的主題皮膚。用戶對視頻或音頻進行加速等操作都會被記憶下來,用戶再次點擊該視頻或音頻就會恢復(fù)用戶的操作
- 播放模式:播放模式主要有5種,分別是 單個播放、單個循環(huán)、循環(huán)播放列表、順序播放、隨機播放
- 排序模式:排序模式主要有5種,分別是 默認排序、大小排序、時間排序、隨機排序、名稱排序
- 置頂功能:保持應(yīng)用界面始終在最頂端
- 加減速功能:音視頻加速或者減速播放
- 拖拽文件或文件夾:用戶可以把文件或者文件夾拖拽進音視頻播放器中,應(yīng)用會過濾掉不能播放的文件
- 全屏功能:實現(xiàn)了應(yīng)用的全屏功能,這里是使用了electron提供的全屏api,沒有使用html5的全屏api
- 右鍵菜單功能:目前已經(jīng)實現(xiàn)了大多數(shù)右鍵菜單的功能,沒實現(xiàn)的后續(xù)實現(xiàn)
音視頻播放實現(xiàn)
音視頻播放實現(xiàn)。一開始我是想著直接使用HTML5提供的標簽,但是這個標簽局限性很大,它只支持三種視頻格式:MP4、WebM、Ogg,但是目前主流視頻格式還有avi、mkv、wmv等視頻格式。然后我就想著對那些不是MP4、WebM、Ogg的視頻格式進行轉(zhuǎn)碼,但是需要使用ffmepg來進行轉(zhuǎn)碼,electron進行打包的時候是不會把ffmepg這個工具打包進去的,所以這就要求每一個使用這個音視頻播放器的用戶需要自己去手動安裝ffmepg和配置環(huán)境,這種做法顯然是不行的。同時轉(zhuǎn)碼的過程是需要時間,一旦遇見那些幾個G是視頻,起碼要花費幾分鐘進行轉(zhuǎn)碼,然后才能響應(yīng)用戶的操作,這對于用戶來說是極其差的用戶體驗。最后我選擇了使用express在electron中搭建一個微型服務(wù)器,當express接收到前端界面的請求時,就把所需要的視頻讀取出來,以流的形式返回給前端,因為實在electron環(huán)境下,所以使用的是localhost,這樣就可以快速的響應(yīng)用戶的操作,逼近原生播放器的體驗。
代碼:
let pathSrc = req.query.video; let stat = fs.statSync(pathSrc); let fileSize = stat.size; let range = req.headers.range; if (range) { //有range頭才使用206狀態(tài)碼 let parts = range.replace(/bytes=/, "").split("-"); let start = parseInt(parts[0], 10); let end = parts[1] ? parseInt(parts[1], 10) : start + 999999; // end 在最后取值為 fileSize - 1 end = end > fileSize - 1 ? fileSize - 1 : end; let chunksize = (end - start) + 1; let file = fs.createReadStream(pathSrc, { start, end }); let head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); } else { let head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(pathSrc).pipe(res); }
右鍵菜單實現(xiàn)
右鍵菜單我一開始的做法是監(jiān)聽右鍵事件,通過動態(tài)生成DOM,然后插入到頁面中。但是這種做法并不可行。因為生成的右鍵菜單需要出現(xiàn)在用戶鼠標點擊的位置附近,用戶鼠標出現(xiàn)的位置可能是應(yīng)用程序中間,可能是左上角,右上角等等。因為是使用DOM生成,渲染出來的右鍵菜單不能超出文檔的范圍,否則就會出現(xiàn)滾動條。所以當用戶的鼠標位置在界面邊界的時候,需要計算出右鍵菜單應(yīng)該出現(xiàn)在鼠標所在位置的上面、下面或者左上角等等,這需要經(jīng)過一系列大量的計算才能得出結(jié)果,這在electron的渲染進程顯然是不可行,因為這么復(fù)雜的計算可能會造成頁面卡頓。所以后面我是用electron的Menu模塊,在主進程中生成右鍵菜單,減輕渲染進程的負擔,同是還減少了大量的DOM操作,但是是用electron的Menu模塊生成的右鍵菜單就是白底黑字,樣式可能沒有符合預(yù)期的效果。但是通過2種生成右鍵菜單的利益權(quán)衡后,采用electron的Menu模塊生成右鍵菜單才是最佳的選擇。
代碼
let contextMenuTemplate = [ { label: "播放順序", submenu: [ { label: this.playMode == 1 ? "√ 單個播放" : " 單個播放", click: () => { this.setPlayMode(1); } }, { label: this.playMode == 2 ? "√ 單個循環(huán)" : " 單個循環(huán)", click: () => { this.setPlayMode(2); } }, { label: this.playMode == 3 ? "√ 循環(huán)列表" : " 循環(huán)列表", click: () => { this.setPlayMode(3); } }, { label: this.playMode == 4 ? "√ 順序播放" : " 順序播放", click: () => { this.setPlayMode(4); } }, { label: this.playMode == 5 ? "√ 隨機播放" : " 隨機播放", click: () => { this.setPlayMode(5); } } ] }, { type: "separator" }, { label: "聲音", submenu: [ { label: this.volumePercent == 0.1?"√ 10%":" 10%", click:()=>{ let inWidth = 0.1*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.2?"√ 20%":" 20%", click:()=>{ let inWidth = 0.2*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.3?"√ 30%":" 30%", click:()=>{ let inWidth = 0.3*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.4?"√ 40%":" 40%", click:()=>{ let inWidth = 0.4*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.5?"√ 50%":" 50%", click:()=>{ let inWidth = 0.5*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.6?"√ 60%":" 60%", click:()=>{ let inWidth = 0.6*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.7?"√ 70%":" 70%", click:()=>{ let inWidth = 0.7*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.8?"√ 80%":" 80%", click:()=>{ let inWidth = 0.8*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 0.9?"√ 90%":" 90%", click:()=>{ let inWidth = 0.9*62 this.setInWidth(inWidth) } }, { label: this.volumePercent == 1?"√ 100%":" 100%", click:()=>{ let inWidth = 1*62 this.setInWidth(inWidth) } }, { label: (this.volumePercent != 0.1&&this.volumePercent != 0.2&&this.volumePercent != 0.3&&this.volumePercent != 0.4&&this.volumePercent != 0.5&&this.volumePercent != 0.6&&this.volumePercent != 0.7&&this.volumePercent != 0.8&&this.volumePercent != 0.9&&this.volumePercent != 1&&this.volumePercent != 0)?`√ 其他(${Math.round(this.volumePercent*100)}%)`:" 其他", }, { label: this.volumePercent == 0?"√ 靜音":" 靜音", click:()=>{ let inWidth = 0 this.setInWidth(inWidth) } } ] }, { type: "separator" }, { label: "設(shè)置" } ]; if (this.currentVideo) { let addMenu = [ { label: this.isPlaying ? "暫停" : "播放", click: () => { this.setPlaying(!this.isPlaying); } }, { type: "separator" } ]; contextMenuTemplate.unshift(...addMenu); contextMenuTemplate.splice(4, 0, { label: this.isFullScreen ? "退出全屏" : "全屏", click: () => { this.setFullScreen(!this.isFullScreen); } }); contextMenuTemplate.push({ label:'文件信息', click:()=>{ this.videoInfo = this.currentVideo this.isShowInfo = true } }) } let m = Menu.buildFromTemplate(contextMenuTemplate); Menu.setApplicationMenu(m); m.popup({ window: remote.getCurrentWindow() });
總結(jié)
為什么直說音視頻播放和右鍵菜單實現(xiàn)?因為這2個功能是我重寫的次數(shù)最多的功能,特別是音視頻播放這個功能,我還寫了很多demo去測試不同的播放方法,測試不同播放方法的性能問題,最終才選擇了搭建一個微型服務(wù)器這個方法。其他的功能沒什么需要特別講解的地方,其他功能都是細節(jié)問題,同住還要注意封裝公共代碼,降低耦合度,分模塊,分功能去編寫代碼。因為一開始我并沒有注意到這些地方,寫到后面代碼越來越多,出現(xiàn)問題的時候都無從下手,不知道改哪里,這使得我花費了大量的時間對代碼進行重構(gòu),整理。
效果圖
效果圖1
效果圖2
效果圖3
效果圖4
效果圖5
效果圖6
效果圖7
最后如果大家覺得我這個音視頻播放器還可以的話,歡迎去我的github:
https://github.com/c10342/player 給個star
到此這篇關(guān)于基于electron的音視頻播放器的文章就介紹到這了,更多相關(guān)electron音視頻播放器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
isArray()函數(shù)(JavaScript中對象類型判斷的幾種方法)
我們知道,JavaScript中檢測對象類型的運算符有:typeof、instanceof,還有對象的constructor屬性2009-11-11javascript將非數(shù)值轉(zhuǎn)換為數(shù)值
parseInt()不能轉(zhuǎn)換浮點型數(shù)值,我們用parseFloat()來解決。這篇文章主要介紹了javascript將非數(shù)值轉(zhuǎn)換為數(shù)值,需要的朋友可以參考下2018-09-09JS實現(xiàn)點擊按鈕控制Div變寬、增高及調(diào)整背景色的方法
這篇文章主要介紹了JS實現(xiàn)點擊按鈕控制Div變寬、增高及調(diào)整背景色的方法,涉及javascript動態(tài)操作頁面元素屬性的相關(guān)技巧,適用于動態(tài)更換頁面皮膚的功能,需要的朋友可以參考下2015-08-08