【Flutter】just_audioを使って音声ファイルを再生する
概要
Flutterで音声ファイルの再生を可能にするパッケージはいくつかありますが、この記事ではjust_audio
を使って、基本となる再生とバックグラウンド再生などを解説していきます
その1: 音声ファイルを再生する ⬅︎ 今回
その2: 音声ファイルをバックグラウンド再生する
just_audio
以外のパッケージを知りたい方はこちらを参考にしてみてください
環境
Dart: 2.14.4
Flutter: 2.10.3
just_audio: 0.9.19
rxdart: 0.27.3
audio_video_progress_bar: 0.9.0
全体像
今回作るサンプルの全体像は下記の通りです
UI:
- 再生ファイルの進捗を表示するスライダー
- 音声ファイルの操作を行うボタン(再生、停止)
State:
- Sliderの状態値
ProgressBarState
- 音声ファイルの状態値
AudioState
-
AudioPlayer
オブジェクト - UI側へは
Provider
を使って依存関係を流し込んでいきます
全体のコードはこちらを参照ください Github
AudioPlayerクラスについて
音声ファイルの状態や操作は、just_audio
パッケージで提供されているAudioPlayer
クラスを使って行っていきます
音声ファイルの操作に必要あメソッドや音声ファイルに関する様々な状態値のStreamを兼ね備えており、今回のコアとなるクラスです
このオブジェクトから今回は下記のメソッドとStreamを利用していきます
今回使うメソッド
-
setUrl()
:音声ファイルをURLからダウンロード -
play()
:音声ファイルの再生 -
pause()
:音声ファイルの停止 -
seek()
:再生位置の変更 -
dispose()
:AudioPlayerオブジェクトの破棄
状態値のStream
-
PlayerStateStream
:音声ファイルの再生状況を表すPlayerState
オブジェクトが流れるStream -
PositionStream
:現在の再生位置を表すStream。Duration
オブジェクトが流れています。 -
BufferedPositionStream
:ダウンロードが完了した位置を表すStream。Duration
オブジェクトが流れています。 -
DurationStream
:音声ファイル全体の分数を表す。全体の分数が分かった時点で一度だけDuration
オブジェクトを流します。
状態管理クラスで準備する変数
状態管理クラスでは以下変数を管理します。
class JustAudioScreenState extends ChangeNotifier {
AudioPlayer _audioPlayer; // AudioPlayerオブジェクト
ProgressBarState progressBarState = ProgressBarState( // スライダーの状態値
current: Duration.zero,
buffered: Duration.zero,
total: Duration.zero,
);
AudioState audioState = AudioState.paused; // ボタン用の音声ファイルの状態値
StreamSubscription _playerStateSubscription; // playerStateStreamへのサブスクリプション
StreamSubscription _progressBarSubscription; // スライダー状態値用のStreamへのサブスクリプション
static final _url = // 再生する音楽ファイルのダウンロードURL
'https://firebasestorage.googleapis.com/v0/b/flutter-toybox.appspot.com/o/audios%2Fmusic_box.mp3?alt=media';
}
上述の通り、Provider
を使ってUI側に注入していきます。
状態管理クラスで準備するメソッド
初期化メソッド
状態管理クラス生成時に走らせるメソッドです
AudioPlayer
クラスのインスタンス化し、setUrl()
で音声ファイルのダウンロード先を設定しています。
/* --- INITIALIZE --- */
void init() {
_audioPlayer = AudioPlayer()..setUrl(_url); // インスタンス化と音声ファイルの設定
_listenToPlaybackState(); // ボタン用のサブスクリプションを行うメソッド
_listenForProgressBarState(); // スライダー用のサブスクリプションを行うメソッド
}
音声ファイルの操作メソッド
ボタンとスライダー操作でこちらのメソッドを発火させます
/* --- PLAYER CONTROL --- */
void play() => _audioPlayer.play();
void pause() => _audioPlayer.pause();
void seek(Duration position) => _audioPlayer.seek(position);
状態値の操作メソッド
ボタン用の状態値AudioState
とスライダー用の状態値ProgressBarState
の更新をUI側に通知します
/* --- STATE CONTROL --- */
void setAudioState(AudioState state) {
audioState = state;
notifyListeners();
}
void setProgressBarState(ProgressBarState state) {
progressBarState = state;
notifyListeners();
}
disposeメソッド
状態管理クラスが破棄される際にAudioPlayerオブジェクトの破棄とStreamへのサブスクリプションを閉じてメモリを解放します
void dispose() {
_audioPlayer.dispose();
_playbackSubscription.cancel();
_progressBarSubscription.cancel();
super.dispose();
}
ボタン、スライダー
ボタンUI
ボタンのUIは音声ファイルの状態に応じて、「再生ボタン」、「停止ボタン」、「ローディング中」の三つを切り替えます
switch (state) {
// ローディング中
case AudioState.loading:
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: 32,
width: 32,
child: CircularProgressIndicator(),
),
);
case AudioState.ready:
case AudioState.paused:
// 再生ボタン
return IconButton(
onPressed: () =>
context.read<JustAudioScreenState>().play(),
icon: Icon(Icons.play_arrow),
iconSize: 32.0,
);
case AudioState.playing:
// 停止ボタン
return IconButton(
onPressed: () =>
context.read<JustAudioScreenState>().pause(),
icon: Icon(Icons.pause),
iconSize: 32.0,
);
default:
return SizedBox(
height: 32,
width: 32,
);
}
ボタンState
Stateクラス
ボタンが監視する状態値は音声ファイルの状態です。以下4つの状態のいずれかになる状態値としてenumで定義したAudioState
を状態管理クラス側で管理します。
-
ready
:再生準備OK -
paused
:停止中 -
playing
:再生中 -
loading
:読み込み中
enum AudioState {
ready,
paused,
playing,
loading,
}
Streamにサブスクライブ
音声ファイルの状態値はAudioPlayer
オブジェクトのPlayerStateStream
にサブスクライブする事で監視します
PlayerStateStream
から流れてくるPlayerState
オブジェクトはprocessingState
とplaying
というフィールドを持っており、この2つの値を見る事で音声ファイルの状態を判定していきます
-
PlayerState.processingState
:音声ファイルのダウンロード状況。以下4つの状態を取ります-
idle
:ロード前 -
loading
:ローディング中 -
buffering
:バッファリング中で再生出来ない状態 -
ready
:再生可能な量データのローディング完了 -
completed
:ローディング完了
-
-
PlayerState.playing
:音声ファイルを再生中かのbool値
void _listenToPlaybackState() {
_playerStateSubscription =
_audioPlayer.playerStateStream.listen((PlayerState state) {
if (isLoadingState(state)) {
setAudioState(AudioState.loading);
} else if (isAudioReady(state)) {
setAudioState(AudioState.ready);
} else if (isAudioPlaying(state)) {
setAudioState(AudioState.playing);
} else if (isAudioPaused(state)) {
setAudioState(AudioState.paused);
} else if (hasCompleted(state)) {
setAudioState(AudioState.paused);
}
});
}
/* --- UTILITY METHODS --- */
bool isLoadingState(PlayerState state) {
return state.processingState == ProcessingState.loading ||
state.processingState == ProcessingState.buffering;
}
bool isAudioReady(PlayerState state) {
return state.processingState == ProcessingState.ready && !state.playing;
}
bool isAudioPlaying(PlayerState state) {
return state.playing && !hasCompleted(state);
}
bool isAudioPaused(PlayerState state) {
return !state.playing && !isLoadingState(state);
}
bool hasCompleted(PlayerState state) {
return state.processingState == ProcessingState.completed;
}
スライダーUI
自前でSliderを用意する事もできますが、音声メディア再生に特化したaudio_video_progress_bar
というライブラリを使っていきます
ProgressBar
クラスは音声メディア再生に特化した様々なフィールドを持っていますが、その中から今回は下記の4つを使っていきます
ProgressBar(
progress: null, // 現在の再生位置
buffered: null, // ダウンロードが完了した分数
total: null, // 音声ファイル全体の分数
onSeek: (Duration position) => null, // 再生位置の変更メソッド
),
スライダーState
Stateクラス
上記のProgressBar
クラスに対して、progress
、buffered
、total
をState側から与え続ける事で再生に伴って、スライダーを変化させていきます
State側でprogress
、buffered
、total
に対応する「現在の再生位置」、「ダウンロードが完了した分数」、「音声ファイルのトータル分数」を併せ持つProgressBarState
というクラスを作ってまとめて管理してしまいましょう
class ProgressBarState {
ProgressBarState({
this.progress,
this.buffered,
this.total,
});
final Duration progress;
final Duration buffered;
final Duration total;
}
Streamにサブスクライブ
ProgressBarState
クラスの1つ1つのフィールドの状態値はAudioPlayer
クラスの以下3つのStreamから受け取ります
-
PositionStream
: progressに対応 -
BufferedPositionStream
: bufferedに対応 -
DurationStream
: totalに対応
1本1本のStreamに個別にサブスクライブし、それぞれProgressBarState
オブジェクトを更新する様に書く事も出来ますが、記述量が多くなるので、今回はStreamの加工などが容易に出来るrx_dart
パッケージを使って、3本のStreamを1本にまとめて、それをサブスクライブしましょう
void _listenForProgressBarState() {
_progressBarSubscription = CombineLatestStream.combine3(
_audioPlayer.positionStream,
_audioPlayer.bufferedPositionStream,
_audioPlayer.durationStream,
(Duration current, Duration buffer, Duration total) => ProgressBarState(
current: current,
buffered: buffer,
total: total ?? Duration.zero,
),
).listen((ProgressBarState state) => setProgressBarState(state));
}
void setProgressBarState(ProgressBarState state) {
progressBarState = state;
notifyListeners();
}
完成
以上で完成です。様々なStreamを組み合わせて挙動を操作しているので少し複雑に見えますが、実際にはStreamの値をそのままUIに反映させているだけであまり難しい事はしていません。
バックグラウンド再生についてもこちらでご紹介しているので参考にしてみてください
参考
Discussion