TL;DR: In this article, you will learn how to develop a mobile audio player app using Ionic framework and Angular. You will handle audio operations using RxJS and Observables, and you will also explore how you can manage application's state with NgRx. To secure your application, you will use Auth0. If needed, you can find the final code on this GitHub repository.

Introduction

Creating an audio player is always an intimidating task. Specially if you think about managing media's state, reacting to media events, and reflecting these changes correctly on the UI (User Interface). So, in this article, you will use Angular and Ionic (with some other libraries) to easily solve these issues.

To handle media playback in a reactive way, you will adapt JavaScript's Audio object with a RxJS Observable and you will use the NgRx store to manage the state of your audio player.

Besides that, you will also use Auth0 to secure your mobile app and, in a subsequent article, you will learn how to create a secure backend to provide the list of music files to your app (for now, you will use a mock service with static data).

"Let's use @Ionicframework, @angular, RxJS, and NgRx to build a mobile audio player app."

Prerequisites for Ionic Development

Since you are going to make a mobile application, you will need to have the required SDKs for building the app. In this article, you will use Cordova to package your Ionic app into native mobile packages.

The following sections show the steps you will need to follow before starting the development of your application.

Install Native SDKs for iOS

For the iOS platform, you will need a Mac OS X environment and Xcode installed on it. For more information about configuring native SDKs in a Mac OS X environment, check this reference. After installing Xcode, you will also need some command-line tools and the ios-deploy tool to run a simulator.

To install these tools, proceed as follows:

  • From the command line, run xcode-select --install to install the Xcode command-line tool.
  • Then, run npm install -g ios-deploy to install the ios-deploy tool.

If you don't have Node.js and NPM installed in your machine, please, check the instruction on this reference.

Install Native SDKs for Android

For Android applications, you will need to have Android SDKs and some tools. The steps below briefly explain how to install these SDKs and tools in your environment. However, if you need more information, you can check this link for a more thorough explanation on how to install everything:

  • JDK: You will need to have a JDK installed and the JAVA_HOME environment variable pointing to your JDK installation.
  • Gradle: You will also need to install Gradle and add it to the PATH variable in your environment variables.
  • Android SDK: Most importantly, you will need Android SDKs to generate apk files for your app. So, install the Android Studio IDE and, using the sdkmanager, install these:
    1. Android Platform SDK;
    2. build-tools for that SDK version;
    3. and Android Support Repository.

After these, you will need to set the ANDROID_HOME environment variable to your Android SDK location. It's also recommended to add Android SDK's tools, tools/bin, and platform-tools directories to PATH variable.

Install Node.js and Tools

As already mentioned, you will need to install Node.js in your development machine. So, if you haven't done so yet, go to the download page of Node.js and follow the instructions there.

After installing it, you will need to install Cordova CLI and Ionic CLI via npm:

npm install -g ionic cordova

Scaffolding the Ionic App

After installing all the environment dependencies, you can focus on scaffolding your Ionic app. To do this, issue the following command on a terminal:

ionic start audio-player blank

This command will ask you two questions:

  1. Would you like to integrate your new app with Cordova to target native iOS and Android? (y/N): You can input y (yes) as you are going to build the app for mobile devices.
  2. Install the free Ionic Pro SDK and connect your app? (Y/n): Press n because you won't really need to use any Ionic Pro feature in this tutorial.

Running the Application

Before continuing, make sure you can start your application on some mobile device or emulator.

For example, if you are on a Mac OS X environment and you want to use an emulator to test your application, you can simply run:

# for the iOS app
ionic cordova run ios -lc

Note: -lc above means that you want Ionic to spin up a server to live-reload www files (the l) and to print out console logs to terminal (the c).

For reference, these are the other commands that you can use when aiming your current development machine (i.e. a browser on it) or Android:

# serve locally
ionic serve

# for the android app
ionic cordova run android

Installing Project Dependencies

Having confirmed that you can run the basic app in some mobile device, you can start by installing the dependencies. To build your mobile audio player, you will use the following NPM libraries:

  • @angular/animations: a package to improve your app UX by adding some animations;
  • @ngrx/store: a library built to integrate RxJS and Angular applications to help you manage the state of your apps;
  • moment.js: a library that helps manipulating dates and times in JavaScript;
  • auth0-js: the official Auth0 library for JavaScript apps;
  • @auth0/cordova: the official Auth0 library for Cordova apps;
  • rxjs: a reactive programming library for JavaScript;
  • rxjs-compat: a package to get backward compatibility with RxJS previous to version 6;

To install these libraries, you can use the following commands:

# make sure you are in the project root
cd audio-player

# install all libs
npm install --save @angular/animations @ngrx/store moment auth0-js @types/auth0-js @auth0/cordova rxjs@6.2.1 rxjs-compat@6.2.1

