Integrar ExoPlayer con Android Leanback

Leanback es la librería de Android que facilita el desarrollar aplicaciones para Android TV, muchas de estas aplicaciones requieren o hacen uso de un reproductor multimedia, por lo que en este tutorial veremos como integrar el reproductor ExoPlayer.

Lo primero crear nuestro proyecto de la siguiente manera:

Con esto creamos un proyecto vacío, lo que haremos ahora será agregar nuestra actividad, por ahora con el siguiente código:

public class LeanbackActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leanback);
    }
}

Debemos hacer las siguientes modificaciones en el AndroidManifest.xml el primer elemento para indicar si nuestra app se utilizará solo en dispositivos que utilicen la UI Leanback o también podrá ejecutarse en otros dispositivos en este ultimo caso establecemos el atributo a false como lo haremos nosotros, en el segundo indicaremos que no es necesario disponer de una pantalla táctil además se requiere para que Google Play identifique la aplicación como una de Android TV.

    <uses-feature android:name="android.software.leanback" android:required="false" />
    <uses-feature android:name="android.hardware.touchscreen" android:required="false" />

Para que la actividad se pueda iniciar debemos establecer el CATEGORY_LEANBACK_LAUNCHER, también se requiere de definamos el atributo android:banner de application este debe ser una imagen de 320 x 180

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.Exoplayerleanback"
        android:banner="@mipmap/ic_launcher">

        <activity
            android:name=".LeanbackActivity"
            android:theme="@style/Theme.Exoplayerleanback">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

Con esto ya podemos correr la app aunque no veremos nada en pantalla, el siguiente paso es agregar el reproductor que en nuestro caso usaremos para reproducir contenido en streaming en formato HLS para ello utilizaremos ExoPlayer en la versión 2.12.x al momento de la redacción.

Para usar este reproductor agregamos la siguiente dependencia: com.google.android.exoplayer:exoplayer:2.X.X y com.google.android.exoplayer:extension-leanback:2.12.1 además cambiamos la versión de compilación de Java a 1.8

android {
    compileSdkVersion 29
    buildToolsVersion "30.0.2"

    defaultConfig {...}
    buildTypes {...}

    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.leanback:leanback:1.0.0'
    implementation 'com.google.android.exoplayer:exoplayer:2.12.1'
    implementation 'com.google.android.exoplayer:extension-leanback:2.12.1'

}

No debemos olvidar agregar los permisos para poder acceder a internet ya que vamos a reproducir contenido online.

Al definir el layout de la actividad añadiremos un PlayerView y un fragment el primero hace referencia al reproductor ExoPlayer y el segundo lo usaremos para controlar el mismo, el código XML quedará de la siguiente manera:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LeanbackActivity">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:use_controller="false"/>

    <fragment
        android:id="@+id/video_fragment"
        android:name="com.carmelo.exoplayer_leanback.LeanbackFragmentPlayer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</FrameLayout>

Es importante mencionar que establecimos en atributo use_controller a false porque deseamos ocultar los controles por defecto del reproductor y utilizar los controles de la UI Leanback.

Para incluir los controles Leanback el Fragment de extender la clase PlaybackSupportFragment aquí crearemos los métodos para inicializar y controlar la reproducción de contenido, mostramos los métodos preparePlayer(...) el cual se encarga de preparar el reproductor para visualizar el contenido indicado e initPlayer(...) el cual inicializa el reproductor y lo adapta a la UI Leanback.

public class LeanbackFragmentPlayer extends PlaybackSupportFragment
        implements Player.EventListener, VideoPlayerGlue.OnActionClickedListener {

    private SimpleExoPlayer player;
    private VideoPlayerGlue playerGlue;

    private void preparePlayer(Context context, ExoPlayer player, String videoUri) {

        // Produces DataSource instances through which media data is loaded.
        DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
                Util.getUserAgent(context, "receiver"));

        // This is the MediaSource representing the media to be played.
        MediaSource videoSource =
                new HlsMediaSource.Factory(dataSourceFactory)
                        .createMediaSource(MediaItem.fromUri(videoUri));

        // Prepare the player with the source.
        player.setMediaSource(videoSource);
        player.prepare();
    }

    private void initPlayer() {

        PlayerView playerView = getActivity().findViewById(R.id.player);

        player = new SimpleExoPlayer.Builder(getContext()).build();
        playerView.setPlayer(player);

        PlayerAdapter playerAdapter = new LeanbackPlayerAdapter(getContext(), player, 16);

        playerGlue = new VideoPlayerGlue(getContext(), playerAdapter, this);
        playerGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
        playerGlue.playWhenPrepared();

        setAdapter(new ArrayObjectAdapter());

        String url = "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8";
        preparePlayer(getContext(), player, url);
    }
    
    //...
}    

