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."
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
xcode-select --install
to install the Xcode command-line tool. - Then, run
npm install -g ios-deploy
to install theios-deploy
tool.
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 thesdkmanager
, install these:- 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
y
(yes) as you are going to build the app for mobile devices. - 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 (thel
) and to print out console logs to terminal (thec
).
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
andrxjs-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
.
- To emit a value, you can call the
observer.next
method with the desired value. - In case of an error, you can use the
observer.error
function to throw the error and make the observable stop. - 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)
andObservable.subscribe()
. Formersubscribe
function is an input toObservable.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
anaction
to a function known asreducer
. Thisreducer
is responsible for understanding theaction
and generating a new state based on the actiontype
anddata
.
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 theconfig.xml
file. There, you will see something like<widget id="io.ionic.starter" ...
. In this case, your package id would beio.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 likeionic-audio-player
, oryour-name
, etc. In that case, your Auth0 domain would beionic-audio-player.auth0.com
. You can also find your subdomain on the upper right corner of your Auth0 dashboard, as shown in this screenshot:
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
file://*, http://localhost:8080
in the Allowed Origins (CORS) box. - Still in the Settings tab, add
YOUR_PACKAGE_ID://YOUR_AUTH_DOMAIN/cordova/YOUR_PACKAGE_ID/callback
, to the Allowed Callback URLs. - Add
http://localhost:8080
to the Allowed Logout URLs. - 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 theallow-navigation
property of theconfig.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
andYOUR_AUTH_DOMAIN
with your own data (the same as used while installing your project dependencies: e.g.io.ionic.starter
andionic-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
andclientId
: 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 theconfig.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 likeemail
,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 theloggedIn
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 thethis.user
,this.accessToken
, andthis.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:
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:
constructor
: this method will create an instance ofHomePage
and subscribe to theisLoggedIn
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 toplayStream
.resetState
: this method will reset the state of the current music;playStream
: this method will subscribe toaudioProvider.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
andonSeekEnd
: 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:
- 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
subscribe
to theisLoggedIn$
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!!!"
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!