Note: In the command above, you installed both rxjs@6.2.1 and rxjs-compat@6.2.1 because Ionic (at least, at the time of writing) ships with Angular 5 and because Angular 5 uses RxJS 5 APIs.

Creating an Ionic Service to Manage the Playback

After installing your app's dependencies, you can start working on the playback feature.

Creating an RxJS Observable

The Observable that you are going to create is the central piece of your application. RxJS comes with a helper function named create to help you create custom observables. It takes subscribe function as an input.

Observable.create(subscribe): Observable<any>;

This subscribe function takes an observer object and returns a function. Observer objects provide three methods: next, error, and complete.

  1. To emit a value, you can call the observer.next method with the desired value.
  2. In case of an error, you can use the observer.error function to throw the error and make the observable stop.
  3. If you no longer need the observer and there are no more values to emit, you can call the observer.complete method.

Also, calling Observable.create will return an Observable to which you can subscribe via the subscribe method. This method returns a function that you can call when you want to unsubscribe from the observable.

Don't be confused with Observable.create(subscribe) and Observable.subscribe(). Former subscribe function is an input to Observable.create, which is sort of like blueprint of an observable, and latter is the one which invokes the execution of an observable.

In your audio player app, you are going to create an observable to get notifications about media events like playing, pause, timeupdate, and so on. So, basically, you will listen to the media event's of Audio() inside the observable and then notify the rest of the app via the observer.next method.

Now that you understand why do you need an observable, you can start by creating a service in your Ionic app:

ionic generate provider audio

This will generate a service in a file called audio.ts under ./src/providers/audio/ and this service will be added to NgModule in app.module.ts. Replace the contents of the audio.ts file with:

import {Injectable} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import * as moment from 'moment';

@Injectable()
export class AudioProvider {
  private stop$ = new Subject();
  private audioObj = new Audio();

  constructor() { }

  private streamObservable(url) {
    let events = [
      'ended', 'error', 'play', 'playing', 'pause', 'timeupdate', 'canplay', 'loadedmetadata', 'loadstart'
    ];

    const addEvents = function(obj, events, handler) {
      events.forEach(event => {
        obj.addEventListener(event, handler);
      });
    };

    const removeEvents = function(obj, events, handler) {
      events.forEach(event => {
        obj.removeEventListener(event, handler);
      });
    };

    return Observable.create(observer => {
      // Play audio
      this.audioObj.src = url;
      this.audioObj.load();
      this.audioObj.play();

      // Media Events
      const handler = (event) => observer.next(event);
      addEvents(this.audioObj, events, handler);

      return () => {
        // Stop Playing
        this.audioObj.pause();
        this.audioObj.currentTime = 0;

        // Remove EventListeners
        removeEvents(this.audioObj, events, handler);
      };
    });
  }
}

Now, whenever you want to play a new audio file, you will create this observable and listen to all these media events. You will do this via a new method called playStream() that you are going to add to the AudioProvider class:

// ... import statements ...

export class AudioProvider {
  // ... constructors and other methods ...

  playStream(url) {
    return this.streamObservable(url).pipe(takeUntil(this.stop$));
  }
}

It's important to notice that you are automatically unsubscribing from this observable if this.stop$ emits any value.

Wrapping Up the AudioProvider Service

Now that you have the basis of the AudioProvider service, you can develop the rest of its methods: play, pause, stop, seekTo, and formatTime. As their implementation is self-explanatory, you can simply add these five methods to the AudioProvider service as shown below:

// ... import statements ...

export class AudioProvider {

  // ... constructors and other methods ...

  play() {
    this.audioObj.play();
  }

  pause() {
    this.audioObj.pause();
  }

  stop() {
    this.stop$.next();
  }

  seekTo(seconds) {
    this.audioObj.currentTime = seconds;
  }

  formatTime(time, format) {
    return moment.utc(time).format(format);
  }
}

Reading the Music Files

After creating the audio service for the playback features, you will need to create a service to get a list of files. To do so, you can create a cloud service using Ionic:

ionic generate provider cloud

This command will generate a service in a file called cloud.ts under ./src/providers/cloud. Now, replace the contents of this file with:

import { Injectable } from '@angular/core';
import { of } from 'rxjs';

@Injectable()
export class CloudProvider {
  files:any = [
    { url: 'https://ia801504.us.archive.org/3/items/EdSheeranPerfectOfficialMusicVideoListenVid.com/Ed_Sheeran_-_Perfect_Official_Music_Video%5BListenVid.com%5D.mp3', 
      name: 'Perfect by Ed Sheeran'
    },
    {
      url: 'https://ia801609.us.archive.org/16/items/nusratcollection_20170414_0953/Man%20Atkiya%20Beparwah%20De%20Naal%20Nusrat%20Fateh%20Ali%20Khan.mp3',
      name: 'Man Atkeya Beparwah by Nusrat Fateh Ali Khan'
    },
    { url: 'https://ia801503.us.archive.org/15/items/TheBeatlesPennyLane_201805/The%20Beatles%20-%20Penny%20Lane.mp3',
      name: 'Penny Lane by The Beatles'
    }
  ];
  getFiles() {
   return of(this.files);
  }
}