En initPlayer(...) le prestamos atención a la clase LeanbackPlayerAdapter esta no sirve de puente para conectar la UI con ExoPlayer además tenemos VideoPlayerGlue esta última la definimos nosotros y nos servirá para dar funcionalidad a cada uno de los controles del reproductor, podremos agregar o personalizar las funcionalidades del mismo, esta clase se define de la siguiente manera:

public class VideoPlayerGlue extends PlaybackTransportControlGlue<PlayerAdapter> {

    private static final long TEN_SECONDS = TimeUnit.SECONDS.toMillis(10);

    public interface OnActionClickedListener {
        void onPrevious();
        void onNext();
    }

    private final OnActionClickedListener mActionListener;

    private PlaybackControlsRow.RepeatAction mRepeatAction;
    private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
    private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
    private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
    private PlaybackControlsRow.SkipNextAction mSkipNextAction;
    private PlaybackControlsRow.FastForwardAction mFastForwardAction;
    private PlaybackControlsRow.RewindAction mRewindAction;
    private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptionAction;
    private PlaybackControlsRow.ShuffleAction mShuffleAction;
    private PlaybackControlsRow.PictureInPictureAction mPictureAction;

    public VideoPlayerGlue(
            Context context,
            PlayerAdapter playerAdapter,
            OnActionClickedListener actionListener) {

        super(context, playerAdapter);

        mActionListener = actionListener;

        mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(context);
        mSkipNextAction = new PlaybackControlsRow.SkipNextAction(context);
        mFastForwardAction = new PlaybackControlsRow.FastForwardAction(context);
        mRewindAction = new PlaybackControlsRow.RewindAction(context);

        mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
        mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
        mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
        mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
        mRepeatAction = new PlaybackControlsRow.RepeatAction(context);

        mClosedCaptionAction = new PlaybackControlsRow.ClosedCaptioningAction(context);
        mShuffleAction = new PlaybackControlsRow.ShuffleAction(context);
        mPictureAction = new PlaybackControlsRow.PictureInPictureAction(context);

    }

    @Override
    protected void onCreatePrimaryActions(ArrayObjectAdapter adapter) {
        super.onCreatePrimaryActions(adapter);

        adapter.add(mRewindAction);
        adapter.add(mSkipPreviousAction);
        adapter.add(mSkipNextAction);
        adapter.add(mFastForwardAction);
    }

    @Override
    protected void onCreateSecondaryActions(ArrayObjectAdapter adapter) {...}

    @Override
    public void onActionClicked(Action action) {
        if (shouldDispatchAction(action)) {

            if(action == mFastForwardAction) {
                getPlayerAdapter().seekTo(getPlayerAdapter().getCurrentPosition() + 60000);
            }

            return;
        }
        // Super class handles play/pause and delegates to abstract methods next()/previous().
        super.onActionClicked(action);
    }

    // Should dispatch actions that the super class does not supply callbacks for.
    private boolean shouldDispatchAction(Action action) {
        return action == mRewindAction
                || action == mFastForwardAction
                || action == mThumbsDownAction
                || action == mThumbsUpAction
                || action == mRepeatAction
                || action == mShuffleAction
                || action == mClosedCaptionAction
                || action == mPictureAction;
    }

    @Override
    public void next() {
        mActionListener.onNext();
    }

    @Override
    public void previous() {
        mActionListener.onPrevious();
    }
    
}

Con esto tenemos lo siguiente:

El código completo se puede descargar en el siguiente enlace: Exoplayer-leanback code

 

Comentarios

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Conectar SQL Server con Java

Histogramas OpenCV Python