Electron+vue build a local player from scratch

Why?

My girlfriend works in the late stage of audio. She usually collects some audio music and needs to see the spectrum waveform of audio. It's very inconvenient to play music and see the waveform every time with a large software such as au. Seeing her so hard, as a program ape, I feel very sad. Therefore, there is such a small software. The technologies involved in the software are mainly electron, Vue and node. The waveform is mainly displayed through wavesurfer Generate.

Start from scratch - build the project

The project is built through vue scaffold, so cli tools need to be installed. If installed, you can skip this step

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

After installation, the project is built through scaffolding

vue create music

vue needs to be integrated with electron. There are mature vue plug-ins in the community, Vue CLI Plugin Electron Builder.

vue add electron-builder

Lazy people can go directly to clone. My built shelf can be developed directly, Poke here.

Start from scratch - project development

First, clarify the functional requirements of the player, mainly these

  • Without adding a file directory, load any audio files in the local file system and directly call the player to play
  • Previous and subsequent functions
  • Sound volume control
  • Customize software window

How to associate playback

How to realize associated playback? Because I'm not very familiar with electron, I checked the information of electron for a long time and finally found the configuration item. I need to configure fileAssociations

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

After configuration, through the electron ic Open file event , get the local path of the open audio file. For windows, you need to pass process Argv to get the file path.

const filePath = process.argv[1];

How to load local audio files

After getting the local path of the file through the configuration in the previous step, the next step is to read the information of the audio file through the path. Because the audio plug-in cannot resolve the absolute path, it needs to go through the file system of node fs.readFileSync Read the buffer information of the file.

let buffer = fs.readFileSync(diskPath); //Read the file and convert the cache

After reading, you need to convert the buffer into a node readable stream

const stream = this.bufferToStream(buffer);//Convert buffer data into node readable stream

Conversion method bufferToStream

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

After converting the audio stream into a blob object, you need to convert the audio stream into a blob object to load it method

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

Transfer to blob

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

      //Convert blob objects into blob links
      let filePath = window.URL.createObjectURL(fileUrl);
      // console.log(filePath);
      this.wavesurfer.load(filePath);

      // Auto play
      this.wavesurfer.play();
      this.playing = true;
    })
    .catch(err => {
      console.log(err);
    });

In this way, the local file can be loaded and played

Previous song next song function

The function of the previous song and the next song here is based on the absolute path of the file obtained above, through the path module of node, path.dirname Gets the parent directory of the file.

const dirPath = path.dirname(diskPath);

Then pass fs.readdir Reading all files in the directory will return a file name array, find the subscript of the file being played in the directory, judge the name of the previous song and the next song through the array subscript, and then assemble it into an absolute path to read the playing resources

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

Sound volume control

The volume control needs to obtain the range value by listening to the input typing event, and then set the style background-image , dynamically calculate the percentage, and then call the setVolume method of wavesurfer to adjust the volume

:style="`background-image:linear-gradient( to right, ${fillColor}, ${fillColor} ${percent}, ${emptyColor} ${percent})`"

Change volume changeVol event

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

Custom title block

Personally, I think the menu bar provided by the system is too ugly, so I set no border, and add the function of minimizing and closing. Minimize and close is through ipc communication. After the rendering process listens to the click operation, it notifies the main process to carry out the corresponding operation.

Rendering Progress

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

Main process

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

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

Open multiple instances of the problem

During the actual test, it is found that if you open a new music, you will reopen an instance, which can not be played over. Later, it is found that there is a problem in electron by consulting the data second-instance Event, you can listen whether the second instance is opened. When the second instance is executed and called app.requestSingleInstanceLock() "), this event will be triggered in the first instance of the application, and the relevant information of the second instance will be returned. Then, the main process will notify the rendering process of the local absolute path of the second instance through the main process. After receiving the information, the rendering process will immediately load the resources of the second instance. app.requestSingleInstanceLock() Indicates whether the application instance successfully acquired the lock. If it fails to obtain the lock, it can be assumed that another application instance has obtained the lock and is still running, so it can be closed directly, which avoids the problem of opening multiple instances

Main process

const gotTheLock = app.requestSingleInstanceLock();
if (gotTheLock) {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    // Monitor whether there is a second instance and send the local path of the second instance to the rendering process
    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();
}

Rendering Progress

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

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

Automatic update

The reason for the demand is that when I am very excited to give my girlfriend finished products, I am embarrassed to be tried out by my girlfriend with many bug s (cover ing my face), and then frequently modify and pack them, and then send them to her through private email. Especially troublesome, so this demand is urgent. Finally, I checked the data and realized this requirement through electron Updater

Installing the electron Updater

yarn add electron-updater

Publish settings

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

Main process listening

autoUpdater.on("checking-for-update", () => {});
autoUpdater.on("update-available", info => {
  dialog.showMessageBox({
    title: "New version release",
    message: "There are new content updates, which will be reinstalled for you later",
    buttons: ["determine"],
    type: "info",
    noLink: true
  });
});

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

Generate Github Access Token
Because github is used as the update station, corresponding operation permissions are required locally. Go here to generate a token, Poke this , set in powershell after generation

[Environment]::SetEnvironmentVariable("GH_TOKEN","<YOUR_TOKEN_HERE>","User")
# For example [environment]: setenvironmentvariable ("gh_token", "sdfdsfgsdg14463232", "user")

Package and upload Github

yarn electron:build -p always

After completing the above steps, the software will automatically upload the packaged file to the release, and then edit the release to release directly. The software is updated based on the version number, so remember to change the version number

Start from zero - end

As a program ape, the happiest thing is to be praised by my girlfriend. Although this is a small program and its implementation is not difficult, when I finally make the smallest available version and present it in front of my girlfriend, I see the moved eyes of my girlfriend. I think this should be the only time when I feel happy as a program ape. There are still many improvements in the software. The source code is here, Poke here, Github

Tags: node.js Front-end Vue.js Electron

Posted by jamesgrayking on Mon, 09 May 2022 21:23:01 +0300