The getFiles method above basically mocks an HTTP request by returning an Observable with a static files object.

Managing Ionic App's State with NgRx Store

To help you manage the state of your application, you will take advantage of the NgRx Store library. This store is based on Redux, which is very famous in the React world for managing state, and integrates Redux concepts with RxJS.

If you don't know what Redux is (or how it works), here it goes a brief explanation around it:

In Redux, the state is managed in a central place. What this means is that you have just one object which stores the current state of your whole application. If at any point, you want to update this state, you need to dispatch an action to a function known as reducer. This reducer is responsible for understanding the action and generating a new state based on the action type and data.

Creating a Reducer with NgRx Store

By default, the NgRx Action interface exposes only one property: the type. As you will need to send some information along with the type of your actions, you are going to extend the NgRx Action interface to suit your needs.

So, to define this interface extension, you will create a file named store.ts inside a new directory called store (under ./src/providers/) and add the following code to it:

import {Action} from '@ngrx/store';

export interface MediaAction extends Action {
  type: string;
  payload?: any;
}

Then, you will create different actions for the different media events (like canplay, playing, and so on). As such, update the store.ts file as follows:

// ... import statement and MediaAction interface ...

export const CANPLAY = 'CANPLAY';
export const LOADEDMETADATA = 'LOADEDMETADATA';
export const PLAYING = 'PLAYING';
export const TIMEUPDATE = 'TIMEUPDATE';
export const LOADSTART = 'LOADSTART';
export const RESET = 'RESET';

After that, you will be able to implement the reducer function that receives and treats instances of MediaAction:

// ... import, MediaAction, and consts ...

export function mediaStateReducer(state: any, action: MediaAction) {
  let payload = action.payload;
  switch (action.type) {
    case CANPLAY:
      state = Object.assign({}, state);
      state.media.canplay = payload.value;
      return state;
    case LOADEDMETADATA:
      state = Object.assign({}, state);
      state.media.loadedmetadata = payload.value;
      state.media.duration = payload.data.time;
      state.media.durationSec = payload.data.timeSec;
      state.media.mediaType = payload.data.mediaType;
      return state;
    case PLAYING:
      state = Object.assign({}, state);
      state.media.playing = payload.value;
      return state;
    case TIMEUPDATE:
      state = Object.assign({}, state);
      state.media.time = payload.time;
      state.media.timeSec = payload.timeSec;
      return state;
    case LOADSTART:
      state.media.loadstart = payload.value;
      return Object.assign({}, state);
    case RESET:
      state = Object.assign({}, state);
      state.media = {};
      return state;
    default:
      state = {};
      state.media = {};
      return state;
  }
}

Within each case statement in the code above, you are generating a new state to your app. It's important to note that, as NgRx works with immutable objects, you need to create a new state object instead of updating the existing one. In this case, you are using Object.assign to create the new state object based on the current one.

Now, to register your reducer in your Ionic app, open the app.module.ts file and update it as follows:

// ... other import statements ...
import { StoreModule } from '@ngrx/store';
import { mediaStateReducer } from '../providers/store/store';

@NgModule({
  // ... declarations ...
  imports: [
    // ... other imported modules ...
    StoreModule.forRoot({
      appState: mediaStateReducer
    }),
    IonicModule.forRoot(MyApp)
  ],
  // ... bootstrap, entryComponents, and providers ...
})
export class AppModule {}

Now, you will be able to access the current state using the appState key anywhere in your Ionic application.

Authentication on Ionic Apps

To develop a secure app, you are going to rely on Auth0 to handle the authentication of your users. As such, you can sign up for a free Auth0 account here. Then, you will need to set up an Auth0 Application to represent your mobile app.

Installing Dependencies

To secure your Ionic app with Auth0, you will have to install some Cordova plugins:

# replace {YOUR_PACKAGE_ID} with your app identifier and
# replace YOUR_AUTH0_DOMAIN with your Auth0 Domain
ionic cordova plugin add cordova-plugin-customurlscheme --variable URL_SCHEME={YOUR_PACKAGE_ID} --variable ANDROID_SCHEME={YOUR_PACKAGE_ID} --variable ANDROID_HOST={YOUR_AUTH0_DOMAIN} --variable ANDROID_PATHPREFIX=/cordova/{YOUR_PACKAGE_ID}/callback
ionic cordova plugin add cordova-plugin-safariviewcontroller

