Electron+vue從零開始打造一個(gè)本地播放器的方法示例
為什么要做?
女朋友工作是音頻后期,平常會(huì)收集一些音頻音樂,需要看音頻的頻譜波形,每次用au這種大型軟件播放音樂看波形,很不方便,看到她這么辛苦,身為程序猿的我痛心疾首,于是,就有了這么一個(gè)小軟件,軟件涉及到的技術(shù)主要為electron,vue,node,波形的展示主要通過wavesurfer生成。
從零開始-搭建項(xiàng)目
項(xiàng)目通過vue腳手架搭建的,所以需要安裝cli工具,如果已經(jīng)裝了,可以跳過這一步.
npm install -g @vue/cli # OR yarn global add @vue/cli
裝好后,通過腳手架搭建項(xiàng)目
vue create music
vue需要與electron集成,這里社區(qū)已經(jīng)有比較成熟的vue插件了,Vue CLI Plugin Electron Builder。
vue add electron-builder
懶人可以直接去clone我的搭建好得架子直接開發(fā), 戳這里。
從零開始-項(xiàng)目開發(fā)
首先先明確下這個(gè)播放器的功能需求,主要有這幾個(gè)
- 不添加文件目錄,加載任意的本地文件系統(tǒng)內(nèi)的音頻文件,直接調(diào)用播放器播放
- 前一首后一首功能
- 聲音音量控制
- 自定義軟件窗口
如何關(guān)聯(lián)播放
如何實(shí)現(xiàn)關(guān)聯(lián)播放?因?yàn)閷?duì)electron不是很熟,查了很久electron的資料,終于找到了配置項(xiàng),需要配置fileAssociations
fileAssociations: [ { ext: ["mp3", "wav", "flac", "ogg", "m4a"], name: "music", role: "Editor" } ],
配置好后,通過electron的open-file事件,獲取打開的音頻文件的本地路徑。對(duì)于windows,需要通過process.argv,來獲取文件路徑。
const filePath = process.argv[1];
如何加載本地音頻文件
上一步通過配置拿到文件的本地路徑后,下一步就是通過路徑讀取音頻文件的信息。由于音頻的插件無法解析絕對(duì)路徑,所以需要通過node的文件系統(tǒng),通過fs.readFileSync讀取到文件的buffer信息。
let buffer = fs.readFileSync(diskPath); //讀取文件,并將緩存區(qū)進(jìn)行轉(zhuǎn)換
讀取后需要將buffer轉(zhuǎn)換成node可讀流
const stream = this.bufferToStream(buffer);//將buffer數(shù)據(jù)轉(zhuǎn)換成node 可讀流
轉(zhuǎn)換方法 bufferToStream
bufferToStream(binary) { const readableInstanceStream = new Readable({ read() { this.push(binary); this.push(null); } }); return readableInstanceStream; }
轉(zhuǎn)換成流后需要將音頻流轉(zhuǎn)換成blob對(duì)象來加載,實(shí)現(xiàn)方法
module.exports = streamToBlob function streamToBlob (stream, mimeType) { if (mimeType != null && typeof mimeType !== 'string') { throw new Error('Invalid mimetype, expected string.') } return new Promise((resolve, reject) => { const chunks = [] stream .on('data', chunk => chunks.push(chunk)) .once('end', () => { const blob = mimeType != null ? new Blob(chunks, { type: mimeType }) : new Blob(chunks) resolve(blob) }) .once('error', reject) }) }
轉(zhuǎn)blob
let fileUrl; // blob對(duì)象 streamToBlob(stream) .then(res => { fileUrl = res; // console.log(fileUrl); //將blob對(duì)象轉(zhuǎn)成blob鏈接 let filePath = window.URL.createObjectURL(fileUrl); // console.log(filePath); this.wavesurfer.load(filePath); // 自動(dòng)播放 this.wavesurfer.play(); this.playing = true; }) .catch(err => { console.log(err); });
這樣就實(shí)現(xiàn)了加載本地文件播放了
上一首下一首功能
這里的上一首下一首的功能是基于上面獲取到的文件的絕對(duì)路徑,通過node的path模塊,path.dirname獲取到文件的父級(jí)目錄。
const dirPath = path.dirname(diskPath);
然后通過fs.readdir讀取目錄下所有文件,會(huì)返回一個(gè)文件名數(shù)組,找到該目錄下正在播放的文件的下標(biāo),通過數(shù)組下標(biāo)判斷前一首和后一首歌曲的名稱,然后再組裝成絕對(duì)路徑,讀取資源播放
playFileList(diskPath, pos) { let isInFiles; let fileIndex; let preIndex; let nextIndex; let fullPath; let dirPath = path.dirname(diskPath); let basename = path.basename(diskPath); fs.readdir(dirPath, (err, files) => { isInFiles = files.includes(basename); if (isInFiles && pos === "pre") { fileIndex = files.indexOf(basename); preIndex = fileIndex - 1; fullPath = path.resolve(dirPath, files[preIndex]); this.loadMusic(fullPath); } if (isInFiles && pos === "next") { fileIndex = files.indexOf(basename); nextIndex = fileIndex + 1; fullPath = path.resolve(dirPath, files[nextIndex]); this.loadMusic(fullPath); } }); },
聲音音量控制
音量控制需要通過監(jiān)聽input的鍵入事件,獲取到range的值,然后通過設(shè)置樣式background-image,動(dòng)態(tài)計(jì)算百分比,然后調(diào)用wavesurfer的setVolume方法調(diào)節(jié)音量
:style="`background-image:linear-gradient( to right, ${fillColor}, ${fillColor} ${percent}, ${emptyColor} ${percent})`"
改變音量changeVol事件
changeVol(e) { let val = e.target.value; let min = e.target.min; let max = e.target.max; let rate = (val - min) / (max - min); this.percent = 100 * rate + "%"; console.log(this.percent, rate); this.wavesurfer.setVolume(Number(rate)); },
自定義標(biāo)題欄
個(gè)人覺得系統(tǒng)自帶的菜單欄太丑了,就給設(shè)置了無邊框再自己加上最小化,關(guān)閉的功能。最小化,關(guān)閉是通過ipc通信,渲染進(jìn)程監(jiān)聽到有點(diǎn)擊操作后,通知主進(jìn)程進(jìn)行相應(yīng)的操作。
渲染進(jìn)程
close() { ipcRenderer.send("close"); }, minimize() { ipcRenderer.send("minimize"); }
主進(jìn)程
ipcMain.on("close", () => { win.close(); app.quit(); }); ipcMain.on("minimize", () => { win.minimize(); });
打開多個(gè)實(shí)例的問題
在實(shí)際測試的過程中發(fā)現(xiàn)會(huì)出現(xiàn),打開一首新的音樂播放,就會(huì)出現(xiàn)重新開一個(gè)實(shí)例的現(xiàn)象,不能實(shí)現(xiàn)覆蓋播放,后面查閱資料發(fā)現(xiàn)electron有一個(gè)second-instance事件,可以監(jiān)聽是否打開了第二個(gè)實(shí)例。當(dāng)?shù)诙€(gè)實(shí)例被執(zhí)行并且調(diào)用 時(shí),這個(gè)事件將在應(yīng)用程序的首個(gè)實(shí)例中觸發(fā),并且會(huì)返回第二個(gè)實(shí)例的相關(guān)信息,然后通過主進(jìn)程通知渲染進(jìn)程,告知渲染進(jìn)程第二個(gè)實(shí)例的本地絕對(duì)路徑,渲染進(jìn)程接收到信息后,立馬加載第二個(gè)實(shí)例的資源。app.requestSingleInstanceLock(),表示應(yīng)用程序?qū)嵗欠癯晒θ〉昧随i。 如果它取得鎖失敗,可以假設(shè)另一個(gè)應(yīng)用實(shí)例已經(jīng)取得了鎖并且仍舊在運(yùn)行,所以可以直接關(guān)閉掉,這樣就避免了打開多個(gè)實(shí)例的問題
主進(jìn)程
const gotTheLock = app.requestSingleInstanceLock(); if (gotTheLock) { app.on("second-instance", (event, commandLine, workingDirectory) => { // 監(jiān)聽是否有第二個(gè)實(shí)例,向渲染進(jìn)程發(fā)送第二個(gè)實(shí)例的本地路徑 win.webContents.send("path", `${commandLine[commandLine.length - 1]}`); if (win) { if (win.isMinimized()) win.restore(); win.focus(); } }); app.on("ready", async () => { createWindow(); }); } else { app.quit(); }
渲染進(jìn)程
ipcRenderer.on("path", (event, arg) => { const newOriginPath = arg; // console.log(newOriginPath); this.loadMusic(newOriginPath); });
自動(dòng)更新
需求的起因是,在很興奮的給女朋友成品的時(shí)候,尷尬的被女朋友試出很多bug(捂臉ing),然后頻繁的修改打包,然后通過私發(fā)傳給她。特別麻煩,所以這個(gè)需求很急迫。最后查了資料,通過electron-updater實(shí)現(xiàn)了這個(gè)需求.
安裝electron-updater
yarn add electron-updater
發(fā)布設(shè)置
electronBuilder: { builderOptions: { publish: ['github'] } }
主進(jìn)程監(jiān)聽
autoUpdater.on("checking-for-update", () => {}); autoUpdater.on("update-available", info => { dialog.showMessageBox({ title: "新版本發(fā)布", message: "有新內(nèi)容更新,稍后將重新為您安裝", buttons: ["確定"], type: "info", noLink: true }); }); autoUpdater.on("update-downloaded", info => { autoUpdater.quitAndInstall(); });
生成Github Access Token
因?yàn)槭怯胓ithub作為更新站,所以本地需要相應(yīng)的操作權(quán)限,去這里生成token,戳這,生成后,在powershell中設(shè)置
[Environment]::SetEnvironmentVariable("GH_TOKEN","<YOUR_TOKEN_HERE>","User") # 例如 [Environment]::SetEnvironmentVariable("GH_TOKEN","sdfdsfgsdg14463232","User")
打包上傳Github
yarn electron:build -p always
完成上面步驟后軟件會(huì)自動(dòng)上傳打包后的文件到release,然后編輯下release就可以直接發(fā)布了,軟件是基于版本號(hào)更新的,所以記得一定要改版本號(hào)
從零開始-結(jié)束
作為程序猿最開心的事莫過于得到女朋友的夸獎(jiǎng),雖然這是一個(gè)小程序,實(shí)現(xiàn)難度也不高,但是最后做出最小可用的版本呈現(xiàn)在女朋友面前的時(shí)候,看到女盆友感動(dòng)的眼神,我想,這應(yīng)該是我作為程序猿唯一感到欣慰的時(shí)候。軟件還有很多可以改進(jìn)的地方,源碼在此,戳這里Github
到此這篇關(guān)于Electron+vue從零開始打造一個(gè)本地播放器的方法示例的文章就介紹到這了,更多相關(guān)Electron+vue本地播放器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue watch如何深度監(jiān)聽數(shù)組每一項(xiàng)的變化
這篇文章主要介紹了vue watch如何深度監(jiān)聽數(shù)組每一項(xiàng)的變化問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07Vue百度地圖實(shí)現(xiàn)定位和marker拖拽監(jiān)聽功能
這篇文章主要介紹了Vue百度地圖實(shí)現(xiàn)定位和marker拖拽監(jiān)聽功能,實(shí)現(xiàn)思路非常簡單,本文結(jié)合實(shí)例代碼效果圖給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-11-11詳解Vue的watch中的immediate與watch是什么意思
這篇文章主要介紹了詳解Vue的watch中的immediate與watch是什么意思,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12淺談vue中改elementUI默認(rèn)樣式引發(fā)的static與assets的區(qū)別
下面小編就為大家分享一篇淺談vue中改elementUI默認(rèn)樣式引發(fā)的static 與assets的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-02-02Vue3中如何修改父組件傳遞到子組件中的值(全網(wǎng)少有!)
大家都知道,vue是具有單向數(shù)據(jù)流的傳遞特性,下面這篇文章主要給大家介紹了關(guān)于Vue3中如何修改父組件傳遞到子組件中值的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04詳解 vue better-scroll滾動(dòng)插件排坑
本篇文章主要介紹了詳解 vue better-scroll滾動(dòng)插件排坑,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02vue3 Element-Plus el-backtop無效問題及解決
這篇文章主要介紹了vue3 Element-Plus el-backtop無效問題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03Vue數(shù)據(jù)更新頁面卻沒有更新的幾種情況以及解決方法
我們?cè)陂_發(fā)過程中會(huì)碰到數(shù)據(jù)更新,但是頁面卻沒有更新的情況,下面這篇文章主要給大家介紹了關(guān)于Vue數(shù)據(jù)更新頁面卻沒有更新的幾種情況以及解決方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06