TL;DR:本書では、Ionic frameworkAngular を使ってモバイルオーディオプレーヤーアプリを作成する方法について学んでいきます。RxJSObservable を使うオーディオ操作を処理し、 NgRx でアプリケーションの状態を管理する方法についても学んでいきます。 アプリケーションをセキュアにするには、Auth0 を使用します。必要であれば、この GitHub レポジトリで最後のコードを見つけることもできます

はじめに

オーディオプレーヤーを作ることは威圧を感じる作業です。メディアの状態 管理、メディア イベントへの応答、そしてこれら変更を UI(ユーザー インターフェイス)に適切に反映させるなどを考えると特にです。ですから、本書では、これら問題を簡単に解決する Angular と Ionic を(他のライブラリも一緒に)使います。

メディア再生を反応の早い方法で処理するには、JavaScript Audio __オブジェクトRxJS Observable に適用し、NgRx ストアを使ってオーディオプレーヤーの状態を管理します。

そのほかに、モバイルアプリを安全にする Auth0 を使用しますが、後のアーティクルでは、音楽ファイルのリストをアプリに提供するために安全なバックエンドを作る方法について学びます(本書では、スタティック データのモックサービスを使用します)。

「@Ionicframework、@angular、RxJS、NgRx を使ってモバイルオーディオプレーヤーアプリを作りましょう。」

Ionic 開発の必要条件

これからモバイルアプリケーションを作るので、アプリを作るのに必要な SDK が必要です。本書では、Ionic アプリをネイティブ モバイルパッケージにする Cordova を使用します。

次のセクションでは、アプリケーションを作成する前に従わなければならないステップについて説明します。

iOS 用ネイティブ SDK をインストールする

iOS プラットフォームには Mac OS X 環境が必要で、Xcode がインストールされていなければなりません。ネイティブ SDK Mac OS X 環境に構成することについては、このリファレンスを確認してください。Xcode をインストールしたら、コマンドライン ツールとシミュレーターを実行する ios-deploy ツールも必要です。

これらツールをインストールするには、次に従ってください。

  • コマンドラインから xcode-select --install を実行して、Xcode コマンドラインツールをインストールします。
  • それから、npm install -g ios-deploy を実行して ios-deploy ツールをインストールします。

マシンに Node.js NPM がインストールされていない場合は、このレファレンスを確認してください。

Android 用ネイティブ SDK をインストールする

Android アプリケーションには、Android SDK とツールが必要です。以下のステップでは、ご使用の環境に SDK とツールをインストールする方法を説明していますが、詳細説明が必要な場合は、すべてをインストールする方法について詳しく説明するこのリンクを確認してください

  • JDK:JDK __をインストールし、 JDK インストールを指す JAVA_HOME 環境変数をインストールする必要があります。
  • Gradle:Gradle もインストールし、それを環境変数の PATH 変数に追加する必要があります。
  • Android SDK:最も重要なのは、アプリに apk ファイルを生成する Android SDK が必要ですから、Android Studio IDE をインストールし、sdkmanager を使って次をインストールします。
    1. Android Platform SDK
    2. SDK バージョン用ビルド ツール
    3. Android サポートレポジトリ

これらをインストールしたら、ANDROID_HOME 環境変数を Android SDK の位置情報に設定する必要があります。Android SDK の tools ディレクトリ、tools/bin ディレクトリ、platform-tools ディレクトリを PATH 変数に追加することもお勧めします。

 Node.js とツールをインストールする

すでにお伝えしましたが、開発マシンに Node.js をインストールする必要がありますので、まだインストールされていないのであれば、 Node.js __のダウンロードページに行き、その指示に従ってください。

インストールが終わったら、npm を介して Cordova CLI と Ionic CLI をインストールする必要があります。

npm install -g ionic cordova

Ionic アプリをスキャフォールディングする

すべての環境依存関係をインストールしたら、Ionic アプリのスキャフォールディングに集中して実行します。このためには、次のコマンドを端末に発行します。

ionic start audio-player blank

このコマンドで次の2つの質問を表示されます。

  1. ネイティブ iOS と Android をターゲットにする Cordova を新しいアプリに統合しますか?(はい/いいえ) : モバイルデバイスのアプリを作成するのであれば、y(はい)を入力します。
  2. 無料の Ionic Pro SDK をインストールし、お使いのアプリに接続しますか?(はい/いいえ):Ionic Pro 機能をこのチュートリアルで使用する必要がない場合は、n を押してください。