Note: You will have to replace {YOUR_PACKAGE_ID} above with the package id of your Ionic app. You can find this information in the config.xml file. There, you will see something like <widget id="io.ionic.starter" .... In this case, your package id would be io.ionic.starter.

Note: Besides that, you will also need to replace {YOUR_AUTH0_DOMAIN} with your Auth0 domain. When creating your Auth0 account, you chose a subdomain like ionic-audio-player, or your-name, etc. In that case, your Auth0 domain would be ionic-audio-player.auth0.com. You can also find your subdomain on the upper right corner of your Auth0 dashboard, as shown in this screenshot:

Find your Auth0 subdomain.

Set Up an Auth0 Application

  1. Go to your Auth0 Dashboard and click the "create a new application" button.
  2. Name your new app (e.g. "Ionic Audio Player"), select "Native App" as its type, and click the "Create" button.
  3. In the Settings tab of your new Auth0 app, add file://*, http://localhost:8080 in the Allowed Origins (CORS) box.
  4. Still in the Settings tab, add YOUR_PACKAGE_ID://YOUR_AUTH_DOMAIN/cordova/YOUR_PACKAGE_ID/callback, to the Allowed Callback URLs.
  5. Add http://localhost:8080 to the Allowed Logout URLs.
  6. Click the "Save Changes" button.

Note: If running your app with the live reload feature, you might need to add an URL different than http://localhost:8080 to the Allowed Origins (CORS) box. When running your app, check the allow-navigation property of the config.xml file to find out the correct URL. For example: http://192.168.0.14:8100.

Note: On step 4, you will need to replace YOUR_PACKAGE_ID and YOUR_AUTH_DOMAIN with your own data (the same as used while installing your project dependencies: e.g. io.ionic.starter and ionic-audio-player.auth0.com).

Configuring Auth0 on Ionic

Now, you will need to create a file called auth.config.ts in a new directory: ./src/providers/auth0/. Inside that file, you can add the following code:

export const AUTH_CONFIG = {
  clientID: 'YOUR_CLIENT_ID',// Needed for Auth0 (capitalization: ID)
  clientId: 'YOUR_CLIENT_ID', // needed for Auth0Cordova (capitalization: Id)
  domain: 'YOUR_AUTH_DOMAIN',
  packageIdentifier: 'your.app.id' // found on config.xml widget ID (e.g., com.auth0.ionic)
};

This list explains what these values mean:

  • clientID and clientId: They are the Client Id property available in your Auth0 Application (the one created above).
  • domain: It's your Auth0 Domain.
  • packageIdentifier: It's the widget ID of your Ionic application. You have this in the config.xml file of your application, as described before.

Before continuing, make sure to replace YOUR_CLIENT_ID, YOUR_AUTH_DOMAIN, and your.app.id with your own data.

Auth Service

After creating your Auth0 account and defining the auth.config.ts file, you will need to define an authentication service in your Ionic app. As such, create a new file called auth.service.ts in the same ./src/providers/auth0/ directory and add the following content to it:

import {Injectable, NgZone} from '@angular/core';
import {Storage} from '@ionic/storage';
import {Subject} from 'rxjs';
// Import AUTH_CONFIG, Auth0Cordova, and auth0.js
import {AUTH_CONFIG} from './auth.config';
import Auth0Cordova from '@auth0/cordova';
import * as auth0 from 'auth0-js';

@Injectable()
export class AuthService {
  Auth0 = new auth0.WebAuth(AUTH_CONFIG);
  Client = new Auth0Cordova(AUTH_CONFIG);
  accessToken: string;
  user: any;
  loggedIn: boolean;
  loading = true;
  isLoggedIn$ = new Subject();

  constructor(public zone: NgZone, private storage: Storage) {
    this.storage.get('profile').then(user => (this.user = user));
    this.storage.get('access_token').then(token => (this.accessToken = token));
    this.storage.get('expires_at').then(exp => {
      this.loggedIn = Date.now() < JSON.parse(exp);
      this.loading = false;
      this.isLoggedIn$.next(this.loggedIn);
    });
  }

  login() {
    return new Promise((resolve, reject) => {
      this.loading = true;
      const options = {
        scope: 'openid profile offline_access',
      };
      // Authorize login request with Auth0: open login page and get auth results
      this.Client.authorize(options, (err, authResult) => {
        if (err) {
          this.loading = false;
          reject(err);
        } else {
          // Set access token & id token
          this.storage.set('id_token', authResult.idToken);
          this.storage.set('access_token', authResult.accessToken)
            .then(() => {
              // Set logged in
              this.loading = false;
              this.loggedIn = true;
              this.isLoggedIn$.next(this.loggedIn);
              resolve();
            });
          this.accessToken = authResult.accessToken;
          // Set access token expiration
          const expiresAt = JSON.stringify(
            authResult.expiresIn * 1000 + new Date().getTime()
          );
          this.storage.set('expires_at', expiresAt);
          // Fetch user's profile info
          this.Auth0.client.userInfo(this.accessToken, (err, profile) => {
            if (err) {
              throw err;
            }
            this.storage
              .set('profile', profile)
              .then(val => this.zone.run(() => (this.user = profile)));
          });
        }
      });
    });
  }

