使用 electron 作個播放器

使用 electron 作個播放器

本文同步更新在:https://github.com/whxaxes/blog/issues/8css

前言

雖然 electron 已經出來好長時間了,可是最近才玩了一下,寫篇博文記錄一下,以便往後回顧。html

electron 的入門能夠說是至關簡單,官方提供了一個 quick start,很流暢的就能夠跑起來一個應用。前端

爲啥要作個播放器呢,由於我在好久好久之前寫過一個網頁版的音頻可視化播放器,可是由於是在頁端,因此想播放本地音樂很麻煩,也無法保存。所以就想到用 electron 作個播放器 App,就能夠讀本地的網易雲音樂目錄了。vue

生成骨架

因爲習慣用 vue,所以也準備用 vue 來實現這個應用。而目前就已經有個 electron-vue 的 boilplate 能夠用。所以就直接經過 vue-cli 來進行初始化便可。node

vue init simulatedgreg/electron-vue boom

而後就能夠生成項目骨架了,結構以下:webpack

.
├── .electron-vue
│   ├── build.js
│   ├── dev-client.js
│   ├── dev-runner.js
│   ├── webpack.main.config.js
│   └── webpack.renderer.config.js
├── dist
├── src
│   ├── index.ejs
│   ├── main
│   │   ├── index.dev.js
│   │   ├── index.js
│   └── renderer
│       ├── assets
│       ├── components
|       ├── App.vue
│       ├── main.js
│       └── store.js
├── .eslintignore
├── .eslintrc.js
├── .travis.yml
├── appveyor.yml
├── .babelrc
├── package.json
├── README.md

生成好以後,就直接執行git

yarn dev

就能夠看到一個應用出現啦,而後就能夠愉快的開始開發了。github

主進程與渲染進程

在 electron 中有 main process 以及 renderer process 之分,簡單來講,main process 就是用來建立窗口之類的,相似於後臺,renderer process 就是跑在 webview 中的。兩個進程中能調用的接口有部分是通用,也有一部分是獨立的。不過不論是在哪一個進程中,均可以調用 node 的經常使用模塊,好比 fs、path 。web

所以在 webview 跑的頁面代碼中,也能夠經過 fs 模塊讀取本地文件,這點仍是很方便的。vue-cli

並且,就算在 renderer process 中想調用 main process 的接口也是能夠的,能夠經過 remote 模塊。好比我須要監聽當前窗口是否進入全屏,就能夠這樣寫:

import { remote } from 'electron';
const win = remote.app.getCurrentWindow();
win.on('enter-full-screen', () => {
 // do something
});

簡直方便至極。

建立窗口

建立窗口的邏輯是在主進程中作的,邏輯很簡單,就按照 electron 的 quick start 的方式進行建立便可。並且經過 electron-vue 生成的代碼,其實也已經幫你把這塊邏輯寫好了。就本身進行一些小修改就能夠了。

import { app, BrowserWindow, screen } from 'electron'

app.on('ready', () => {
    const { width, height } = screen.getPrimaryDisplay().workAreaSize;
    cosnt win = new BrowserWindow({
        height, width,
        useContentSize: true,
        titleBarStyle: 'hidden-inset',
        frame: false,
        transparent: true,
    });
    
    win.loadURL(`file://${__dirname}/index.html`);
})

因爲我作的播放器,想全身是黑色風格的,所以在建立窗口時,傳入 titleBarStyleframetransparent這幾個參數,能夠把頂部欄隱藏掉。固然,隱藏以後,窗口就無法拖動了。因此還要在頁面上加個用來拖動的透明頂部欄,再給個 css 屬性:

-webkit-app-region: drag;

就能夠實現窗口拖動了。

通訊

主進程和渲染進程之間的通訊是很經常使用的,通訊是經過 IPC 通道實現的。代碼邏輯寫起來也很簡單

main process 收發消息

import { ipcMain } from 'electron';
ipcMain.on('init', (evt, arg) => {
    evt.sender.send('sync-config', { msg: 'hello' })
});

renderer process 收發消息

import { ipcRenderer } from 'electron';
ipcRenderer.send('init');
ipcRenderer.on('sync-config', (evt, arg) => {
   console.log(arg.msg);
});

有一點要注意的就是,ipcMain 是沒有 send 方法的,若是須要 ipcMain 主動推送消息到渲染進程,須要使用窗口對象實現:

win.webContents.send('sync-config', { msg: 'hello' });

配置保存

每一個應用確定是有一些用戶配置的,好比放音樂的目錄須要保存到配置中,下次打開就能夠直接讀取那個目錄的音樂列表便可。

electron 提供了獲取相關路徑的接口 getPath 用於給應用保存數據。在 getPath 接口中,傳入相應名稱便可獲取到相應的路徑。

electron.app.getPath('home'); // 獲取用戶根目錄
electron.app.getPath('userData'); // 用於存儲 app 用戶數據目錄
electron.app.getPath('appData'); // 用於存儲 app 數據的目錄,升級會被福噶
electron.app.getPath('desktop'); // 桌面目錄
...

因爲咱們這些配置數據不能保存在應用下,由於若是保存在應用下,應用升級後就會被覆蓋掉,所以須要保存到 userData 下。

const electron = require('electron');
const dataPath = (electron.app || electron.remote.app).getPath('userData');
const fileUrl = path.join(dataPath, 'config.json');
let config;