アプリケーションを実行する

次に進む前に、モバイルデバイスやエミュレーターでアプリケーションを始めることができるかを確認します。

たとえば、Mac OS X 環境をご使用で、アプリケーションをテストするためにエミュレーターを使用する場合は、次を実行します。

# iOS アプリの場合
ionic cordova run ios -lc

:上記の -lc は Ionic が サーバーをスピンアップして www ファイルをライブでリロードするl)ことと、コンソールログを端末に印刷するc)ことを意味します。

参考として、以下は現在の開発マシン(ブラウザーがあるものなど)や Android を狙うときに使用できる他のコマンドです。

# ローカルで使用可能にします
ionic serve

# android アプリの場合
ionic cordova run android

プロジェクトの依存関係をインストールする

基本的なアプリをモバイルデバイスで実行できることが確認できたら、依存関係をインストールして次のステップに進みます。モバイルオーディオプレーヤーを作るには、次の NPM ライブラリを使います。

  • @angular/animations:アニメーションを追加してアプリ UX を向上するパッケージ
  • @ngrx/store:アプリケーションの状況を管理するのに役立つように RxJS アプリケーションと Angular アプリケーションを統合して構築するライブラリ
  • moment.js:日付や時間を JavaScript で操作するのに役立つライブラリ
  • auth0-js:JavaScript アプリ用の公式 Auth0 ライブラリ
  • @auth0/cordova:Cordova アプリ用の公式 Auth0 ライブラリ
  • rxjs:JavaScript 用のリアクティブプログラミングライブラリ
  • rxjs-compat:RxJS previous との互換性を下位のバージョン6にするパッケージ

上記のライブラリをインストールするには、次のコマンドを使用します。

# プロジェクトルートにいることを確認します
cd audio-player

# すべてのライブラリをインストールします
npm install --save @angular/animations @ngrx/store moment auth0-js @types/auth0-js @auth0/cordova rxjs@6.2.1 rxjs-compat@6.2.1

:上記のコマンドでは、Ionic (少なくても本書の作成時)は Angular 5 を同梱し、Angular 5 は RxJS 5 API を使用するので、rxjs@6.2.1rxjs-compat@6.2.1 をインストールしました。

再生を管理する Ionic サービスを作成する

アプリの依存関係をインストールしたら、再生機能に取り組みます。

RxJS Observable を作成する

これから作成する Observable はアプリケーションの中心的なものです。RxJS にはカスタム Observable を作るのに役立つ create という名前のヘルパー機能が同梱されています。入力として subscribe 関数が必要です。

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

この subscribe 関数は observer オブジェクトを取り、関数を返します。Observer オブジェクトは Nexterrorcomplete の3つのメソッドを提供します。

  1. 値を発行するには、必要な値で observer.next メソッドを呼び出します。
  2. エラーの場合、そのエラーをスローする observer.error 関数を使って Observable を停止させます。
  3. これから Observable が必要でなく発行する値がなければ、observer.complete メソッドを呼び出します。

Observable.create も呼び出して Observablesubscribe メソッドを介してサブスクライブできるものに返します。このメソッドは Observable から解除したいときに呼び出すことができる関数を返します。

Observable.create(subscribe)Observable.subscribe() を間違えないようにしてください。以前の subscribe 関数は Observable.create への入力で、これは Observable の青写真のようなもので、後者は Observable の実行を呼び出すものです。

オーディオプレーヤーアプリでは、playingpausetimeupdate などのようなメディアイベントについての通知を得るための Observable を作りますから、簡単に言えば、Observable 内の Audio() のメディアイベントに耳を傾けてから、observer.next メソッドを介してアプリの残りを通知します。

これで、Observable が必要であることをご理解いただけたと思いますので、サービスを Ionic アプリに作成することから始めましょう。

ionic generate provider audio

これによって、./src/providers/audio/ の下の audio.ts と呼ばれるファイルにサービスを生成し、このサービスは app.module.tsNgModule に追加されます。audio.ts ファイルのコンテンツを次と置き換えます。

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 => {
      // 再生オーディオ
      this.audioObj.src = url;
      this.audioObj.load();
      this.audioObj.play();

      // メディアイベント
      const handler = (event) => observer.next(event);
      addEvents(this.audioObj, events, handler);

      return () => {
        // 再生停止
        this.audioObj.pause();
        this.audioObj.currentTime = 0;

        // EventListeners を削除します
        removeEvents(this.audioObj, events, handler);
      };
    });
  }
}