  logout() {
    this.storage.remove('profile');
    this.storage.remove('access_token');
    this.storage.remove('expires_at');
    this.storage.remove('id_token');
    this.accessToken = null;
    this.user = {};
    this.loggedIn = false;
    this.isLoggedIn$.next(this.loggedIn);
  }
}

To better understand how the code above works, take a look into the following explanation:

  • accessToken: This is the JWT Token that your users will get from Auth0. These tokens are used to identify the user.
  • user: This property holds the user data like email, firstname, lastname, and so on.
  • loggedIn: This boolean holds the authentication state of the user.
  • isLoggedIn$: This is a RxJS Subject. Think of this as the reactive version of the loggedIn property. You will use it in your Angular Component to get user's authentication state.

Now, take a look at the methods of the service above:

  • constructor(): In the constructor, you check if the user is previously authenticated or not. Based on it, you set the value of the this.user, this.accessToken, and this.loggedIn properties.
  • login(): In login method, you authorize the user and, if the user is successfully authenticated, you fetch their profile information. You save this information in the permanent storage via @ionic/store and also set the appropriate properties of the service to reflect the authentication state.
  • logout(): In logout method, you remove all the user information from permanent storage and set the properties of service to reflect the logout state.

Auth Callback

To handle the redirection from Auth0 after authentication, you will have to update the app.component.ts file, as shown here:

import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import Auth0Cordova from '@auth0/cordova';

import { HomePage } from '../pages/home/home';
@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage:any = HomePage;

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) {
    platform.ready().then(() => {
      statusBar.styleDefault();
      splashScreen.hide();

      // Redirect back to app after authenticating
      (window as any).handleOpenURL = (url: string) => {
        Auth0Cordova.onRedirectUri(url);
      };
    });
  }
}

Developing the Audio Player UI on Ionic

So far, you have written code that is not related to the front-end and user interface (UI) of your application. In this section, you will design the UI and its behavior. In the end, your application will look like this:

Ionic audio player demo app UI

The Audio Player HTML

Inside the ./src/pages/home/ directory, you will find the home.html file. In this file, you will add some HTML to define your player. As you will see, on the top, you will have a navigation bar which contains the name of the application and a log out button. This button will be shown if the user is logged in.

Besides the header, you will have the ion-content with a login button, your app's logo, and the list of media files:

<ion-header>
  <ion-navbar color="primary">
    <ion-title>Audio Player</ion-title>
    <ion-buttons end>
      <button *ngIf="loggedIn" ion-button icon (click)="logout()">Logout</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <p *ngIf="auth.loading" text-center>Loading...</p>
  <ng-template [ngIf]="!auth.loading || !loggedIn">
    <div padding id="app-section" text-center>
      <ion-icon color="primary" name="musical-notes"></ion-icon>
      <h2 id="app-title">Audio Player</h2>
      <button outline ion-button block color="primary" *ngIf="!loggedIn" (click)="login()">Log In</button>
    </div>
  </ng-template>

  <ion-list *ngIf="files.length && loggedIn">
    <ion-list-header>Hello {{auth.user?.name}}</ion-list-header>
    <ng-container *ngFor="let file of files; let i = index">
      <ion-item text-wrap (click)="openFile(file, i)">
        <ion-icon color="primary" item-start name="musical-note"></ion-icon>{{ file.name }}
        <p item-end *ngIf="currentFile.index === i">SELECTED</p>
        <ion-icon item-end name="play" *ngIf="currentFile.index !== i"></ion-icon>
      </ion-item>
    </ng-container>
  </ion-list>

</ion-content>

In the footer of your Ionic application, you will have two ion-toolbar.

In the first ion-toolbar, you will have a seekbar created with ion-range. This item allows users to change the current time of the audio file being played. Here it is HTML for that:

<!-- ... ion-header and ion-content ... -->
<ion-footer *ngIf="currentFile.file && loggedIn" [@showHide]="displayFooter">
 <ion-toolbar color="primary">
    <ion-range min="0" color="light" [max]="state.durationSec" [formControl]="seekbar" (ionFocus)="onSeekStart()" (ionBlur)="onSeekEnd($event)"
      name="seekbar">
      <ion-label color="light" range-left>{{ state.time }}</ion-label>
      <ion-label color="light" range-right>{{ state.duration }}</ion-label>
    </ion-range>
  </ion-toolbar>
