はじめまして! OPENREC でアルバイトをしている22年度新卒入社の四宮(@1125__rui)です。

今回はOPENRECのAndroidアプリに新しく組み込んだMediaControlsというAndroidの機能を紹介したいと思います。

目次

MediaControlsとは

MediaControlsとはAndroid11からサポートされた新しい機能です。

MediaControlsを実装することでクイック設定パネルの下にメディアコントロール用の特殊な領域が設定され、MediaSessionを通して再生しているコンテンツが表示されるようになります。

最大5つまで再生コンテンツが表示され、表示領域はカルーセルになっているので切り替えて確認可能です。

MediaControlsを組み込むことで以下のメリットが得られます

  • 再生メディアの通知が大量に表示され、他の通知を圧迫することがなくなる
  • 再生デバイスの切り替え、コンテンツのシークがパネルから可能になる
  • 再生コンテンツは半永続的に表示されるのでアプリを開かずともパネルから再生の再開が可能になる

またCompatライブラリを利用することで、Android10以前でもメディア通知として以下のような表示が可能です。

MediaControlsの例です

MediaControlsを表示する

MediaControlsは

  • 通知をメディアスタイルで表示する
  • MediaSessionを再生機構に組み込んでいる

の2点を実装することで自動的に表示されます。


private lateinit var mediaSession: MediaSessionCompat
private fun setup() {
    // MediaSessionの作成
    mediaSession = MediaSessionCompat(context, "MediaService")
		
    // MediaStyleにMediaSessionのトークンを渡す
    val style = 
	NotificationCompat.MediaStyle()
	    .setMediaSession(mediaSession.sessionToken)
    val notification = 
        NotificationCompat
            .Builder(this@Service, CHANNEL_ID)
            .setStyle(style)
            .setSmallIcon(R.drawable.icon)
            .build()

    // 通常の通知として出す場合
    notificationManager.notify(notificationId, notification)
    // Serviceで始める場合
    startForeground("SERVICE_ID", notification)		
}

通常の通知同様に再生・停止ボタンのようなカスタムしたアクションをトリガーするボタンを設定することも可能です。設定したアクションは通知の下部にセンタリングされて表示されます。


private fun setup() {
    .....
    // アクションの作成
    val pauseAction: NotificationCompat.Action = 
        NotificationCompat.Action.Builder(pauseIcon, "Pause", pauseIntent)
            .build()
    val disableVideoAction: NotificationCompat.Action =
        NotificationCompat.Action.Builder(disableVideoIcon, "DisableVideo", disableVideoIntent)
            .build()
    val stopAction: NotificationCompat.Action = 
	NotificationCompat.Action.Builder(stopIcon, "Stop", stopIntent)
            .build()

    // 左から順に並びます
    notification.addAction(pauseAction)
    notification.addAction(disableVideoAction)
    notification.addAction(stopAction)
}

カスタムアクションを設定したMediaControlsの例です

通知がたたまれている状態でも表示するカスタムボタンを指定することも可能です。
ただし一つしか表示できず、一番最後に設定したものが表示されます。


private fun setup() {
    ......
		
    // 左から数えて0番目に設定されているアクションを表示する
    style.setShowActionsInCompactView(0)
}

折り畳まれたMediaControlsの例です
MediaSessionについての詳細を知りたい場合はこちらも合わせて参考にしてください。

MediaMetadataの設定

メディア通知に表示するコンテンツの詳細情報はMediaMetadataを通して設定することが可能です。MediaMetadataを通して設定した場合は常時表示画面にも情報が表示されるのでNotificationよりもMediaMetadataに設定する形がおすすめです。


private lateinit var mediaSession: MediaSessionCompat

private fun setup() {
    val metadata = 
        MediaMetadataCompat.Builder()
            .putString(MediaMetadata.METADATA_KEY_TITLE, "しのみーの配信です")
            .putString(MediaMetadata.METADATA_KEY_ARTIST, "しのみー")
            .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, albumBitmap)
            .putLong(MediaMetadata.METADATA_KEY_DURATION, 18)
		
    mediaSession.setMetadata(metadata.build())
}

メタデータを設定したMediaControlsの例です