これで、新しいオーディオファイルを再生したいときは、この Observable を作成してこれらすべてのメディアイベントを聞きます。これは AudioProvider クラスに追加する playStream() と呼ばれる新しいメソッドを介して行います。

// ... ステートメントをインポートします ...

export class AudioProvider {
  // ... コンストラクターとその他メソッド ...

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

this.stop$ が任意の値を発行したら、この Observable から自動的に解除されることにご留意いただくことが大切です。

AudioProvider サービスを完成させる

これで AudioProvider サービスの基盤ができたので、 メソッドの残りの playpausestopseekToformatTime を実行します。これらの実装は分かりやすいので、これら5つのメソッドを以下に表示のように AudioProvider サービスに追加します。

// ... ステートメントをインポートします ...

export class AudioProvider {

  // ... コンストラクターとその他メソッド ...

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

音楽ファイルを読み取る

再生機能のオーディオサービスを作った後、ファイルのリストを取得するサービスを作る必要があります。このために、Ionic を使ってクラウドサービスを作ります。

ionic generate provider cloud

このコマンドは ./src/providers/cloud の下の cloud.ts という名前のファイルにサービスを生成します。ここで、このファイルのコンテンツを次と置き換えます。

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

上記の getFiles メソッドが基本的に、スタティック files オブジェクトで Observable を返して HTTP リクエストをモックします。

Ionic アプリの状態を NgRx ストアで管理する

アプリケーションの状態を管理するのに役立てるため NgRx __ストアライブラリを活用します。このストアは Redux をベースにしたもので、React の世界では状態を管理するのに非常に有名なもので、Redux 概念と RxJS とを統合します。

Redux が何か(またはその機能)をご存知でない方のために、次で簡単に説明しています。

Redux では、状態は中心的位置で管理されています。つまり、アプリケーション全体の最新状態を格納するオブジェクトがひとつだけということです。どの時点であっても、この状態を更新したいのであれば、 reducer として知られる関数に actiondispatch する必要があります。この reduceraction を理解し、typedata のアクションをベースにした新しい状態を生成する責任があります。

NgRx ストアで Reducer を作成する

デフォルトで、NgRx Action インターフェースはひとつのプロパティ type だけを表示します。アクションのタイプと一緒に情報を送信する必要があるので、ニーズに合わせて NgRx Action インターフェイスを拡張します。

ですから、インターフェイスの拡張を定義するには、store./src/providers/ の下)という名前の新しいディレクトリ内の store.ts という名前のファイルを作り、次のコードをそれに追加します。

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

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

それから、異なるメディアイベントに異なるアクションを作ります(canplayplaying、など)。そのようなものとして、store.ts ファイルを次のように更新します。

// ... ステートメントと MediaAction インターフェイスをインポートします ...

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

その後、MediaAction のインスタンスを受信し、処理する Reducer 関数を実装できるようになります。

// ... インポート、MediaAction、Const ...

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

上記のコード内の各 case ステートメント内で、アプリに新しい state を生成していきます。NgRx は immutable オブジェクトと連携するので、既存のものを更新する代わりに新しい状態オブジェクトを作る必要があることに留意することが重要です。この場合、現在のものをベースにして新しい state オブジェクトを作る Object.assign を使用します。

ここで、Ionic アプリの Reducer を登録するには、app.module.ts ファイルを開き、次のように更新します。

// ... その他のステートメントをインポートします ...
import { StoreModule } from '@ngrx/store';
import { mediaStateReducer } from '../providers/store/store';

@NgModule({
  // ... 宣言 ...
  imports: [
    // ... その他インポートされたモジュール ...
    StoreModule.forRoot({
      appState: mediaStateReducer
    }),
    IonicModule.forRoot(MyApp)
  ],
  // ... ブートストラップ、entryComponents、プロバイダー ...
})
export class AppModule {}

これで Ionic アプリケーションのどこにでもある appState キーを使って現在の状態にアクセスできるようになります。

Ionic アプリの認証

セキュリティアプリを作成するには、ユーザーの認証を処理する Auth0 に依存します。そのようなものとして、無料 Auth0 アカウントはこちらから登録してください。それから、モバイルアプリを表す Auth0 アプリケーションをセットアップする必要があります。

依存関係をインストールする

Ionic アプリを Auth0 でセキュアにするには、Cordova プラグインをインストール必要があります。

# {YOUR_PACKAGE_ID} と独自のアプリ識別子を置き換え、
# YOUR_AUTH0_DOMAIN を独自の Auth0 ドメインと置き換えます
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

注:上記の {YOUR_PACKAGE_ID} と Ionic アプリのパッケージ ID を置き換えます。この情報は config.xml ファイルにあります。このファイルには <widget id="io.ionic.starter" ... のような情報があります。この場合、パッケージ ID は io.ionic.starter です。

注:そのほかに {YOUR_AUTH0_DOMAIN} と独自の Auth0 ドメインを置き換える必要があります。Auth0 アカウントを作成するとき、ionic-audio-playeryour-name、などのようなサブドメインを選択します。この場合、Auth0 ドメインは ionic-audio-player.auth0.com になります。次のスクリーンショットに表示されているように、Auth0 ダッシュボードの右上にもサブドメインがあります。

Find your Auth0 subdomain.

Auth0 アプリケーションをセットアップする

