Electron+vue从零开始打造一个本地播放器

BCM9HI.png

为何要作?

女友工做是音频后期,日常会收集一些音频音乐,须要看音频的频谱波形,每次用au这种大型软件播放音乐看波形,很不方便,看到她这么辛苦,身为程序猿的我痛心疾首,因而,就有了这么一个小软件,软件涉及到的技术主要为electron,vue,node,波形的展现主要经过wavesurfer生成。html

从零开始-搭建项目

项目经过vue脚手架搭建的,因此须要安装cli工具,若是已经装了,能够跳过这一步.vue

npm install -g @vue/cli
# OR
yarn global add @vue/cli

装好后,经过脚手架搭建项目node

vue create music

vue须要与electron集成,这里社区已经有比较成熟的vue插件了,Vue CLI Plugin Electron Buildergit

vue add electron-builder

懒人能够直接去clone个人搭建好得架子直接开发, 戳这里github

从零开始-项目开发

首先先明确下这个播放器的功能需求,主要有这几个web

  • 不添加文件目录,加载任意的本地文件系统内的音频文件,直接调用播放器播放
  • 前一首后一首功能
  • 声音音量控制
  • 自定义软件窗口

如何关联播放

如何实现关联播放?由于对electron不是很熟,查了好久electron的资料,终于找到了配置项,须要配置fileAssociationsvue-cli

fileAssociations: [
          {
            ext: ["mp3", "wav", "flac", "ogg", "m4a"],
            name: "music",
            role: "Editor"
          }
        ],

配置好后,经过electron的open-file事件,获取打开的音频文件的本地路径。对于windows,须要经过process.argv,来获取文件路径。shell

const filePath = process.argv[1];

如何加载本地音频文件

上一步经过配置拿到文件的本地路径后,下一步就是经过路径读取音频文件的信息。因为音频的插件没法解析绝对路径,因此须要经过node的文件系统,经过fs.readFileSync读取到文件的buffer信息。macos

let buffer = fs.readFileSync(diskPath); //读取文件,并将缓存区进行转换

读取后须要将buffer转换成node可读流npm

const stream = this.bufferToStream(buffer);//将buffer数据转换成node 可读流

转换方法 bufferToStream

bufferToStream(binary) {
      const readableInstanceStream = new Readable({
        read() {
          this.push(binary);
          this.push(null);
        }
      });
      return readableInstanceStream;
    }

转换成流后须要将音频流转换成blob对象来加载,实现方法

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)
  })
}

转blob

let fileUrl; // blob对象
  streamToBlob(stream)
    .then(res => {
      fileUrl = res;
      // console.log(fileUrl);

      //将blob对象转成blob连接
      let filePath = window.URL.createObjectURL(fileUrl);
      // console.log(filePath);
      this.wavesurfer.load(filePath);

      // 自动播放
      this.wavesurfer.play();
      this.playing = true;
    })
    .catch(err => {
      console.log(err);
    });

这样就实现了加载本地文件播放了

上一首下一首功能

这里的上一首下一首的功能是基于上面获取到的文件的绝对路径,经过node的path模块,path.dirname获取到文件的父级目录。

const dirPath = path.dirname(diskPath);

而后经过fs.readdir读取目录下全部文件,会返回一个文件名数组,找到该目录下正在播放的文件的下标,经过数组下标判断前一首和后一首歌曲的名称,而后再组装成绝对路径,读取资源播放

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);
        }
      });
    },

声音音量控制

音量控制须要经过监听input的键入事件,获取到range的值,而后经过设置样式background-image,动态计算百分比,而后调用wavesurfer的setVolume方法调节音量

: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));
    },

自定义标题栏

我的以为系统自带的菜单栏太丑了,就给设置了无边框再本身加上最小化,关闭的功能。最小化,关闭是经过ipc通讯,渲染进程监听到有点击操做后,通知主进程进行相应的操做。

渲染进程

close() {
      ipcRenderer.send("close");
    },
    minimize() {
      ipcRenderer.send("minimize");
    }

主进程

ipcMain.on("close", () => {
  win.close();
  app.quit();
});

ipcMain.on("minimize", () => {
  win.minimize();
});

打开多个实例的问题

在实际测试的过程当中发现会出现,打开一首新的音乐播放,就会出现从新开一个实例的现象,不能实现覆盖播放,后面查阅资料发现electron有一个second-instance事件,能够监听是否打开了第二个实例。当第二个实例被执行而且调用 app.requestSingleInstanceLock()") 时,这个事件将在应用程序的首个实例中触发,而且会返回第二个实例的相关信息,而后经过主进程通知渲染进程,告知渲染进程第二个实例的本地绝对路径,渲染进程接收到信息后,立马加载第二个实例的资源。app.requestSingleInstanceLock(),表示应用程序实例是否成功取得了锁。 若是它取得锁失败,能够假设另外一个应用实例已经取得了锁而且仍旧在运行,因此能够直接关闭掉,这样就避免了打开多个实例的问题

主进程

const gotTheLock = app.requestSingleInstanceLock();
if (gotTheLock) {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    // 监听是否有第二个实例,向渲染进程发送第二个实例的本地路径
    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();
}

渲染进程

ipcRenderer.on("path", (event, arg) => {
    const newOriginPath = arg;

    // console.log(newOriginPath);
    this.loadMusic(newOriginPath);
  });

自动更新

需求的原由是,在很兴奋的给女友成品的时候,尴尬的被女友试出不少bug(捂脸ing),而后频繁的修改打包,而后经过私发传给她。特别麻烦,因此这个需求很急迫。最后查了资料,经过electron-updater实现了这个需求.

安装electron-updater

yarn add electron-updater

发布设置

electronBuilder: {
      builderOptions: {
        publish: ['github']
      }
    }

主进程监听

autoUpdater.on("checking-for-update", () => {});
autoUpdater.on("update-available", info => {
  dialog.showMessageBox({
    title: "新版本发布",
    message: "有新内容更新,稍后将从新为您安装",
    buttons: ["肯定"],
    type: "info",
    noLink: true
  });
});

autoUpdater.on("update-downloaded", info => {
  autoUpdater.quitAndInstall();
});

生成Github Access Token
由于是用github做为更新站,因此本地须要相应的操做权限,去这里生成token,戳这,生成后,在powershell中设置

[Environment]::SetEnvironmentVariable("GH_TOKEN","<YOUR_TOKEN_HERE>","User")
# 例如 [Environment]::SetEnvironmentVariable("GH_TOKEN","sdfdsfgsdg14463232","User")

打包上传Github

yarn electron:build -p always

完成上面步骤后软件会自动上传打包后的文件到release,而后编辑下release就能够直接发布了,软件是基于版本号更新的,因此记得必定要改版本号

从零开始-结束

做为程序猿最开心的事莫过于获得女友的夸奖,虽然这是一个小程序,实现难度也不高,可是最后作出最小可用的版本呈如今女友面前的时候,看到女盆友感动的眼神,我想,这应该是我做为程序猿惟一感到欣慰的时候。软件还有不少能够改进的地方,源码在此,戳这里Github