</ion-footer>

In the second ion-toolbar, you will have the rest of the playback controls, as follows:

<!-- ... ion-header and ion-content -->

<ion-footer *ngIf="currentFile.file && loggedIn" [@showHide]="displayFooter">

  <!-- ... seekbar control here-->

 <ion-toolbar color="primary" padding>
    <ion-grid>
      <ion-row align-items-center id="media-controls">
        <button clear ion-col ion-button [disabled]="isFirstPlaying()" (click)="previous()">
          <ion-icon color="light" name="skip-backward"> </ion-icon>
        </button>
        <button clear ion-col ion-button *ngIf="!state.playing" (click)="play()">
          <ion-icon color="light" name="play"></ion-icon>
        </button>
        <button clear ion-col ion-button *ngIf="!!state.playing" (click)="pause()">
          <ion-icon color="light" name="pause"></ion-icon>
        </button>
        <button clear ion-col ion-button [disabled]="isLastPlaying()" (click)="next()">
          <ion-icon color="light" name="skip-forward"></ion-icon>
        </button>
      </ion-row>
    </ion-grid>
  </ion-toolbar>
</ion-footer>

You can find the final version of this file here.

Styling the Audio Player

Just to improve the look and feel of your app, you will do some minor styling in the home.scss file (you can find it under ./src/pages/home/), as shown below:

page-home {
  #app-section {
    #app-title {
      color: color($colors, 'primary');
      text-transform: uppercase;
    }
    ion-icon {
      font-size: 15rem;
    }
  }
  #media-controls {
    button {
      ion-icon {
        font-size: 2.5rem;
      }
    }
  }
}

The Audio Player UI Controller

To help you control your audio player user interface, you will implement a controller responsible for the following things:

  1. When the app is opened, it will check if the user is authenticated or not.
  2. If the user is not authenticated, it will show the authentication UI.
  3. After the authentication process, it will fetch the media file from your mock service and show in the audio player.
  4. Then, it will enable your users to perform media actions like play, pause, or switch media file. It will also enable users to log in and log out.
  5. If the user logs out, it will clear the authentication state from the storage and show the login UI.

As you are using the default HomePage to implement your audio player, you will implement most of the logic inside the HomePage class. So, throughout the following sections, you will implement the following methods:

  • constructor: this method will create an instance of HomePage and subscribe to the isLoggedIn subject;
  • login: this method will enable users to log in;
  • getDocuments: this method will load the music files;
  • presentLoading: this method will present a nice loading screen;
  • ionViewWillLoad: this method will add listeners to media events to update the screen;
  • openFile: this method will fetch the music URL and pass it to playStream.
  • resetState: this method will reset the state of the current music;
  • playStream: this method will subscribe to audioProvider.playStream so it can dispatch actions to the reducer;
  • pause: this method will allow users to pause the playback;
  • play: this method will allow users to start the playback again;
  • stop: this method will allow users to stop the playback;
  • next: this method will allow users to move to the next music;
  • previous: this method will allow users to the previous music;
  • isFirstPlaying: this method will be used to block the previous button;
  • isLastPlaying: this method will be used to block the next button;
  • onSeekStart and onSeekEnd: these methods will be used while using the seek feature;
  • logout: this method will allow users to log out;
  • reset: this method will reset the state of the Ionic app;

However, before focusing on the implementation of these methods, you can add some cool animations to enhance the UX of your app. These animations will appear when the audio player switch between the inactive and active states. This basically means that, when a user starts playing the audio file, your app will show music controls to the user.

To do this, open the ./src/pages/home/home.ts file and replace its code with this:

import {Component, ViewChild} from '@angular/core';
import {trigger, state, style, animate, transition } from '@angular/animations';
import {NavController, NavParams, Navbar, Content, LoadingController} from 'ionic-angular';
import {AudioProvider} from '../../providers/audio/audio';
import {FormControl} from '@angular/forms';
import {CANPLAY, LOADEDMETADATA, PLAYING, TIMEUPDATE, LOADSTART, RESET} from '../../providers/store/store';
import {Store} from '@ngrx/store';
import {CloudProvider} from '../../providers/cloud/cloud';
import {AuthService} from '../../providers/auth0/auth.service';
import {pluck, filter, map, distinctUntilChanged} from 'rxjs/operators';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html',
  animations: [
    trigger('showHide', [
      state(
        'active',
        style({
          opacity: 1
        })
      ),
      state(
        'inactive',
        style({
          opacity: 0
        })
      ),
      transition('inactive => active', animate('250ms ease-in')),
      transition('active => inactive', animate('250ms ease-out'))
    ])
  ]
})
export class HomePage { }

Don't mind all these unused imports, you will need them really soon.