  1. Auth0 ダッシュボードに移動し、「新しいアプリケーションの作成」ボタンをクリックします。
  2. 新しいアプリの名前を付け(「Ionic オーディオ プレーヤー」など)、そのタイプに「ネイティブアプリ」を選択し、「作成」ボタンをクリックします。
  3. 新しい Auth0 アプリの設定タブで、許可されたオリジン(CORS) ボックスの file://*, http://localhost:8080 を追加します。
  4. 設定タブで、YOUR_PACKAGE_ID://YOUR_AUTH_DOMAIN/cordova/YOUR_PACKAGE_ID/callback許可されたコールバック URL に追加します。
  5. http://localhost:8080許可されたログアウト URL に追加します。
  6. 「変更の保存」ボタンをクリックします。

注:アプリをライブでの再読み込み機能で実行しているのであれば、 http://localhost:8080 とは異なる URL を 許可されたオリジン(CORS) ボックスに追加しなければならないかもしれません。アプリを実行しているとき、正しい URL を見つけるために config.xml ファイルの allow-navigation プロパティを確認します。例えば:http://192.168.0.14:8100 などです。

注:ステップ4では、YOUR_PACKAGE_IDYOUR_AUTH_DOMAIN を独自のデータ( io.ionic.starterionic-audio-player.auth0.com のようなプロジェクトの依存関係をインストールしたときに使用した同じデータ)と交換します。

Ionic で Auth0 を構成する

ここで、auth.config.ts という名前のファイルを新しいディレクトリの ./src/providers/auth0/ に作成します。そのファイル内に、次のコードを追加します。

export const AUTH_CONFIG = {
  clientID: 'YOUR_CLIENT_ID',// Auth0 に必要(大文字化:ID)
  clientId: 'YOUR_CLIENT_ID', // Auth0Cordova に必要(大文字化:Id)
  domain: 'YOUR_AUTH_DOMAIN',
  packageIdentifier: 'your.app.id' // config.xml ウィジェット ID にあります(com.auth0.ionic など)
};

以下のリストがこれら値の意味を説明します。

  • clientIDclientId:Auth0 アプリケーション(上記で作成したもの)で使用可能な Client Id プロパティです。
  • domain:独自の Auth0 ドメインです。
  • packageIdentifier: 独自の Ionic アプリケーションのウィジェット ID です。上記で説明したように、これはアプリケーションの config.xml ファイルにあります。

次に進む前に、YOUR_CLIENT_IDYOUR_AUTH_DOMAINyour.app.id とを独自のデータに置き換えてください。

認証サービス

Auth0 アカウントを作成して、auth.config.ts ファイルを定義したら、Ionic アプリの認証サービスを定義する必要があります。そのようなものとして、auth.service.ts という名前の新しいファイルを ./src/providers/auth0/ ディレクトリに作成し、そのファイルに次のコンテンツを加えます。

import {Injectable, NgZone} from '@angular/core';
import {Storage} from '@ionic/storage';
import {Subject} from 'rxjs';
// AUTH_CONFIG、Auth0Cordova、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',
      };
      // Auth0 で認証ログインリクエスト: ログインページを開き、認証結果を取得します
      this.Client.authorize(options, (err, authResult) => {
        if (err) {
          this.loading = false;
          reject(err);
        } else {
          // アクセストークンと ID トークンを設定します
          this.storage.set('id_token', authResult.idToken);
          this.storage.set('access_token', authResult.accessToken)
            .then(() => {
              // ログイン済みに設定します
              this.loading = false;
              this.loggedIn = true;
              this.isLoggedIn$.next(this.loggedIn);
              resolve();
            });
          this.accessToken = authResult.accessToken;
          // アクセストークンの有効期限を設定します
          const expiresAt = JSON.stringify(
            authResult.expiresIn * 1000 + new Date().getTime()
          );
          this.storage.set('expires_at', expiresAt);
          // ユーザーのプロファイル情報をフェッチします
          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);
  }
}

