Creating Custom Music Player Desktop App With Angular And ElectronJS

Creating Custom Music Player Desktop App With Angular And ElectronJS


Date: May 7, 2020 Author: Nenad Borovčanin

In this article I will show you how to build a music player native desktop application using Angular and Electron. This player will be compatible with Windows, Linux, and MacOS.

What is ElectronJS

Electron is an open source library developed by GitHub for building cross-platform desktop applications with HTML, CSS, and JavaScript. Electron accomplishes this by combining Chromium and Node.js into a single runtime and apps can be packaged for Mac, Windows, and Linux.

Set Up the Angular application

First of all we will generate new Angular project using Angular CLI:

ng new music-player

After project is created successfully, cd into the directory and install Electron with the following npm commad:

npm install --save-dev electron@latest

In root of your project, create file main.js:

touch main.js

Let’s put some code in it. The code below will simply create a GUI window and load the index.html file that should be available under the dist folder after we build our Angular application.

const {app, BrowserWindow} = require('electron')
const url = require("url");
const path = require("path");

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 640,
    height: 640,
    webPreferences: {
      nodeIntegration: true
    }
  });

  mainWindow.loadURL(
    url.format({
      pathname: path.join(__dirname, `/dist/index.html`),
      protocol: "file:",
      slashes: true
    })
  );
  // Open the DevTools.
 // mainWindow.webContents.openDevTools();

  mainWindow.on('closed', function () {
    mainWindow = null
  })
}

app.on('ready', createWindow)

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
});

app.on('activate', function () {
  if (mainWindow === null) createWindow()
});

Now add the main key to set the main.js file as the entry point in package.json.

Also add "start:electron" script to scripts:

"name": "music-player",
"version": "0.0.0",
"main": "main.js",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"start:electron": "ng build --base-href ./ && electron ."
},

Now open angular.json and make sure that outputPath has the value dist:

"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",

We will use Bootstrap for UI. So, let’s install it and its dependencies:

nenad@hp:~/playground/music-player$ npm install --save bootstrap jquery popper

Openindex.html, and load the libraries (this can also be done trough angular.json):

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MusicPlayer</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"/>

  <script>let $ = require('jquery');</script>
  <script>require('popper.js');</script>
  <script>require('bootstrap');</script>
  
</head>
<body>
<app-root></app-root>
</body>
</html>

I will add a few mp3songs in the assets folder.

./music-player/src/assets$ tree
.
├── Evanescence-Bring Me To Life(with lyrics).mp3
├── logo.png
├── Numb (Official Video) - Linkin Park.mp3
└── System Of A Down - Toxicity (Official Video).mp3

Create file model.tsand export song interface:

export interface ISong {
id: number;
title: string;
path: string;
}