The constructor

Now, you will do three things:

  1. You will define the properties that will help you control the audio player.
  2. You will inject all components that your audio player will use to read music, manage authentication, etc.
  3. And you will subscribe to the isLoggedIn$ subject.

Therefore, update the definition of the HomePage class as follows:

// ... imports statements ...

@Component({
  // ... selector, templateUrl, etc ...
})
export class HomePage {
  files: any = [];
  seekbar: FormControl = new FormControl("seekbar");
  state: any = {};
  onSeekState: boolean;
  currentFile: any = {};
  displayFooter: string = "inactive";
  loggedIn: Boolean;
  @ViewChild(Navbar) navBar: Navbar;
  @ViewChild(Content) content: Content;

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    public audioProvider: AudioProvider,
    public loadingCtrl: LoadingController,
    public cloudProvider: CloudProvider,
    private store: Store<any>,
    public auth: AuthService
  ) {
    this.auth.isLoggedIn$.subscribe((isLoggedIn: any) => {
      this.loggedIn = isLoggedIn;
      if (isLoggedIn) {
        this.getDocuments();
      }
    });
  }
}

Don't worry about the getDocuments method. You will implement it in no time.

The login Method

Then, to enable users to log in, you will add the following method to HomePage:

// ... import statements and @Component declaration ...
export class HomePage {

  // ... constructor ...

  login() {
    this.auth.login()
      .then(() => { console.log('Successful Login'); })
      .catch(error => { console.log(error); });
  }
}

The getDocuments Method

Now, you will implement the getDocuments method with a nice loader on the screen (by using the presentLoading method) and fetch files via the cloudProvider's getFiles method, as shown below:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  getDocuments() {
    let loader = this.presentLoading();
    this.cloudProvider.getFiles().subscribe(files => {
      this.files = files;
      loader.dismiss();
    });
  }

  presentLoading() {
    let loading = this.loadingCtrl.create({
      content: 'Loading Content. Please Wait...'
    });
    loading.present();
    return loading;
  }
}

The ionViewWillLoad Method

As you may know, in Ionic, just like in Angular, you have lifecycle hooks. One of these lifecycle hooks is the ionViewWillLoad one. You will use this hook to add listeners to media state changes. So, when changes are detected, you can update your screen.

The listening process will be achieved by using NgRx store inside this lifecycle hook method:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  ionViewWillLoad() {
    this.store.select('appState').subscribe((value: any) => {
      this.state = value.media;
    });

    // Resize the Content Screen so that Ionic is aware of the footer
    this.store
      .select('appState')
      .pipe(pluck('media', 'canplay'), filter(value => value === true))
      .subscribe(() => {
        this.displayFooter = 'active';
        this.content.resize();
      });

    // Updating the Seekbar based on currentTime
    this.store
      .select('appState')
      .pipe(
        pluck('media', 'timeSec'),
        filter(value => value !== undefined),
        map((value: any) => Number.parseInt(value)),
        distinctUntilChanged()
      )
      .subscribe((value: any) => {
        this.seekbar.setValue(value);
      });
  }
}

The openFile Method

Whenever the user clicks on a media file, the openFile method will be fired. Then, this method will fire the playStream method with the url of the file chosen.

In this article, this data will come from the mock service that you implemented before. On a subsequent article, you will refactor this class to fetch information from a backend API.

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  openFile(file, index) {
    this.currentFile = { index, file };
    this.playStream(file.url);
  }
}

The resetState Method

The playStream method that you will implement, first need to reset the current media state via the resetState method. So, you can implement it as follows:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  resetState() {
    this.audioProvider.stop();
    this.store.dispatch({ type: RESET });
  }
}

To do this, the resetState method stops the currently running media file and dispatch the RESET action to reset the media state.

The playStream Method

Then, the playstream method can fire the playStream method of your AudioProvider. This method on the provider returns an observable that you will use to subscribe and start listening to media events like canplay, playing, etc.

Based on each particular event, you will dispatch a store action with the appropriate payload. Basically, these actions will store media information like current time and duration of the media.

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  playStream(url) {
    this.resetState();
    this.audioProvider.playStream(url).subscribe(event => {
      const audioObj = event.target;

      switch (event.type) {
        case 'canplay':
          return this.store.dispatch({ type: CANPLAY, payload: { value: true } });

        case 'loadedmetadata':
          return this.store.dispatch({
            type: LOADEDMETADATA,
            payload: {
              value: true,
              data: {
                time: this.audioProvider.formatTime(
                  audioObj.duration * 1000,
                  'HH:mm:ss'
                ),
                timeSec: audioObj.duration,
                mediaType: 'mp3'
              }
            }
          });

        case 'playing':
          return this.store.dispatch({ type: PLAYING, payload: { value: true } });

        case 'pause':
          return this.store.dispatch({ type: PLAYING, payload: { value: false } });

        case 'timeupdate':
          return this.store.dispatch({
            type: TIMEUPDATE,
            payload: {
              timeSec: audioObj.currentTime,
              time: this.audioProvider.formatTime(
                audioObj.currentTime * 1000,
                'HH:mm:ss'
              )
            }
          });

        case 'loadstart':
          return this.store.dispatch({ type: LOADSTART, payload: { value: true } });
      }
    });
  }
}