上記のコード機能をよりよく理解するには、次の説明をお読みください。

  • accessToken:これは JWT トークンで、ユーザーが Auth0 から取得します。これらトークンはユーザーを識別するために使用します。
  • user:このプロパティはemailfirstnamelastname、などのようなユーザーデータを保留します。
  • loggedIn:このブール値はユーザーの認証状態を保留します。
  • isLoggedIn$:これは RxJS サブジェクトです。これを loggedIn プロパティの最有効化バージョンとして考えてください。これはユーザーの認証状態を得るために Angular コンポーネントで使用できます。

では、上記のサービス方法を見てみましょう。

  • constructor():コンストラクターで、ユーザーが以前、認証したか否かを確認します。それを元に、this.user プロパティ、this.accessToken プロパティ、this.loggedIn プロパティの値を設定します。
  • login():ログイン方法では、ユーザーの認証が成功しているかそのユーザーを認証し、そのプロファイル情報をフェッチします。この情報を @ionic/store を介して永続記憶領域に保存し、認証状態を反映するサービスの適切なプロパティにも設定します。
  • logout():ログアウト方法では、ユーザーのすべての情報を永続記憶領域から削除し、ログアウト状態を反映するサービスのプロパティを設定します。

認証コールバック

認証後、Auth0 からのリダイレクトを処理するには、以下のように、app.component.ts ファイルを更新する必要があります。

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();

      // 認証した後に、アプリにリダイレクトします
      (window as any).handleOpenURL = (url: string) => {
        Auth0Cordova.onRedirectUri(url);
      };
    });
  }
}

Ionic にオーディオプレーヤー UI を作成する

これまでは、フロントエンドやアプリケーションのユーザーインターフェイス(UI)に関係ないコードを記述してきました。このセクションでは、UI やその動作をデザインします。最終的に、アプリケーションは次のようになります。

Ionic audio player demo app UI

オーディオプレーヤー HTML

./src/pages/home/ ディレクトリ内で home.html ファイルを見つけます。このファイルにプレーヤーを定義する HTML を追加します。ご覧のように、スクリーンのトップにナビゲーション バーがあり、そのバーにはアプリケーションの名前とログアウトボタンがあります。このボタンはユーザーがログインしたときに、表示されます。

ヘッダーのほかに、 ion-content のログインボタン、アプリのロゴ、メディアファイルのリストが表示されます。

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

Ionic アプリケーションのフッターには ion-toolbar が2つあります。

最初の ion-toolbar には、ion-range で作成された seekbar があります。これは、ユーザーが再生されるオーディオファイルの最新時刻を変更できるようにします。以下はその HTML です。

<!-- ... ion-header と 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>

ふたつめの ion-toolbar には、次のような再生コントロールの残りがあります。

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

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

  <!-- ... シークバーコントロールはこちら-->

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

このファイルの最終バージョンはこちらをご覧ください

オーディオプレーヤーのスタイルを設定する

アプリの外観を多少向上させるため、以下のように、home.scss ファイルのスタイルを設定します(このファイルは ./src/pages/home/ の下にあります)。

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

オーディオプレーヤーの UI コントローラー

