Leanback な Android TV アプリに LibVLCを組み込む

#この記事では
Android Studio で Android TV むけブランクプロジェクトを作成したときに生成されるプレーヤーをVLCベースに差し替えます。

　android.media.MediaPlayer　　の代わりに VLCのエンジンである
　org.videolan.libvlc　　　　　を使えるようにします。


↓Android Studio のエミュレータで実際に再生してみたところ。
![a52bf942-d7e7-4263-b5c0-d9662cdfa0f5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/414368/cd9fbca1-19cc-3028-b15d-ce53c56cb500.png)


### 準備

まずはlibVLCが使えるようにGradleでライブラリを読み込みます。

```gradle:Gradle_file_for_Module
dependencies {

    ... 

    //libVLC
    implementation 'org.videolan.android:libvlc-all:3.4.3'
} 
```

### VLC用のPlayerAdapterの作成
つぎに、VLC用のPlayerAdapterを作っていきます。

Leanbackのパッケージですでに用意されている MediaPlayer用のPlayerAdapterである``androidx.leanback.media.MediaPlayerAdapter`` をコピーして改造していくのが手っ取り早いです。

```kotlin:VlcPlayerAdapter.kt
package com.daigorian.epcltvapp

import android.content.Context
import android.net.Uri
import android.util.Log
import android.view.SurfaceHolder
import androidx.leanback.media.PlaybackBaseControlGlue
import androidx.leanback.media.PlaybackGlueHost
import androidx.leanback.media.PlayerAdapter
import androidx.leanback.media.SurfaceHolderGlueHost
import org.videolan.libvlc.LibVLC
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.MediaPlayer.*
import java.io.IOException
import java.util.*
import kotlin.math.roundToLong

/**
 * This implementation extends the [PlayerAdapter] with a [org.videolan.libvlc.MediaPlayer].
 */
class VlcPlayerAdapter(var mContext: Context) : PlayerAdapter() {
    /**
     * Return the VlcPlayer associated with the VlcPlayerAdapter. App can use the instance
     * to config DRM or control volumes, etc.
     *
     * @return The VlcPlayer associated with the VlcPlayerAdapter.
     */

    // For LibVLC
    private val args: ArrayList<String?> = arrayListOf("")
    private val mLibVLC: LibVLC = LibVLC(mContext, args)
    private val vlcPlayer = MediaPlayer(mLibVLC)

    var mSurfaceHolderGlueHost: SurfaceHolderGlueHost? = null

    var mInitialized = false // true when the VlcPlayer is prepared/initialized
    var mMediaSourceUri: Uri? = null
    var mHasDisplay = false
    var mBufferedProgress: Long = 0
    var mBufferingStart = false
    fun notifyBufferingStartEnd() {
        callback.onBufferingStateChanged(
            this@VlcPlayerAdapter,
            mBufferingStart || !mInitialized
        )
    }

    var mDuration = -1L
    var mTime = -1L
    var mIsSeekable = false

    override fun onAttachedToHost(host: PlaybackGlueHost) {
        if (host is SurfaceHolderGlueHost) {
            mSurfaceHolderGlueHost = host
            mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(this.VideoPlayerSurfaceHolderCallback())
        }
    }

    /**
     * Will reset the [org.videolan.libvlc.MediaPlayer] and the glue such that a new file can be played. You are
     * not required to call this method before playing the first file. However you have to call it
     * before playing a second one.
     */
    fun reset() {
        changeToUninitialized()
        vlcPlayer.stop()
        if(vlcPlayer.hasMedia()){
            vlcPlayer.media?.release()
            vlcPlayer.media = null
        }
    }

    fun changeToUninitialized() {
        if(vlcPlayer.hasMedia()){
            vlcPlayer.media?.release()
            vlcPlayer.media = null
        }
        if (mInitialized) {
            mInitialized = false
            notifyBufferingStartEnd()
            if (mHasDisplay) {
                callback.onPreparedStateChanged(this@VlcPlayerAdapter)
            }
        }
    }

    /**
     * Release internal VlcPlayer. Should not use the object after call release().
     */
    fun release() {
        changeToUninitialized()
        mHasDisplay = false
        vlcPlayer.release()
        mLibVLC.release()
    }

    override fun onDetachedFromHost() {
        if (mSurfaceHolderGlueHost != null) {
            mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(null)
            mSurfaceHolderGlueHost = null
        }
        reset()
        release()
    }


    fun setDisplay(surfaceHolder: SurfaceHolder?) {
        val hadDisplay = mHasDisplay
        mHasDisplay = surfaceHolder != null
        if (hadDisplay == mHasDisplay) {
            return
        }
        if (surfaceHolder != null){
            vlcPlayer.vlcVout.setVideoSurface(surfaceHolder.surface,surfaceHolder)
            vlcPlayer.vlcVout.setWindowSize(surfaceHolder.surfaceFrame.width(),surfaceHolder.surfaceFrame.height())
            vlcPlayer.vlcVout.attachViews()
            mInitialized = true
        }else{
            mInitialized = false
            vlcPlayer.vlcVout.detachViews()
        }

        callback.onPreparedStateChanged(this@VlcPlayerAdapter)
    }




    override fun isPlaying(): Boolean {
        return vlcPlayer.isPlaying
    }

    override fun getDuration(): Long {
        return mDuration
    }

    override fun getCurrentPosition(): Long {
        return mTime
    }

    override fun play() {
        if ( vlcPlayer.isPlaying) {
            return
        }
        vlcPlayer.play()
    }

    override fun pause() {
        if (isPlaying) {
            vlcPlayer.pause()
        }
    }

    override fun seekTo(newPosition: Long) {
        if (!mInitialized) {
            return
        }
        vlcPlayer.time = newPosition
    }

    override fun getBufferedPosition(): Long {
        return mBufferedProgress
    }

    override fun getSupportedActions(): Long {

        return (PlaybackBaseControlGlue.ACTION_PLAY_PAUSE
                + PlaybackBaseControlGlue.ACTION_REWIND
                + PlaybackBaseControlGlue.ACTION_FAST_FORWARD).toLong()
    }

    /**
     * Sets the media source of the player with a given URI.
     *
     * @return Returns `true` if uri represents a new media; `false`
     * otherwise.
     */
    fun setDataSource(uri: Uri?): Boolean {
        if (if (mMediaSourceUri != null) mMediaSourceUri == uri else uri == null) {
            return false
        }
        mMediaSourceUri = uri
        prepareMediaForPlaying()
        return true
    }

    private fun prepareMediaForPlaying() {
        reset()

        try {
            val media = Media( mLibVLC, mMediaSourceUri )
            vlcPlayer.media = media
        } catch (e: IOException) {
            throw java.lang.RuntimeException("Invalid asset folder")
        }

        vlcPlayer.setEventListener { event ->
            when (event.type) {
                Event.MediaChanged -> {
                    Log.d(TAG, "libvlc Event.MediaChanged:")
                }
                Event.Opening -> {
                    Log.d(TAG, "libvlc Event.Opening")

                    if (mSurfaceHolderGlueHost == null || mHasDisplay) {
                        callback.onPreparedStateChanged(this@VlcPlayerAdapter)
                    }
                }
                Event.Buffering -> {
                    Log.d(TAG, "libvlc Event.Buffering" + event.buffering)
                    mBufferedProgress = (duration * event.buffering / 100).roundToLong()
                    callback.onBufferedPositionChanged(this@VlcPlayerAdapter)
                    if (mTime < mBufferedProgress) {
                        mBufferingStart = false
                        notifyBufferingStartEnd()
                    } else {
                        mBufferingStart = true
                        notifyBufferingStartEnd()
                    }
                }
                Event.Playing -> {
                    Log.d(TAG, "libvlc Event.Playing")
                    callback.onPlayStateChanged(this@VlcPlayerAdapter)
                }
                Event.Paused -> {
                    Log.d(TAG, "libvlc Event.Paused")
                    callback.onPlayStateChanged(this@VlcPlayerAdapter)
                }
                Event.Stopped -> {
                    // Playback of a media list player has stopped
                    Log.d(TAG, "libvlc Event.Stopped")
                }
                Event.EndReached -> {
                    // A media list has reached the end.
                    Log.d(TAG, "libvlc Event.EndReached")
                    callback.onPlayStateChanged(this@VlcPlayerAdapter)
                    callback.onPlayCompleted(this@VlcPlayerAdapter)
                }
                Event.EncounteredError -> {
                    Log.d(TAG, "libvlc Event.EncounteredError")

                    callback.onError(
                        this@VlcPlayerAdapter, 0,
                        "an error occurred"
                    )
                }
                Event.TimeChanged -> {
                    //Log.d(TAG, "libvlc Event.TimeChanged:"+ event.timeChanged)
                    mTime = event.timeChanged
                    callback.onCurrentPositionChanged(this@VlcPlayerAdapter)
                }
                Event.PositionChanged -> {
                    //Log.d(TAG, "libvlc Event.PositionChanged:"+ event.positionChanged )
                }
                Event.SeekableChanged -> {
                    Log.d(TAG, "libvlc Event.SeekableChanged:" + event.seekable)
                    mIsSeekable = event.seekable
                    callback.onMetadataChanged(this@VlcPlayerAdapter)
                }
                Event.PausableChanged -> {
                    Log.d(TAG, "libvlc Event.PausableChanged")
                }
                Event.LengthChanged -> {
                    Log.d(TAG, "libvlc Event.LengthChanged:" + event.lengthChanged)
                    mDuration = event.lengthChanged
                    callback.onDurationChanged(this@VlcPlayerAdapter)
                }
                Event.Vout -> {
                    Log.d(TAG, "libvlc Event.Vout")
                }
                Event.ESAdded -> {
                    // A track was added
                    Log.d(TAG, "libvlc Event.ESAdded")
                }
                Event.ESDeleted -> {
                    // A track was removed
                    Log.d(TAG, "libvlc Event.ESDeleted")
                }
                Event.ESSelected -> {
                    // Tracks were selected or unselected
                    Log.d(TAG, "libvlc Event.ESSelected")
                }
                Event.RecordChanged -> {
                    Log.d(TAG, "libvlc Event.RecordChanged")
                }
                else -> {
                    Log.d(TAG, "libvlc Event.Unknown")
                }


            }

        }
        notifyBufferingStartEnd()
        //callback.onPlayStateChanged(this@VlcPlayerAdapter)
    }

    /**
     * @return True if VlcPlayer OnPreparedListener is invoked and got a SurfaceHolder if
     * [PlaybackGlueHost] provides SurfaceHolder.
     */
    override fun isPrepared(): Boolean {
        return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay)
    }

    /**
     * Implements [SurfaceHolder.Callback] that can then be set on the
     * [PlaybackGlueHost].
     */
    internal inner class VideoPlayerSurfaceHolderCallback : SurfaceHolder.Callback {
        override fun surfaceCreated(surfaceHolder: SurfaceHolder) {
            setDisplay(surfaceHolder)
        }

        override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) {}
        override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
            setDisplay(null)
        }
    }

    companion object {
        private const val TAG = "VlcPlayerAdapter"

    }

}
```

