Open9

「just_audio」についての技術調査

Ryo24Ryo24

initState()でしていること。

void initState()
@override
  void initState() {
    super.initState();
    // インスタンスをバインディングオブザーバー(イベント発生時、通知する)に登録する
    WidgetsBinding.instance?.addObserver(this);
    // 画面の向きを指定する
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      statusBarColor: Colors.black, // 上部のステータスバーの色
    ));
    _init();
  }

https://api.flutter.dev/flutter/widgets/WidgetsBinding/addObserver.html
https://api.flutter.dev/flutter/services/SystemChrome/setSystemUIOverlayStyle.html

_init();

_init()
Future<void> _init() async {
    // アプリのオーディオ関連のプラグインの使用を宣言。
    final session = await AudioSession.instance;
    // オーディオセッションを定義する。(ポッドキャストやオーディオブックなど、連続音声を再生向け) / other: AudioSessionConfiguration.music(音楽再生向け)
    await session.configure(AudioSessionConfiguration.speech());
    // 再生中のイベント(エラー)を取得
    _player.playbackEventStream.listen((event) {},
        onError: (Object e, StackTrace stackTrace) {
          print('A stream error occurred: $e');
        });
    // オーディオファイルを取得する
    try {
      await _player.setAudioSource(AudioSource.uri(Uri.parse(
          "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3")));
    } catch (e) {
      print("Error loading audio source: $e");
    }
  }
  • AudioSessionConfiguration.music:音楽プレーヤーアプリ向け
  • AudioSessionConfiguration.speech:ポッドキャストおよびオーディオブックアプリなど連続再生向け
    2つの違いは最適化の問題で、軽く聞いただけだと違いは感じなかった。
Ryo24Ryo24

dispose()でしていること

dispose()
@override
  void dispose() {
    // インスタンスをオブザーバー登録を削除する
    WidgetsBinding.instance?.removeObserver(this);
    // デコーダやバッファを解放し、他のアプリ(リソース)が活用できるようにする。
    _player.dispose();
    super.dispose();
  }
Ryo24Ryo24

didChangeAppLifecycleState()

didChangeAppLifecycleState()
/// システムがフォアグランド/バックグランドに遷移した時に呼び出される。
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // バックグラウンドで動作している。
    if (state == AppLifecycleState.paused) {
      _player.stop();
    }
  }

https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver/didChangeAppLifecycleState.html
https://api.flutter.dev/flutter/dart-ui/AppLifecycleState-class.html

Ryo24Ryo24

stop methodを使うとエラーを吐く

https://pub.dev/documentation/just_audio/latest/just_audio/AudioPlayer/stop.html

stop methodの実装部分
IconButton(
   icon: const Icon(Icons.stop),
   onPressed: () async => await _player.stop(),
)

エラー文

エラー文
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: PlatformException(abort, Loading interrupted, null, null)
#0      AudioPlayer._setPlatformActive.checkInterruption (package:just_audio/just_audio.dart:1161:7)
#1      AudioPlayer._setPlatformActive.setPlatform (package:just_audio/just_audio.dart:1272:11)
<asynchronous suspension>
flutter: error
flutter: PlatformException(abort, Loading interrupted, null, null)

just_audio/just_audio.dart:1161:7 を覗く

コメントアウトで説明
/// 原文
// Checks if we were interrupted and aborts the current activation. If we
// are interrupted, there are two cases:
// 1. If we were activating the native platform, abort with an exception.
// 2. If we were activating the idle dummy, abort silently.
//
// We should call this after each awaited call since those are opportunities
// for other coroutines to run and interrupt this one.

/// 翻訳
中断されたかどうかをチェックし、現在の起動を中止する。
中断された場合、2つのケースがある。
 1. ネイティブプラットフォームを起動していた場合、例外を発生させて中断する。
 2. アイドルダミーを起動していた場合は、静かに終了します。
    
 待機中の呼び出しは、他のコルーチンが実行される機会であるため、それぞれの呼び出しの後にこの関数を呼び出す必要があります。