オーディオプレーヤーのユーザーインターフェイスをコントロールするには、次を担当するコントローラーを実装します。

  1. アプリ―が開くと、ユーザーが認証されているか否かを確認します。
  2. ユーザーが認証されていなければ、認証 UI が表示されます。
  3. 認証プロセスの後、モックサービスからメディアファイルがフェッチされ、オーディオプレーヤーに表示されます。
  4. それから、ユーザーは再生、一時停止、メディアファイルの切り替えなどメディア操作が可能になります。ユーザーがログインしたりログアウトしたりできるようにもします。
  5. ユーザーがログアウトしたら、保存スペースから認証状態が削除され、そのログイン UI が表示されます。

オーディオプレーヤーを実装するデフォルトの HomePage を使用しているので、HomePage クラス内のほとんどのロジックを実装します。次のセクションでは、次のメソッドを実装していきます。

  • constructor:このメソッドは HomePage のインスタンスを作成し、isLoggedIn サブジェクトに登録する
  • login:このメソッドはユーザーのログインを可能にする
  • getDocuments: このメソッドは音楽ファイルを読み込む
  • presentLoading:このメソッドは読み込み画面を表示する
  • ionViewWillLoad:このメソッドは画面を更新するメディアイベントにリスナーを追加する
  • openFile:このメソッドは音楽 URL をフェッチし、それを playStream にパスする
  • resetState:このメソッドは現在の音楽状態をリセットする
  • playStream:このメソッドはアクションが Reducer にディスパッチされるように audioProvider.playStream に登録する
  • pause:このメソッドはユーザーが再生を一時停止できるようにする
  • play:このメソッドはユーザーが再生を再度開始できるようにする
  • stop:このメソッドはユーザが再生を停止できるようにする
  • next:このメソッドはユーザーが次の音楽に移動できるようにする
  • previous:このメソッドはユーザーが以前の音楽に移動できるようにする
  • isFirstPlaying:このメソッドは[前へ] ボタンの使用を禁止する
  • isLastPlaying:このメソッドは[次へ] ボタンの使用を禁止する
  • onSeekStart および onSeekEnd:これらのメソッドはシーク機能の使用中に使用する
  • logout:このメソッドはユーザーのログアウトを可能にする
  • reset:このメソッドは Ionic アプリの状態をリセットする

ただし、これらメソッドの実装にフォーカスする前に、UX アプリを拡張するアニメーションを追加できます。これらアニメーションはオーディオプレーヤーが inactive 状態や active 状態に切り替わるときに表示されます。簡単に説明すると、ユーザーがオーディオファイルを再生し始めると、アプリはユーザーに音楽コントロールを表示します。

このためには、./src/pages/home/home.ts ファイルを開き、そのコードを次に置き換えます。

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

未使用のインポートはもうすぐ必要になりますので、現時点では気にしないでください。

constructor

これから、次の3つを実行します。

  1. オーディオプレーヤーをコントロールするのに役立つプロパティを定義する
  2. オーディオプレーヤーが音楽を読み込んだり、認証を管理したりするときに使用するすべてのコンポーネントを挿入する
  3. isLoggedIn$ サブジェクトに subscribe する

従って、HomePage クラスの定義を次のように更新します。

// ... ステートメントをインポートします ...

@Component({
  // ... セレクター、templateUrl、など...
})
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();
      }
    });
  }
}

getDocuments メソッドは気にしないでください。その実装はもうすぐ行います。

Login メソッド

それでは、ユーザーがログインできるように、次のメソッドを HomePage に追加します。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ... コンストラクター ...

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

getDocuments メソッド

では、画面で素晴らしいローダーを使って getDocuments メソッドを実装し(presentLoading メソッドを使って)、cloudProvidergetFiles メソッドを介してファイルを次のようにフェッチします。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

ionViewWillLoad メソッド

ご存知のように、Ionic には Angular のようにライフサイクルフックがあります。このライフサイクルフックのひとつが ionViewWillLoad です。このフックを使ってリスナーをメディア状態の変更に追加しますので、変更が検出されると、画面を更新できます。

リスニングのプロセスはこのライフサイクルフックのメソッド内にある NgRx ストアを使って達成されます。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド...

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

    // Ionic がフッターに対応するように Content Screen のサイズを変更します
    this.store
      .select('appState')
      .pipe(pluck('media', 'canplay'), filter(value => value === true))
      .subscribe(() => {
        this.displayFooter = 'active';
        this.content.resize();
      });

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

openFile メソッド

ユーザーがメディアファイルをクリックすると、openFile メソッドが起動します。それから、このメソッドは選択された fileurlplayStream メソッドを起動します。