### 作成した PlayerAdapter を PlaybackVideoFragment から開く。
あとは、Media Player用 PlayerAdapterを開いているPlaybackVideoFragment ２か所修正して
VLC 用のPlayerAdapterを読み込ませるだけです。

```kotlin:PlaybackVideoFragment.kt 
package com.daigorian.epcltvapp

import android.net.Uri
import android.os.Bundle
import androidx.leanback.app.VideoSupportFragment
import androidx.leanback.app.VideoSupportFragmentGlueHost
// import androidx.leanback.media.MediaPlayerAdapter
import androidx.leanback.media.PlaybackTransportControlGlue
import androidx.leanback.widget.PlaybackControlsRow

/** Handles video playback with media controls. */
class PlaybackVideoFragment : VideoSupportFragment() {

//  private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter>
    private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<VlcPlayerAdapter>　←ここと

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val (_, title, description, _, _, videoUrl) =
            activity?.intent?.getSerializableExtra(DetailsActivity.MOVIE) as Movie

        val glueHost = VideoSupportFragmentGlueHost(this@PlaybackVideoFragment)
//      val playerAdapter = MediaPlayerAdapter(context)
        val playerAdapter = VlcPlayerAdapter(context)　←ここ
        playerAdapter.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE)

        mTransportControlGlue = PlaybackTransportControlGlue(getActivity(), playerAdapter)
        mTransportControlGlue.host = glueHost
        mTransportControlGlue.title = title
        mTransportControlGlue.subtitle = description
        mTransportControlGlue.playWhenPrepared()

        playerAdapter.setDataSource(Uri.parse(videoUrl))
    }

    override fun onPause() {
        super.onPause()
        mTransportControlGlue.pause()
    }
} 
```


意外と簡単❣