他のコルーチンが実行され、このコルーチンを中断する機会となるからです。

原因

playerStateStream property は、PlayerState class のstreamを返すため、再生状態と処理状態を把握することができる。そのため、stop methodでリソースを解放すると処理状態を把握することができずエラーを吐く。

対策

playbackEventStream propertyを使う。
playbackEventStream propertyPlaybackEvent classのstreamを返すため、再生状態と現在の位置を把握することができる。そのため、stop methodでリソースを解放しても再生状態や現在地に関係しないため、エラーは吐かない。

Ryo24Ryo24

playerStateStreamとplaybackEventStreamを比較

playerStateStreamを先に宣言する

playerStateStreamが先
_player.playerStateStream.listen((event) async {
   print('playerStateStream' + ' / ' + event.processingState.toString());
});

_player.playbackEventStream.listen((event) async {
   print('playbackEventStream' + ' / ' + event.processingState.toString());
});
result
flutter: playbackEventStream / ProcessingState.idle
flutter: playerStateStream / ProcessingState.idle
flutter: playbackEventStream / ProcessingState.idle
flutter: playbackEventStream / ProcessingState.loading
flutter: playerStateStream / ProcessingState.loading
flutter: playbackEventStream / ProcessingState.loading
flutter: playbackEventStream / ProcessingState.ready
flutter: playerStateStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.ready
flutter: playerStateStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.buffering
flutter: playerStateStream / ProcessingState.buffering
flutter: playbackEventStream / ProcessingState.ready
flutter: playerStateStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.completed
flutter: playerStateStream / ProcessingState.completed

playbackEventStreamを先に宣言する

playbackEventStreamが先
_player.playbackEventStream.listen((event) async {
   print('playbackEventStream' + ' / ' + event.processingState.toString());
});

_player.playerStateStream.listen((event) async {
   print('playerStateStream' + ' / ' + event.processingState.toString());
});
result
flutter: playbackEventStream / ProcessingState.idle
flutter: playerStateStream / ProcessingState.idle
flutter: playbackEventStream / ProcessingState.idle
flutter: playbackEventStream / ProcessingState.loading
flutter: playerStateStream / ProcessingState.loading
flutter: playbackEventStream / ProcessingState.loading
flutter: playbackEventStream / ProcessingState.ready
flutter: playerStateStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.ready
flutter: playerStateStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.buffering
flutter: playerStateStream / ProcessingState.buffering
flutter: playbackEventStream / ProcessingState.ready
flutter: playerStateStream / ProcessingState.ready
flutter: playbackEventStream / ProcessingState.completed
flutter: playerStateStream / ProcessingState.completed

結果

playbackEventStream -> playerStateStream の順に処理される。

  • ProcessingState.idle
  • ProcessingState.loading
  • ProcessingState.ready

playbackEventStreamは、上記3つの状態を重複して検知する(ProcessingState.readyはplayerStateStreamでも重複する)。だが例外処理を実装せずstop methodを使える。

個人的な意見

再生可能や終了の検知はplaybackEventStreamを使っていく。理由はmethodに干渉せず、注意しなければいけない事項が少ないためである。

Ryo24Ryo24

連続再生

連続再生するには、
https://pub.dev/documentation/just_audio/latest/just_audio/AudioPlayer/setAudioSource.html
を使う。

実装

ByteData data = await rootBundle.load('assets/audio/cute.mp3');

_player.setAudioSource(ConcatenatingAudioSource(children: [
   AudioSource.uri(Uri.dataFromBytes(Uint8List.view(data.buffer))),
   AudioSource.uri(Uri.parse('https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3')),
])).catchError((error) {
   print("Error loading audio source: $error");
});

setAudioSourceAudioSourceしか受け付けない。そしてAudioSourceUriしか受け付けないため、アセットは ByteData -> buffer -> Uint8Listに変換しないといけない。

参考文献

https://ishouldgotosleep.com/simple-flutter-music-player-app/