Now add logic to app.component.ts:

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { ISong } from './model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  currentProgress$ = new BehaviorSubject(0);
  currentTime$ = new Subject();

  @ViewChild('player', {static: true}) player: ElementRef;

  songs: ISong[] = [];

  audio = new Audio();
  isPlaying = false;
  activeSong;
  durationTime: string;

  ngOnInit() {
    this.songs = this.getListOfSongs();

    this.player.nativeElement.src = this.songs[0];
    this.player.nativeElement.load();
    this.activeSong = this.songs[0];
    this.isPlaying = false;
  }

  playSong(song): void {
    this.durationTime = undefined;
    this.audio.pause();

    this.player.nativeElement.src = song.path;
    this.player.nativeElement.load();
    this.player.nativeElement.play();
    this.activeSong = song;
    this.isPlaying = true;
  }

  onTimeUpdate() {

    // Set song duration time
    if (!this.durationTime) {
      this.setSongDuration();
    }

    // Emit converted audio currenttime in user friendly ex. 01:15
    const currentMinutes = this.generateMinutes(this.player.nativeElement.currentTime);
    const currentSeconds = this.generateSeconds(this.player.nativeElement.currentTime);
    this.currentTime$.next(this.generateTimeToDisplay(currentMinutes, currentSeconds));


    // Emit amount of song played percents
    const percents = this.generatePercentage(this.player.nativeElement.currentTime, this.player.nativeElement.duration);
    if (!isNaN(percents)) {
      this.currentProgress$.next(percents);
    }

  }

  // Play song that comes after active song
  playNextSong(): void {
    const nextSongIndex = this.songs.findIndex((song) => song.id === this.activeSong.id + 1);

    if (nextSongIndex === -1) {
      this.playSong(this.songs[0]);
    } else {
      this.playSong(this.songs[nextSongIndex]);
    }
  }

  // Play song that comes before active song
  playPreviousSong(): void {
    const prevSongIndex = this.songs.findIndex((song) => song.id === this.activeSong.id - 1);
    if (prevSongIndex === -1) {
      this.playSong(this.songs[this.songs.length - 1]);
    } else {
      this.playSong(this.songs[prevSongIndex]);
    }
  }

  // Calculate song duration and set it to user friendly format, ex. 01:15
  setSongDuration(): void {
    const durationInMinutes = this.generateMinutes(this.player.nativeElement.duration);
    const durationInSeconds = this.generateSeconds(this.player.nativeElement.duration);

    if (!isNaN(this.player.nativeElement.duration)) {
      this.durationTime = this.generateTimeToDisplay(durationInMinutes, durationInSeconds);
    }
  }

  // Generate minutes from audio time
  generateMinutes(currentTime: number): number {
    return Math.floor(currentTime / 60);
  }

  // Generate seconds from audio time
  generateSeconds(currentTime: number): number | string {
    const secsFormula = Math.floor(currentTime % 60);
    return secsFormula < 10 ? '0' + String(secsFormula) : secsFormula;
  }

  generateTimeToDisplay(currentMinutes, currentSeconds): string {
    return `${currentMinutes}:${currentSeconds}`;
  }

  // Generate percentage of current song
  generatePercentage(currentTime: number, duration: number): number {
    return Math.round((currentTime / duration) * 100);
  }

  onPause(): void {
    this.isPlaying = false;
    this.currentProgress$.next(0);
    this.currentTime$.next('0:00');
    this.durationTime = undefined;
  }

  getListOfSongs(): ISong[] {
    return [
      {
        id: 1,
        title: 'Evanescence-Bring Me To Life(with lyrics).mp3',
        path: './assets/Evanescence-Bring Me To Life(with lyrics).mp3'
      },
      {
        id: 2,
        title: 'Numb (Official Video) - Linkin Park.mp3',
        path: './assets/Numb (Official Video) - Linkin Park.mp3'
      },
      {
        id: 3,
        title: 'System Of A Down - Toxicity (Official Video).mp3',
        path: './assets/System Of A Down - Toxicity (Official Video).mp3'
      }
    ];
  }
}

Also add code to app.component.html:

<div class="ui container center-screen">
  <div class="row justify-content-center">
      <div class="col-12">
        <img src="'../../assets/logo.png" alt="..." class="img-thumbnail">
      </div>
  </div>
  <br>
  <br>
  <div class="row">
    <div class="col-12"><h4>{{ activeSong?.title }}</h4></div>
  </div>
  <br>
  <div class="row justify-content-center">
    <div class="col-12">
      <div class="container">
        <div class="row">
          <div class="col-6 text-left">
            {{ currentTime$ | async }}
          </div>
          <div class="col-6 text-right" *ngIf="player?.duration">
            {{ durationTime }}
          </div>
        </div>
      </div>
      <div class="progress">
        <div class="progress-bar bg-info" role="progressbar"
             [ngStyle]="{'width': (currentProgress$ | async) + '%'}"
[attr.aria-valuenow]="(currentProgress$ | async) "
              aria-valuemin="0" aria-valuemax="100">
       {{ '<<' }}
   <button type="button" class="btn btn-outline-dark" *ngIf="isPlaying" (click)="player.pause()">Stop</button>   <button type="button" class="btn btn-outline-dark" *ngIf="!isPlaying" (click)="playSong(activeSong)">Play</button>   <button type="button" class="btn btn-outline-dark" (click)="playNextSong()"><b>{{ '>>' }}</b></button> </div>
           

Now let’s run command to start electron

npm run start:electron

Result:

You now have a native desktop music player! The remaining step is to simply build it for the platforms you want to support (Windows, Mac, Linux).

If someone wants to play with this or add some features, github link :
https://github.com/nenadb97/angular-electron-music-player

Thanks for reading 🙂