LoginSignup
0
0

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

Last updated at Posted at 2021-10-13

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

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

↓Android Studio のエミュレータで実際に再生してみたところ。
a52bf942-d7e7-4263-b5c0-d9662cdfa0f5.png

準備

まずはlibVLCが使えるように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 をコピーして改造していくのが手っ取り早いです。

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 2か所修正して
VLC 用のPlayerAdapterを読み込ませるだけです。

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()
    }
} 

意外と簡単❣

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0