本書では、このデータは以前に実装されたモックサービスからきます。後のアーティクルでは、バックエンド API から情報をフェッチするこのクラスをリファクターしていきます。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

resetState メソッド

これから実装する playStream メソッドはまず、resetState メソッドを介して現在のメディア状態をリセットする必要がありますので、次のように実装できます。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

このためには、resetState メソッドが現在実行しているメディアファイルを停止し、そのメディア状態をリセットする RESET アクションを送ります。

playStream メソッド

それから、playstream メソッドは AudioProviderplayStream メソッドを起動します。プロバイダー上のこのメソッドは登録し、聞き始めるために使用する Observable を canplayplaying、などのようなメディアイベントに返します。

各特定のイベントを基にして、適切なペイロードでストア アクションを送ります。基本的に、これらアクションは現在の時間やメディアの期間のようなメディア情報を格納します。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

pause メソッド

playStream メソッドが起動したら、メディア再生が始まります。そのようなものとして、ユーザーは再生を一時停止するかもしれません。そのため、次のように pause メソッドを実装します。

// ... ステートメントと @Component 宣言をインポートします...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

play メソッド

ユーザーがメディアを再度、再生し始めるかもしれないというのも本当です。そのため、次を追加します。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

stop メソッド

それから、メディアを停止するには、次のメソッドを追加します。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

next メソッド

また、ユーザーを次の音楽に移動させるために、次のメソッドを定義します。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

previous メソッド

同様に、以前のトラックを再生するメソッドを提供する必要があります。

// ... ステートメントと @Component 宣言をインポートします...
export class HomePage {

  // ...コンストラクターとその他のメソッド  ...

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

isFirstPlaying メソッドと isLastPlaying メソッド

それから、再生されている音楽がプレイリストの最初のトラックか、最後のトラックかを確認する2つのヘルパーメソッドが必要です。これらのメソッドを使って UI ボタンを無効にしたり有効にしたりします。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

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

onSeekStart メソッドと onSeekEnd メソッド

ユーザーがシーク操作ができるようにもしたいので、ユーザーがシーク操作を始めると、onSeekStart メソッドを起動します。そこでは、ファイルが現在再生されているか否かを確認して、その情報を格納します。それから、オーディオファイルを一時停止します。

シーク操作が終わると、onSeekEnd メソッドを起動し、そこで、ユーザーが選択した時間をフェッチできます。シーク操作前にファイルが再生された場合、その再生を再開します。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

logout メソッドと reset メソッド

最後に、ユーザーが logout メソッドを介してログアウトできるようにします。それと同時に、アプリケーション全体をリセットする reset メソッドがあります。

// ... ステートメントと @Component 宣言をインポートします ...
export class HomePage {

  // ...コンストラクターとその他のメソッド ...

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

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

AppModule クラスを更新する

最後のステップ、アプリを初めて使うことができる前に、app.module.ts ファイル内で必要なライブラリすべてをインポートし始めます。

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';

それから、NgModule で必要な declarationsimportsproviders をすべて定義して終えます。

// ... ステートメントと jwtOptionsFactory 関数をインポートします ...
@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 {}

Ionic オーディオプレーヤーを構築し、実行する

アプリ全体を実装した後、次の ionicコマンドを介して直接実行できます。

# iOS アプリの場合
ionic cordova run ios

# android アプリの場合
ionic cordova run android

「モバイルオーディオプレーヤーを @Ionicframework、@angular、#RxJS、#NgRx を使って作りました!!!」

結論および次のステップ

本書では、Ionic を使ってモバイルオーディオプレーヤーを作りました。RxJS を使ってオーディオ再生機能を開発しました。そのほかに、アプリケーションの状態を管理する NgRx を使いました。また、モバイルアプリでユーザー認証を処理する Auth0 も使いました。これで、静的なオーディオコンテンツを使ったアプリケーションの初めてのバージョンを作り終えました。

次のアーティクルでは、オーディオプレーヤーに動的オーディオコンテンツを提供するために、Node.js や Google Cloud を使ってバックエンドを作ります。バックエンドをホストするサーバーを構成するときに時間をかけ過ぎないようにするため、Node.js アプリ向けのサーバーなしソリューション Web __タスクを利用します。

本書をお楽しみいただけましたでしょうか。次回をお楽しみに!