【Kotlin研修10日目】メディアファイルの再生、MediaPlayerのライフサイクル

# メディアファイルの再生
`メディアファイル`(=`音声`・`動画`ファイル)は、`MediaPlayer`クラスを用いて再生する。
`メディアファイル`を再生する手順は、以下の通り。

> 1. `MediaPlayer`オブジェクトの生成
> 2. `メディアファイル`の`URI`を指定
> 3. `MediaPlayer`オブジェクトのリスナ定義
> 4. `非同期`による再生準備
> 5. `MediaPlayer`オブジェクトの解放

## MediaPlayerオブジェクトの生成
`MediaPlayer()`での`MediaPlayer`オブジェクトの生成時、`MediaPlayer`オブジェクトは`Idle`状態から始まる。

### MediaPlayer
`メディアファイル`を制御する`プレーヤー`を定義するクラス。

### MediaPlayerのライフサイクル

|ライフサイクル|メソッド|呼び出しタイミング|
|:--|:--|:--|
|`Idle`|`MediaPlayer()`<br>または<br>`reset()`|`オブジェクト`の生成|
|`Initialized`|`setDataSource()`|`メディアファイル`の定義|
|`Preparing`|`prepareAsync()`|`非同期`での`メディアファイル`の準備開始|
|`Prepared`|`OnPreparedListener.onPrepared()`<br>または<br>`prepare()`|`メディアファイル`の準備完了|
|`Started`|`start()`|`メディアファイル`の`再生`|
|`Paused`|`pause()`|`メディアファイル`の`一時停止`|
|`PlaybackCompleted`|`OnCompletionListener.onCompletion()`|`メディアファイル`の`再生完了`|
|`Stopped`|`stop()`|`メディアファイル`の`停止`|
|`End`|`release()`|`オブジェクト`の解放|
|`Error`|`OnErrorListener.onError()`|`エラー`の発生|

![MediaPlayer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/77cff2a4-e01d-3b93-cd66-57b6175fb03e.png)
[出典: MediaPlayer](https://www.geeksforgeeks.org/mediaplayer-class-in-android/)

### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // MediaPlayerプロパティ
    // -> オブジェクト解放時はnullにするため、Nullable型で宣言
    private var _player: MediaPlayer? = null

    // アクティビティ初期化時の処理
    override fun onCreate(savedInstanceState: Bundle?) {
        // MediaPlayerオブジェクトの生成
        _player = MediaPlayer()
        ...
    }
    ...
}
```

## 再生するメディアファイルのURI指定
生成した`MediaPlayer`オブジェクトに制御対象の`リソースファイル`を定義することで、`Initialized`状態に遷移させる。

なお、`リソースファイル`は`res`フォルダ内に`Resource Type: raw`の`Android Resource Directory`を作成し、作成したディレクトリ内に配置しておく。

### 定義

```kotlin
MediaPlayer.setDataSource(
    context: Context,
    uri: Uri
): Unit
// パラメータ
// context: MediaPlayerを利用するアクティビティオブジェクト(コンテキスト)
// uri: メディアファイルのURI
```

### String型→Uri型の変換

#### 定義

```kotlin
Uri.parse(uriString: String!): Uri!
// パラメータ
// uriString: Uri型に変換するUri文字列
```

### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // MediaPlayerプロパティ
    private var _player: MediaPlayer? = null

    // アクティビティ初期化時の処理
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // メディアファイルのURI文字列
        val mediaFileUriStr = "android.resource://${packageName}/${R.raw.<メディアファイル名>}"

        // String型 → URI型 への変換
        val mediaFileUri = Uri.parse(mediaFileUriStr)

        // MediaPlayerがnullでない場合の処理
        _player?.let {
            // メディアファイルの指定
            it.setDataSource(this@MainActivity, mediaFileUri)
            ...
        }
    }
}
```

## MediaPlayerオブジェクトへのリスナ定義
`メディアファイル`の`準備完了(イベント)`を検知する`MediaPlayer.OnPreparedListener`インタフェースを実装したリスナクラスと、
`メディアファイル`の`再生完了(イベント)`を検知する`MediaPlayer.OnCompletionListener`インタフェースを実装したリスナクラスを定義する。

### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // MediaPlayerプロパティ
    private var _player: MediaPlayer? = null

    // アクティビティ初期化時の処理
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // MediaPlayerがnullでない場合の処理
        _player?.let {
            ...
            // メディアファイルの準備完了時のリスナ定義
            it.setOnPreparedListener(PlayerPreparedListener())

            // メディアファイルの再生完了時のリスナ定義
            it.setOnCompletionListener(PlayerCompletionListener())
            ...
        }

    // "メディアファイルの準備完了"イベントを検知するリスナクラス
    private inner class PlayerPreparedListener: MediaPlayer.OnPreparedListener {
        // "メディアファイルの準備"完了時の処理
        override fun onPrepared(mp: MediaPlayer?) {
            ...
        }
    }

    // "メディアファイルの再生完了"イベントを検知するリスナクラス
    private inner class PlayerCompletionListener: MediaPlayer.OnCompletionListener {
        // "メディアファイルの再生"完了時の処理
        override fun onCompletion(mp: MediaPlayer?) {
            ...
        }
    }
    ...
}
```

## 非同期によるメディアファイルの再生準備
インターネットの`ストリーミング再生`は一般的に処理に負荷がかかるため、
`同期`でメディアの再生準備を行う`prepare()`メソッドではなく、`非同期`で再生準備を行う`prepareAsync()`メソッドを用いる。

### 定義

```kotlin
MediaPlayer.prepareAsync()
```

### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // MediaPlayerプロパティ
    private var _player: MediaPlayer? = null

    // アクティビティ初期化時の処理
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // MediaPlayerがnullでない場合の処理
        _player?.let {
            ...
            // 非同期でのMediaPlayerオブジェクトの準備
            it.prepareAsync()
        }
        ...
    }
    ...
}
```