ロック画面と常時表示画面での表示例です

注意点として以下があげられます。

  • NotificationとMediaSessionの両方にメタデータを設定した際はMediaSessionに設定した内容が優先される
  • METADATA_KEY_DURATIONをメタデータとして設定しないとシークバーが表示されない
  • Android10以下ではMediaMetadataへの設定のみだと正しく通知にメタデータが表示されないので、バージョンで分岐させてNotificationに直接設定する形を取るか、MediaMetadataとNotificationの両方にメタデータを設定する必要があります

実際にMediaControlsを表示する際は、ExoPlayerやMediaPlayer等のメディア再生機構とMediaSessionを連携させて利用することになると思います。

OPENRECではメディアの再生にExoPlayerを用いているためExoPlayerとMediaSessionを同期する必要がありました。ここではExoPlayerとMediaSessionについて触れたいと思います。

ExoPlayerにはMediaSessionを組み込むためのExoPlayer MediaSession ExtensionMedia2 Extensionが用意されており、これを利用することでExoPlayerとMediaSessionを連携することができます。Media2 Extensionの方がより外部システムに対して連携がしやすく、きめ細やかな制御が期待できますが、今回はそこまでの機能が必要でなかったのでMediaSessionExtensionを採用しています。

ExoPlayerのMediaSessionExtensionが行ってくれる操作は以下の通りです

  •  Playerの状態からPlaybackStateへの自動マッピング
  •  Playerの対応するメソッドごとの基本的な再生アクション(ACTION_PLAY_PAUSE、ACTION_PLAY、ACTION_SEEK_TO、ACTION_FAST_FORWARD、ACTION_REWIND、ACTION_STOP)の自動対応

private lateinit var player: SimpleExoPlayer
private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaSessionConnector: MediaSessionConnector

private fun setup() {
    mediaSession = MediaSession(context, "MediaSession")
    mediaSessionConnector = MediaSessionConnector(mediaSession)

    // mediaSessionConnectorにExoPlayerのPlayerを渡す
    mediaSessionConnector.setPlayer(player)
}

MediaSessionConnectorMediaSession.setActiveを自動で呼び出さないため、プレイヤーの再生状態が変更されたタイミングでセッションのアクティブを自分で切り替える必要があります。
callbackを通じて設定する形をとるか、onStart()onStop()で設定する形があります。
通常のServiceではなくMediaBrowserServiceを実装している場合は前者をおすすめします。


private lateinit var mediaSession: MediaSessionCompat
private val sessionCallback = object : MediaSessionCompat.Callback() {
    override fun onPrepare() { ..... }
    override fun onPause() { ..... }
    override fun onPlay() {
        mediaSession.isActive = true
    }
    override fun onStop() {
        mediaSession.isActive = false
    }
}

private fun setup() {
    .....
    mediaSession.setCallback(sessionCallback)
}

また、シーク操作を有効にしたい場合はPlaybackStateを設定する際にACTION_SEEK_TOを有効にする必要があります。


private lateinit var mediaSession: MediaSessionCompat
private lateinit var player: SimpleExoPlayer
private val callback = object : MediaSessionCompat.Callback() {
    override fun onPlay() {
        setPlayState(PlaybackStateCompat.STATE_PLAYING)
    }
    override fun onStop() {
        setPlayState(PlaybackStateCompat.STATE_STOPPED)
    }
}

private fun setup() {
    .....
    player.addListener(object : Player.EventListener {
        // Playerの状態が変わるたびに呼び出されるcallback
        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
            setPlayState(playbackState)
        }
    })
}

private fun setPlayState(state: PlaybackStateCompat) {
    val bulder = 
        PlaybackStateCompat.Builder()
            // 有効にしたいActionにACTION_SEEK_TOを組み込む
            .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
	    // 現在の再生位置と再生速度を設定する
            .setState(state, currentPosition, 1.0f)
    mediaSession.setPlaybackState(builder.build())
}

おわりに

今回はMediaControlsを紹介しました。
MediaControlsを導入することによって通知欄を通したメディアの操作性を大幅に向上させることができたのではないかと思います。
最後まで読んでいただきありがとうございました。