The pause Method

Once playStream method is fired, the media playback is initiated. As such, your users might want to pause the playback. For that, you will implement the pause method as follows:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  pause() {
    this.audioProvider.pause();
  }
}

The play Method

It's also true that users might want to start playing the media again. For that, you will add the following:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  play() {
    this.audioProvider.play();
  }
}

The stop Method

Then, to stop the media, you will add the following method:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  stop() {
    this.audioProvider.stop();
  }
}

The next Method

Also, to let your users move to the next music, you will define the following method:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  next() {
    let index = this.currentFile.index + 1;
    let file = this.files[index];
    this.openFile(file, index);
  }
}

The previous Method

Similarly, you will need to provide a method to play the previous track:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  previous() {
    let index = this.currentFile.index - 1;
    let file = this.files[index];
    this.openFile(file, index);
  }
}

The isFirstPlaying and isLastPlaying Methods

Then, you will need two helper methods to check if the music being played is the first or the last track from the playlist. You use these methods to disable and enable the UI buttons:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  isFirstPlaying() {
    return this.currentFile.index === 0;
  }

  isLastPlaying() {
    return this.currentFile.index === this.files.length - 1;
  }
}

The onSeekStart and onSeekEnd Methods

Also, you will want to enable your users to do seek operations. So, when the user initiates the seek operation, it will fire the onSeekStart method. In it, it will check if the file is currently being played or not and save that information. Then, it will pause the audio file.

When the seek operation ends, it will fire the onSeekEnd method and, in it, you can fetch the time selected by the user. If a file was being played before seeking, you resume the playback:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  onSeekStart() {
    this.onSeekState = this.state.playing;
    if (this.onSeekState) {
      this.pause();
    }
  }

  onSeekEnd(event) {
    if (this.onSeekState) {
      this.audioProvider.seekTo(event.value);
      this.play();
    } else {
      this.audioProvider.seekTo(event.value);
    }
  }
}

The logout and reset Methods

Finally, you will also allow users to log out via the logout method. Along with that, you will have a reset method to reset whole application:

// ... import statements and @Component declaration ...
export class HomePage {

  // ...constructor and other methods ...

  logout() {
    this.reset();
    this.auth.logout();
  }

  reset() {
    this.resetState();
    this.currentFile = {};
    this.displayFooter = "inactive";
  }
}

Updating the AppModule Class

The final steps, before being able to use your app for the first time, starts with importing all the required libraries inside the app.module.ts file:

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; 
import { StoreModule } from '@ngrx/store';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { mediaStateReducer } from '../providers/store/store';
import { AudioProvider } from '../providers/audio/audio';
import { CloudProvider } from '../providers/cloud/cloud';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { AuthService } from '../providers/auth0/auth.service';
import { IonicStorageModule } from '@ionic/storage';

Then, ends with you having to define all the required declarations, imports, providers in the NgModule, as shown here:

// ... import statements and jwtOptionsFactory function ...
@NgModule({
  declarations: [MyApp, HomePage],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    IonicStorageModule.forRoot(),
    StoreModule.forRoot({
      appState: mediaStateReducer
    }),
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [MyApp, HomePage],
  providers: [
    StatusBar,
    SplashScreen,
    AudioProvider,
    CloudProvider,
    AuthService,
    { provide: ErrorHandler, useClass: IonicErrorHandler }
  ]
})
export class AppModule {}

Building and Running the Ionic Audio Player

After implementing your whole app, you can directly run it via following ionic commands:

# for the iOS app
ionic cordova run ios

# for the android app
ionic cordova run android

"I just built a mobile audio player with @Ionicframework, @angular, #RxJS, and #NgRx!!!"

Conclusion and Next Steps

In this article, you created a mobile audio player app with Ionic. You used RxJS to develop audio playback features. Along with that, you used NgRx to manage the state of the application. Besides that, you also used Auth0 to handle user authentication in your mobile app. With this, you have finished developing the first version of the application with static audio content.

In an upcoming, follow-up article, you will create a backend using Node.js and Google Cloud to serve dynamic audio content to your audio player. To avoid spending too much time around configuring servers to host your backend, you will take advantage of Webtask, a serverless solution for Node.js apps.

I hope you enjoyed this article. Stay tuned for the next part!