if(fs.existSync(fileUrl)) {
  config = JSON.parse(fs.readFileSync(fileUrl));
} else {
  config = {};
  fs.writeFileSync(fileUrl, '{}');
}

不管在 renderer process 中,仍是在 main process 中,都是能夠調用,在 main process 中就經過 electron.app 調用,不然就經過 remote 模塊調用。

雖然不管在 main process 中仍是在 renderer process 中均可以讀到配置,可是考慮到兩個進程中數據同步的問題,我我的以爲,這種配置讀取與寫入,仍是統一在 main process 作好,renderer process 要保存數據就經過 IPC 消息通知 main process 進行數據的更改,保證配置數據的流向是單方向的,好比容易管理。

配置菜單

能夠經過 Menu 類實現。

import { Menu } from 'electron';
Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

template 的格式可直接看官方文檔,就是普通的 json 格式。

音頻播放

講完 electron 相關,而後就能夠講講怎麼播放音頻了。

因爲在 electron 中,在前端代碼中也可使用 node 的模塊,所以,剛開始的想法是,直接用 fs 模塊讀取音頻文件,而後再將讀取的 Buffer 轉成 Uint8Array 再轉成 AudioBuffer ,而後鏈接到音頻輸出上進行播放就好了。大概邏輯以下:

const AC = new window.AudioContext();
const analyser = AC.createAnalyser();
const buf = fs.readFileSync(music.url);
const uint8Buffer = Uint8Array.from(buf);

// 音頻解碼
AC.decodeAudioData(uint8Buffer.buffer)
    .then(audioBuf => {
         const bs = AC.createBufferSource();
         bs.buffer = audioBuf;
         bs.connect(analyser);
         analyser.connect(AC.destination);
         bs.start();
    });

可是,有個問題,音頻解碼很費時間,解碼一個三四分鐘的 mp3 文件就得 2 ~ 4 秒,這樣的話我點擊播放音樂都得等兩秒以上,這簡直不能忍,因此就考慮換種方法,好比用流的方式。

抱着這種想法就去查閱了文檔,結果發現沒有支持流的解碼接口,再接着就想本身來模擬流的方式,讀出來的 buffer 分紅 N 段,而後逐段進行解碼,解碼完一段就播一段,嗯...想的挺好,可是發現這樣作會致使解碼失敗,多是粗暴的將 buffer 分段對解碼邏輯有影響。

上面的方法行不通了,固然還有方法,audio 標籤是支持流式播放的。因而就在啓動應用的時候,建個音頻服務。

function startMusicServer(callback) {
  const server = http.createServer((req, res) => {
    const musicUrl = decodeURIComponent(req.url);
    const extname = path.extname(musicUrl);
    if (allowKeys.indexOf(extname) < 0) {
      return notFound(res);
    }

    const filename = path.basename(musicUrl);
    const fileUrl = path.join(store.get(constant.MUSIC_PATH), filename);
    if (!fs.existsSync(fileUrl)) {
      return notFound(res);
    }

    const stat = fs.lstatSync(fileUrl);
    const source = fs.createReadStream(fileUrl);
    res.writeHead(200, {
      'Content-Type': allowFiles[extname],
      'Content-Length': stat.size,
      'Access-Control-Allow-Origin': '*',
      'Cache-Control': 'max-age=' + (365 * 24 * 60 * 60 * 1000),
      'Last-Modified': String(stat.mtime).replace(/\([^\x00-\xff]+\)/g, '').trim(),
    });
    source.pipe(res);
  }).listen(0, () => {
    callback(server.address().port);
  });

  return server;
}

而後在前端,直接更換 audio 標籤的 src 便可,而後鏈接上音頻輸出:

<audio ref="audio"
           :src="url"
           crossorigin="anonymous"></audio>
const audio = this.$refs.audio;
const source = AC.createMediaElementSource(this.$refs.audio);
source.connect(analyser);
analyser.connect(AC.destination);

就這麼愉快的實現了流式播放了。。。感受白折騰了好久。

音頻可視化

這個其實我在之前的博客裏有說過了,不過再簡單的說一下。在上一段中我會把音頻鏈接到一個 analyser 中,其實這個是一個音頻分析器,能夠將音頻數據轉成頻率數據。咱們就能夠用這些頻率數據來作可視化。

只須要經過如下這段邏輯就能夠獲取到頻率數據了,由於頻率數據數據都是 0 ~ 255 的大小,長度總共 1024,所以用個 Uint8Array 來存儲。

const arrayLength = analyser.frequencyBinCount;
const array = new Uint8Array(arrayLength);
analyser.getByteFrequencyData(array);

而後獲取到這個數據以後,就能夠在 canvas 中把不一樣頻率以圖像的形式畫出來便可。具體就不贅述了,有興趣的能夠看我之前寫的這篇博文

打包

編寫完代碼以後,就可使用 electron-packager 進行打包,在 Mac 上就會打包成 app,在 windows 應該會打成 exe 吧(沒試過)。

安裝 electron-packager (npm install electron-packager -g)以後就能夠打包了。

electron-packager .

總結

electron 仍是至關方便的,讓 web 開發者也能夠輕鬆編寫桌面應用。

上述代碼均在:https://github.com/whxaxes/boom ,有興趣的能夠 clone 下來跑一下玩玩。

相關文章
相關標籤/搜索