Save 37% off PRO during our Black Friday Sale! »

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

15bd11f2c2c5e3dd854153d03a102b0d?s=47 stand.fm
February 03, 2021

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

15bd11f2c2c5e3dd854153d03a102b0d?s=128

stand.fm

February 03, 2021
Tweet

Transcript

  1. stand.fm(Android)における react-native-track-playerの改善 エンジニア [holly(ホリー)] 2021/02/02 PORT Firebase x React Native

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

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

    RNTrackPlayer(Android)のアーキテクチャについて - stand.fmで再生コントロール通知をスワイプで削除できるようにした話 - まとめ 3
  4. RNTrackPlayerとは ~基本的な使い方~
 4

  5. 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
  6. RNTrackPlayerのstand.fmでの利用シーン - 通常の放送の再生 - LIVEの再生 6 通常の放送 LIVE

  7. 実装例: 音声の再生、一時停止、停止、リセット // プレイヤーの初期化 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
  8. 実装例: プレイヤーのオプション設定 内容 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)
  9. 実装例: プレイヤーの外部からのイベントに応じて処理をする // プレイヤーからのイベント通知時のコールバックのセット TrackPlayer.addEventListener('remote-play', e => { // 外部入力(通知コントロールなど)から再生イベントが発生した場合

    }) TrackPlayer.addEventListener('remote-pause', e => { // 外部入力(通知コントロールなど)から一時停止イベントが発生した場合 }) TrackPlayer.addEventListener('remote-jump-forward', e => { // 外部入力(通知コントロールなど)からスキップイベント(次へ)が発生した場合 }) TrackPlayer.addEventListener('remote-jump-backward', e => { // 外部入力(通知コントロールなど)からスキップイベント(前へ)が発生した場合 }) 9
  10. - リリースが2020/04から止まっている - PRのマージは去年の10月が最後... - (特にAndroid)で足りない機能やバグがある - 再生コントロール通知がスワイプで削除できない - Pixel系で再生中にアプリをキルしても

    再生のプロセスがキルされない ただ、いくつか問題点が。。 10 問題に対処するためにリポジトリをforkして独自実装を入れている
  11. - Androidの通知センターでメディアのコントロールが可能 - 一般的な音声アプリ(SpotifyやYouTubeMusicなど)では、 一時停止すると通知はスワイプで削除することができる - RNTrackPlayerではstop(停止)機能が用意されていてそちらで 代用する方法もあるが、ボタンが増えるのでかっこ悪い - 補足:Android11ではメディアコントロールの仕様が変わったため

    対象外 Androidで再生コントロール通知がスワイプで削除できない 自分で直すしかないが、ネイティブ側を直す必要がある... 11
  12. ReactNativeで ネイティブを触るには
 12

  13. 13 ReactNativeのネイティブモジュールとは
 - ReactNativeでネイティブのコードを実行する仕組み - https://reactnative.dev/docs/native-modules-intro 
 引用: https://medium.com/hackernoon/first-experiences-with-react-native-bridging-an-andr oid-native-module-for-app-authentication-501fec247b2b

  14. 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
  15. RNTrackPlayerの ネイティブモジュールは どうなっているのか 15

  16. 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
 プレイヤー操作
 プレイヤー操作
 再生状態の変更通知

  17. 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
  18. 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の コネクションが確立
  19. AndroidのForegroundService - Androidのバックグラウンドでの実行制限が厳しくなり、8.0 から導入されたServiceの一種 - ユーザーが視認可能なService - Serviceを開始したあとに通知を作り、Serviceと通知を紐 付けることによって起動する。 -

    起動後10秒以内に紐付けないと、アプリが落ちる 19 ForegroundService起動のために 
 作成された通知

  20. 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と紐付ける
  21. - 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未満
  22. 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
  23. 23 これらを踏まえて、通知をスワイプで消すためにやること 
 MetadataManagerの
 通知状態の更新時に
 プレイヤーが一時停止状態であれば
 stopForeground(false or STOP_FOREGROUND_DETACH)
 を呼んでForegroundServiceを停止させる


  24. 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
  25. 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
  26. 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
  27. 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
  28. 修正後の動作(通知がスワイプで消せるようになった) 
 28

  29. 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
  30. ライブラリをforkして改修することへの考察 
 pros:
 - 自分たちがやりたいように機能追加できる 
 - メンテナに依存しないので修正を待ったり、マージされるか心配する必要がない 
 -

    ユーザーに早く価値を提供できる 
 cons:
 - 公式の実装が活発だと修正がコンフリクトする可能性がある(追従しづらい) 
 - 修正にはネイティブの知識が必要になることも多い 
 - 既存実装をしっかり考慮して既存仕様に影響のないように適切に修正するのが難しい 
 30
  31. まとめ - stand.fmではRNTrackPlayerを使っているが、UXの改善のために足りない機能や バグについてはフォークをして独自実装をしている - ReactNativeはクロスプラットフォームSDKであるが、ネイティブの知識が必要な場 面も多くある - ネイティブエンジニアが活躍できる場面もしばしばある 31

  32. 32

  33. 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
  34. 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
  35. 35 RNTrackPlayerを例にしたService起動のイメージ 
 js
 Module
 Binder
 Service
 Manager
 Player
 setup


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

  36. 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を呼ぶ