Upgrade to Pro — share decks privately, control downloads, hide ads and more …

stand.fm(Android)におけるreact-native-track-playerの改善/Improvement of react-native-track-player on standfm Android app

stand.fm
February 03, 2021

stand.fm(Android)におけるreact-native-track-playerの改善/Improvement of react-native-track-player on standfm Android app

stand.fm

February 03, 2021
Tweet

More Decks by stand.fm

Other Decks in Programming

Transcript

  1. 自己紹介 - 三堀 裕(みつほり ゆう) - holly(ホリー) - 2021/01にstand.fmに正式ジョイン -

    2020/08~: 業務委託 - Android, Flutterなどモバイル開発がメイン - ReactNativeはstand.fmで初めて触りました - 趣味:ダーツ、旅行、(麻雀) @1013Youmeee youmitsu @youmeee 2
  2. アジェンダ - RNTrackPlayerとは~基本的な使い方~ - RNTrackPlayer(Android)の問題点 - RNTrackPlayerを改善する - ReactNativeのネイティブモジュールについて -

    RNTrackPlayer(Android)のアーキテクチャについて - stand.fmで再生コントロール通知をスワイプで削除できるようにした話 - まとめ 3
  3. react-native-track-player(RNTrackPlayer)とは - ReactNativeの音声再生に特化した軽量な多機能ライブラリ - Android, iOS, Windowsに対応 - アプリ以外のコントロールに対応 -

    OSのコントロール通知, Bluetoothデバイス, SmartWatchなど - 様々な音声フォーマットに対応 - mp3, m4a, Streaming(HLS, DASH)など - Network, Local両音源対応 - https://react-native-track-player.js.org/ 5
  4. 実装例: 音声の再生、一時停止、停止、リセット // プレイヤーの初期化 TrackPlayer.setupPlayer() // 再生する音声のメタデータをキューに追加 TrackPlayer.add({ id: 'unique

    track id', url: 'http://example.com/sample_sound.mp3', title: 'sample_sound', artist: 'Bob', artwork: 'http://example.com/sample_artwork.png', }) // 音声の再生、一時停止、停止 TrackPlayer.play() TrackPlayer.pause() TrackPlayer.stop() // プレイヤーのキューに入っているtrackの削除 TrackPlayer.remove(['unique track id']) // プレイヤーのリセット //(再生中のトラックを停止、キューをクリア) TrackPlayer.reset() // プレイヤーの破棄 //(一度呼ぶとsetupPlayerを呼ばない限り、再生ができなくなる) TrackPlayer.destroy() 7
  5. 実装例: プレイヤーのオプション設定 内容 8 // プレイヤーの詳細設定 TrackPlayer.updateOptions({ stopWithApp: true, //

    アプリがキルされたときにプレイヤーも止めるか jumpInterval: 15, // 何秒単位でスキップできるようにするか capabilities: [ // 通知プレイヤーなどがユーザーに提供する機能の種類 TrackPlayer.CAPABILITY_PLAY, TrackPlayer.CAPABILITY_PAUSE, TrackPlayer.CAPABILITY_JUMP_FORWARD, TrackPlayer.CAPABILITY_JUMP_BACKWARD, ], }) // 再生速度の設定 TrackPlayer.setRate(this.props.rate)
  6. 実装例: プレイヤーの外部からのイベントに応じて処理をする // プレイヤーからのイベント通知時のコールバックのセット TrackPlayer.addEventListener('remote-play', e => { // 外部入力(通知コントロールなど)から再生イベントが発生した場合

    }) TrackPlayer.addEventListener('remote-pause', e => { // 外部入力(通知コントロールなど)から一時停止イベントが発生した場合 }) TrackPlayer.addEventListener('remote-jump-forward', e => { // 外部入力(通知コントロールなど)からスキップイベント(次へ)が発生した場合 }) TrackPlayer.addEventListener('remote-jump-backward', e => { // 外部入力(通知コントロールなど)からスキップイベント(前へ)が発生した場合 }) 9
  7. ReactNativeのネイティブモジュールの実装例( Android) public class MusicModule extends ReactContextBaseJavaModule { @Override public

    String getName() { return "TrackPlayerModule"; } @ReactMethod // js側から呼び出すことができるインターフェース public void setupPlayer(ReadableMap data, final Promise promise) { // ネイティブ側のプレイヤーの初期化処理 } } 14 // プレイヤーの初期化 TrackPlayer.setupPlayer() import { NativeModules } from 'react-native'; const { TrackPlayerModule: TrackPlayer } = NativeModules; function setupPlayer(options) { return TrackPlayerModule.setupPlayer(options || {}); } RNTrackPlayerはNativeModuleで実装されている js Component js Module Native Module
  8. RNTrackPlayerのアーキテクチャ(JS+Android Native) 16 JS
 Native
 ReactComponent RNTrackPlayer js module NativeModuleの関数を

    呼び出すモジュール MusicEvent js側への イベントエミッター MusicModule js側から呼び出される インターフェース MusicService バックグラウンドプロセス MusicBinder Serviceへのブリッジ MusicManager 再生状態の管理・イベント ハンドラ ExoPlayback ExoPlayerのインターフェース MetadataManager 通知コントロールの 管理 ExoPlayer イベントのEmit
 プレイヤー操作
 プレイヤー操作
 再生状態の変更通知

  9. AndroidのServiceとは - Androidでバックグラウンドで処理を実行するための仕組み - Activityのようにライフサイクルがある - Serviceを起動するにはcontext.startService()を呼ぶ - Serviceを起動後、Serviceをバインドすると、起動元から起動中のServiceとの通信が 可能になり、様々な操作が可能になる

    17 @ReactMethod public void setupPlayer(ReadableMap data, final Promise promise) { // 省略 Intent intent = new Intent(getReactApplicationContext(), MusicService.class); context.startService(intent); // Serviceの起動 intent.setAction(Utils.CONNECT_INTENT); context.bindService(intent, this, Context.BIND_AUTO_CREATE); // Serviceのバインド } MusicService.java
  10. MusicService起動の流れ 18 JS
 Native
 ReactComponent RNTrackPlayer js module NativeModuleの関数を 呼び出すモジュール

    MusicEvent js側への イベントエミッター MusicModule js側から呼び出される インターフェース MusicService バックグラウンドプロセス MusicBinder Serviceへのブリッジ MusicManager 再生状態の管理・イベント ハンドラ ExoPlayback ExoPlayerのインターフェース MetadataManager 通知コントロールの 管理 ExoPlayer イベントのEmit
 プレイヤー操作
 プレイヤー操作
 再生状態の変更通知
 ②Serviceが生成される ①Service起動要求 (startService) ③ServiceのBind要求 ④ServiceとModuleの コネクションが確立
  11. ForegroundServiceの起動方法 - context.startForegroundService()を呼び出す 20 MusicModule - ForegroundServiceが開始されたあと、10秒以内にService側で通知を作成し、 startForeground()を呼ばなければならない - 呼ばないとANRになりアプリが落ちる仕様になっている

    MusicService public class MusicModule extends ReactContextBaseJavaModule { @ReactMethod public void setupPlayer(ReadableMap data, final Promise promise) { ... Intent intent = new Intent(getReactApplicationContext(), MusicService.class); context.startForegroundService(intent); ... } ... } public class MusicService extends HeadlessJsTaskService { @Override public int onStartCommand(Intent intent, int flags, int startId) { ... startForeground(1, new NotificationCompat.Builder(this, channel).build()); return START_NOT_STICKY; } } 1. ForegroundServiceを起動 2. 通知を作成後startForeground() でServiceと紐付ける
  12. - ForegroundServiceを停止すると、Serviceと通知の紐付けを解除できる (Serviceは停止されない) - 停止するには、stopForeground(boolean)を呼ぶ。引数にフラグを渡すことで、停止方法を選 べる - ForegroundServiceの停止方法は2つ a. 通知を残したまま停止する

    => false(Android 8.0以降はSTOP_FOREGROUND_DETACH) b. 通知を削除し停止する => true(Android 8.0以降はSTOP_FOREGROUND_REMOVE) ForegroundServiceの停止 21 // a. 通知を残したまま停止する service.stopForeground(STOP_FOREGROUND_DETACH); // Android8.0以上 service.stopForeground(false); // Android8.0未満 // b. 通知を削除し停止する service.stopForeground(STOP_FOREGROUND_REMOVE); // Android8.0以上 service.stopForeground(true); // Android8.0未満
  13. ForegroundService起動→停止の流れ 22 JS
 Native
 ReactComponent RNTrackPlayer js module NativeModuleの関数を 呼び出すモジュール

    MusicEvent js側への イベントエミッター MusicModule js側から呼び出される インターフェース MusicService バックグラウンドプロセス MusicBinder Serviceへのブリッジ MusicManager 再生状態の管理・イベント ハンドラ ExoPlayback ExoPlayerのインターフェース MetadataManager 通知コントロールの 管理 ExoPlayer イベントのEmit
 プレイヤー操作
 プレイヤー操作
 再生状態の変更通知
 ②Service生成→通知生成→ startForeground()でフォアグラウン ドサービスを開始 ①ForegroundService 起動要求 (startForegroundService) ④ForegroundService 停止要求 (stopForeground) ③プレイヤー状態 変更通知 stop
  14. RNTrackPlayerのMetadataManagerの従来の実装 private void updateNotification(boolean isPaused) { if(session.isActive()) { // プレイヤーが有効である場合

    service.startForeground(1, builder.build()); // こちらが呼ばれていた } else { service.stopForeground(true); } } 一時停止時にはisActiveのままなので、stopForeground()が呼ばれない (呼ばれたとしても引数がtrueなので通知が消えてしまう) 24 - RNTrackPlayerではプレイヤーの状態に応じてMetadataManagerで通知の更新を行っている - MediaSessionと呼ばれるもので、プレイヤーが有効かを判断し、通知の更新を行う MetadataManager.java
  15. MetadataManagerのコードの修正後 private void updateNotification(boolean isPaused) { if (isPaused) { //

    一時停止時にはactiveがどうかをチェックしない service.startForeground(1, builder.build()); // activeではあるのでstartForeground()は呼ぶ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { service.stopForeground(STOP_FOREGROUND_DETACH); // ≧8.0の場合通知を残すが、サービスは停止しない } else { service.stopForeground(false); // <8.0の場合通知を残して、サービス停止 } return; } if(session.isActive()) { service.startForeground(1, builder.build()); } else { service.stopForeground(true); } } 一時停止時にはstopForeground(false)が呼ばれるように修正する 25 MetadataManager.java
  16. 26 補足1:アプリが破棄されても通知が残り続けてしまうことがある 
 - 通知とServiceが切り離されると、Serviceがキルされても通知が消えない場合がある 
 - アプリが破棄された際に通知を削除するように修正 
 public

    MusicModule(ReactApplicationContext reactContext) { ... reactContext.addLifecycleEventListener(new LifecycleEventListener() { // Activityのライフサイクルの変化を Listen @Override public void onHostResume() { // no-op } @Override public void onHostPause() { // no-op } @Override public void onHostDestroy() { // Activityが破棄されたときに呼ばれる reactContext.removeLifecycleEventListener(this); NotificationManager nManager = ((NotificationManager) getReactApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE)); if (nManager != null) { nManager.cancel(1); // 通知を削除する } } }); } MusicModule.java
  17. 27 補足2:通知をスワイプすると再度Serviceが起動してしまう 
 - 既存実装でDeleteIntentが設定されているため、通知が削除された際にServiceが起動してしま う
 - DeleteIntent: 通知が削除されたのをイベントとしてブロードキャストするためのもの 


    - Serviceが起動されてもstartForeground()を呼べないのでANRで落ちてしまう 
 public MetadataManager(MusicService service, MusicManager manager) { this.builder = new NotificationCompat.Builder(service, channel); … builder.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(service, PlaybackStateCompat.ACTION_STOP)); // この行を削除 } DeleteIntentをセットしないようにする MetadataManager.java
  18. 29 forkしたコードをどうやってアプリに含めるか 
 1. react-native-track-playerのリポジトリをフォーク
 2. package.jsonにてGithubリポジトリを指定する
 "dependencies": { …,

    "react-native-track-player": "git://github.com/username/package.git#commit" // 例 } https://docs.github.com/ja/packages/guides/configuring-npm-for-use-with-git ※privateリポジトリの場合少し方法が変わるので、こちらをご確認ください
 package.json
  19. ライブラリをforkして改修することへの考察 
 pros:
 - 自分たちがやりたいように機能追加できる 
 - メンテナに依存しないので修正を待ったり、マージされるか心配する必要がない 
 -

    ユーザーに早く価値を提供できる 
 cons:
 - 公式の実装が活発だと修正がコンフリクトする可能性がある(追従しづらい) 
 - 修正にはネイティブの知識が必要になることも多い 
 - 既存実装をしっかり考慮して既存仕様に影響のないように適切に修正するのが難しい 
 30
  20. 32

  21. ForegroundServiceの起動方法~MusicModule側~ - context.startForegroundService()を呼び出す - RNTrackPlayerではMusicModuleで起動している 33 public class MusicModule extends

    ReactContextBaseJavaModule { @ReactMethod public void setupPlayer(ReadableMap data, final Promise promise) { ... Intent intent = new Intent(getReactApplicationContext(), MusicService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent); // 8.0以上の場合フォアグラウンドサービスを起動 } else { context.startService(intent); } ... } ... } MusicModule.java
  22. ForegroundServiceの起動方法~MusicService側~ - ForegroundServiceを開始する場合、10秒以内にService側で通知を作成し、 startForeground()を呼ばなければならない - 呼ばないとANRになりアプリが落ちる仕様になっている 34 public class MusicService

    extends HeadlessJsTaskService { @Override public int onStartCommand(Intent intent, int flags, int startId) { ... onStartForeground(); return START_NOT_STICKY; } private void onStartForeground() { ... // 通知を作成し、startForeground()を呼ぶ startForeground(1, new NotificationCompat.Builder(this, channel).build()); } } MusicService.java
  23. 35 RNTrackPlayerを例にしたService起動のイメージ 
 js
 Module
 Binder
 Service
 Manager
 Player
 setup


    destroy
 ︙
 Stop
 Unbind
 add
 play
 pause
 ModuleからPlayerの操作をする際には 
 Binderを経由して行われる 
 Start
 Bind

  24. 36 RNTrackPlayerを例にしたForegroundService起動のイメージ 
 js
 Module
 Binder
 Service
 Music
 Manager
 Metadata


    Manager
 setup
 destroy
 ︙
 Stop
 Unbind
 play
 pause
 Start
 Bind
 startForeground
 onPlay
 通知更新
 (startForeground)
 再生リクエスト
 一時停止リクエスト
 onPause
 通知更新
 (stopForeground)
 Playerの状態変化時に通知状態を更新す るため、
 start/stopForeground()を呼び出す 
 Service起動後にstartForegroundを呼ぶ