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
object with a RxJS Observable and you will use the NgRx store to manage the state of your audio player.Audio
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.”
Tweet This
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
to install the Xcode command-line tool.xcode-select --install
- Then, run
to install thenpm install -g ios-deploy
tool.ios-deploy
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
environment variable pointing to your JDK installation.JAVA_HOME
- Gradle: You will also need to install Gradle and add it to the
variable in your environment variables.PATH
- Android SDK: Most importantly, you will need Android SDKs to generate
files for your app. So, install the Android Studio IDE and, using theapk
, install these:sdkmanager
- Android Platform SDK;
- build-tools for that SDK version;
- 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:
- Would you like to integrate your new app with Cordova to target native iOS and Android? (y/N): You can input
(yes) as you are going to build the app for mobile devices.y
- Install the free Ionic Pro SDK and connect your app? (Y/n): Press
because you won't really need to use any Ionic Pro feature in this tutorial.n
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:
above means that you want Ionic to spin up a server to live-reload www files (the-lc
) and to print out console logs to terminal (thel
).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:
: a package to improve your app UX by adding some animations;@angular/animations
: a library built to integrate RxJS and Angular applications to help you manage the state of your apps;@ngrx/store
: a library that helps manipulating dates and times in JavaScript;moment.js
: the official Auth0 library for JavaScript apps;auth0-js
: the official Auth0 library for Cordova apps;@auth0/cordova
: a reactive programming library for JavaScript;rxjs
: a package to get backward compatibility with RxJS previous to version 6;rxjs-compat
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
andrxjs@6.2.1
because Ionic (at least, at the time of writing) ships with Angular 5 and because Angular 5 uses RxJS 5 APIs.rxjs-compat@6.2.1
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
.- To emit a value, you can call the
method with the desired value.observer.next
- In case of an error, you can use the
function to throw the error and make the observable stop.observer.error
- If you no longer need the observer and there are no more values to emit, you can call the
method.observer.complete
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
andObservable.create(subscribe)
. FormerObservable.subscribe()
function is an input tosubscribe
, which is sort of like blueprint of an observable, and latter is the one which invokes the execution of an observable.Observable.create
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
andispatch
to a function known asaction
. Thisreducer
is responsible for understanding thereducer
and generating a new state based on the actionaction
andtype
.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
above with the package id of your Ionic app. You can find this information in the{YOUR_PACKAGE_ID}
file. There, you will see something likeconfig.xml
. In this case, your package id would be<widget id="io.ionic.starter" ...
.io.ionic.starter
Note: Besides that, you will also need to replace
with your Auth0 domain. When creating your Auth0 account, you chose a subdomain like{YOUR_AUTH0_DOMAIN}
, orionic-audio-player
, etc. In that case, your Auth0 domain would beyour-name
. You can also find your subdomain on the upper right corner of your Auth0 dashboard, as shown in this screenshot:ionic-audio-player.auth0.com
Set Up an Auth0 Application
- Go to your Auth0 Dashboard and click the "create a new application" button.
- Name your new app (e.g. "Ionic Audio Player"), select "Native App" as its type, and click the "Create" button.
- In the Settings tab of your new Auth0 app, add
in the Allowed Origins (CORS) box.file://*, http://localhost:8080
- Still in the Settings tab, add
, to the Allowed Callback URLs.YOUR_PACKAGE_ID://YOUR_AUTH_DOMAIN/cordova/YOUR_PACKAGE_ID/callback
- Add
to the Allowed Logout URLs.http://localhost:8080
- Click the "Save Changes" button.
Note: If running your app with the live reload feature, you might need to add an URL different than
to the Allowed Origins (CORS) box. When running your app, check thehttp://localhost:8080
property of theallow-navigation
file to find out the correct URL. For example:config.xml
.http://192.168.0.14:8100
Note: On step 4, you will need to replace
andYOUR_PACKAGE_ID
with your own data (the same as used while installing your project dependencies: e.g.YOUR_AUTH_DOMAIN
andio.ionic.starter
).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:
andclientID
: They are the Client Id property available in your Auth0 Application (the one created above).clientId
: It's your Auth0 Domain.domain
: It's the widget ID of your Ionic application. You have this in thepackageIdentifier
file of your application, as described before.config.xml
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:
: This is the JWT Token that your users will get from Auth0. These tokens are used to identify the user.accessToken
: This property holds the user data likeuser
,email
,firstname
, and so on.lastname
: This boolean holds the authentication state of the user.loggedIn
: This is a RxJS Subject. Think of this as the reactive version of theisLoggedIn$
property. You will use it in your Angular Component to get user's authentication state.loggedIn
Now, take a look at the methods of the service above:
: In the constructor, you check if the user is previously authenticated or not. Based on it, you set the value of theconstructor()
,this.user
, andthis.accessToken
properties.this.loggedIn
: 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 vialogin()
and also set the appropriate properties of the service to reflect the authentication state.@ionic/store
: In logout method, you remove all the user information from permanent storage and set the properties of service to reflect the logout state.logout()
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:
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:
- When the app is opened, it will check if the user is authenticated or not.
- If the user is not authenticated, it will show the authentication UI.
- After the authentication process, it will fetch the media file from your mock service and show in the audio player.
- 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.
- 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:
: this method will create an instance ofconstructor
and subscribe to theHomePage
subject;isLoggedIn
: this method will enable users to log in;login
: this method will load the music files;getDocuments
: this method will present a nice loading screen;presentLoading
: this method will add listeners to media events to update the screen;ionViewWillLoad
: this method will fetch the music URL and pass it toopenFile
.playStream
: this method will reset the state of the current music;resetState
: this method will subscribe toplayStream
so it can dispatch actions to the reducer;audioProvider.playStream
: this method will allow users to pause the playback;pause
: this method will allow users to start the playback again;play
: this method will allow users to stop the playback;stop
: this method will allow users to move to the next music;next
: this method will allow users to the previous music;previous
: this method will be used to block the previous button;isFirstPlaying
: this method will be used to block the next button;isLastPlaying
andonSeekStart
: these methods will be used while using the seek feature;onSeekEnd
: this method will allow users to log out;logout
: this method will reset the state of the Ionic app;reset
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
constructor
Now, you will do three things:
- You will define the properties that will help you control the audio player.
- You will inject all components that your audio player will use to read music, manage authentication, etc.
- And you will
to thesubscribe
subject.isLoggedIn$
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
method. You will implement it in no time.getDocuments
The login
Method
login
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
getDocuments
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
ionViewWillLoad
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
openFile
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
resetState
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
playStream
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
pause
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
play
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
stop
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
next
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
previous
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
isFirstPlaying
isLastPlaying
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
onSeekStart
onSeekEnd
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
logout
reset
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
AppModule
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!!!”
Tweet This
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!
About the author
Indermohan Singh
Mobile Developer