## MediaPlayerオブジェクトの解放
`MediaPlayer`は`システムリソース`を消費するため、`アクティビティ`の終了時(=`onDestroy()`)に`MediaPlayer`オブジェクトを**解放**する必要がある。

また、`アクティビティクラス`で`MediaPlayer`をプロパティとして宣言している場合、`MediaPlayer`オブジェクトを確実に解放すると同時に`プロパティ`の値を`null`にしておくことが推奨される。

### 定義

```kotlin
MediaPlayer.release()
```

### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // MediaPlayerプロパティ
    // -> オブジェクト解放時はnullにするため、Nullable型で宣言
    private var _player: MediaPlayer? = null
    ...

    // アクティビティ終了時の処理
    override fun onDestroy() {

        // MediaPlayerがnullでない場合の処理
        // -> let関数ブロック内のitは非Nullable型であるため、
        //    let関数内では_playerの値をnullにできない
        _player?.let {

            // MediaPlayerが再生中であれば、安全のために停止
            if (it.isPlaying) {
                // MediaPlayerの停止
                it.stop()
            }

            // MediaPlayerオブジェクトの解放
            it.release()
        }

        // MediaPlayerをnullに変更
        _player = null

        // アクティビティの終了
        super.onDestroy()
    }
}
```

***

# プレーヤーの操作
`メディアファイル`の制御を行う`MediaPlayer`クラスのメソッドを用いてプレーヤーを操作する。

ただし、メディアファイルの`停止`を行う`stop()`メソッドを用いた場合、
再度`再生`する場合は`prepare()`メソッドを用いて`再生準備`を行う必要がある。
※[上図]()を参照

## メディアファイルの再生・一時停止・停止

|メソッド|内容|
|:--|:--|
|`start()`|再生|
|`pause()`|一時停止|
|`stop()`|停止|

## 再生位置(シーク)の操作
`メディアファイル`の`再生位置`(=`シーク`)を変更する場合は、`MediaPlayer`の`seekTo()`メソッドを用いる。

### 定義

```kotlin
MediaPlayer.seekTo(msec: Int): Unit
// パラメータ
// msec: 開始位置からのオフセット[ms]
```

### メディアファイルの先頭に戻る

#### サンプルコード

```kotlin:MainActivity.kt
MediaPlayer.seekTo(0)
```

### メディアファイルの末尾に進む(再生を完了させる)
`メディアファイル`の再生を完了させるには、`MediaPlayer`クラスの**現在セットされている**`メディアファイル`の再生時間を保持する`duration`プロパティを用いて`シーク操作`を行う。

なお、`メディアファイル`の一時停止(=`Paused`状態)中に終了位置まで`シーク操作`を行った場合、
`MediaPlayer`は依然として`再生完了`(=`PlaybackCompleted`状態)直前の`Paused`状態であるため、
`start()`メソッドを用いて`PlaybackCompleted`状態に遷移させる必要がある。

#### サンプルコード

```kotlin.MainActivity.kt
// MediaPlayerがnullでない場合の処理
MediaPlayer?.let {

    // 再生中のメディアファイルの長さ
    val duration = it.duration

    // 再生位置を末尾に変更
    it.seekTo(duration)

    // MediaPlayerが再生中でない場合の処理
    if (!it.isPlaying) {
        // MediaPlayerの再生
        it.start()
    }
}
```

## ループ機能
現在セットされている`メディアファイル`を`ループ`させるには、`MediaPlayer`クラスの`isLooping`プロパティを操作する。

`ループ機能`の実装にあたって、一般的には`トグルスイッチ`が用いられる。

### トグルスイッチの実装
`トグルスイッチ`は`Boolean`型の値を保持するボタンであり、`チェックボックス`や`ラジオボタン`と同様、`CompoundButton`クラスを継承している。

#### CompoundButtonクラス
`Boolean`型の値を保持する`ボタン`を定義するクラス。

#### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // MediaPlayerプロパティ
    // -> オブジェクト解放時はnullにするため、Nullable型で宣言
    private var _player: MediaPlayer? = null

    // アクティビティ初期化時の処理
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // MediaPlayerオブジェクトの生成
        _player = MediaPlayer()

        // トグルスイッチ
        val loopSwitch = findViewById<SwitchMaterial>(R.id.swLoop)
        ...
        // トグルスイッチをリスナとしてセット
        loopSwitch.setOnCheckedChangeListener(LoopSwitchChangedListener())
    }

    // ループスイッチの"状態遷移"イベントを検知するリスナクラス
    private inner class LoopSwitchChangedListener: CompoundButton.OnCheckedChangeListener {
        // "状態遷移"イベント検知時の処理
        override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
            // ループ機能の変更
            _player?.isLooping = isChecked
        }
    }
}
```