From c9393db8789edfacdaff47fd7254990d11c3ff0f Mon Sep 17 00:00:00 2001 From: susnata Date: Mon, 14 Aug 2017 08:53:03 -0700 Subject: [PATCH] Creating an leanback extension for ExoPlayer. Leanback added support for new transport control, which allows developers to plug in any media player. This extension provides the PlayerAdapter implementation for ExoPlayer. Demo: https://drive.google.com/open?id=0B1GHUu5ruGULZTJVV1pVNlBuVjQ ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165183497 --- core_settings.gradle | 2 + extensions/leanback/README.md | 26 ++ extensions/leanback/build.gradle | 41 +++ .../leanback/src/main/AndroidManifest.xml | 17 + .../ext/leanback/LeanbackPlayerAdapter.java | 314 ++++++++++++++++++ 5 files changed, 400 insertions(+) create mode 100644 extensions/leanback/README.md create mode 100644 extensions/leanback/build.gradle create mode 100644 extensions/leanback/src/main/AndroidManifest.xml create mode 100644 extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java diff --git a/core_settings.gradle b/core_settings.gradle index 20e7b235a2..7a8320b1a1 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -33,6 +33,7 @@ include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-opus' include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' +include modulePrefix + 'extension-leanback' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -50,6 +51,7 @@ project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'exten project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') +project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') if (gradle.ext.has('exoplayerIncludeCronetExtension') && gradle.ext.exoplayerIncludeCronetExtension) { diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md new file mode 100644 index 0000000000..2326887724 --- /dev/null +++ b/extensions/leanback/README.md @@ -0,0 +1,26 @@ +# ExoPlayer Leanback Extension # + +## Description ## + +This [Leanback][] Extension provides a [PlayerAdapter][] implementation for +Exoplayer. + +[PlayerAdapter]: https://developer.android.com/reference/android/support/v17/leanback/media/PlayerAdapter.html +[Leanback]: https://developer.android.com/reference/android/support/v17/leanback/package-summary.html + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-leanback:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle new file mode 100644 index 0000000000..eaf58cc990 --- /dev/null +++ b/extensions/leanback/build.gradle @@ -0,0 +1,41 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(modulePrefix + 'library-core') + compile('com.android.support:leanback-v17:' + supportLibraryVersion) +} + +ext { + javadocTitle = 'Leanback extension for Exoplayer library' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-leanback' + releaseDescription = 'Leanback extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/leanback/src/main/AndroidManifest.xml b/extensions/leanback/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..20cc9bf285 --- /dev/null +++ b/extensions/leanback/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java new file mode 100644 index 0000000000..58c8ee973d --- /dev/null +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.leanback; + +import android.content.Context; +import android.os.Handler; +import android.support.v17.leanback.R; +import android.support.v17.leanback.media.PlaybackGlueHost; +import android.support.v17.leanback.media.PlayerAdapter; +import android.support.v17.leanback.media.SurfaceHolderGlueHost; +import android.util.Pair; +import android.view.SurfaceHolder; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.ErrorMessageProvider; + +/** + * Leanback {@link PlayerAdapter} implementation for {@link SimpleExoPlayer}. + */ +public final class LeanbackPlayerAdapter extends PlayerAdapter { + + private final Context context; + private final SimpleExoPlayer player; + private final Handler handler; + private final Runnable updatePlayerRunnable = new Runnable() { + @Override + public void run() { + getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); + getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + handler.postDelayed(this, updatePeriod); + } + }; + + private SurfaceHolderGlueHost surfaceHolderGlueHost; + private boolean initialized; + private boolean hasDisplay; + private boolean isBuffering; + private ErrorMessageProvider errorMessageProvider; + private final int updatePeriod; + private final ExoPlayerEventListenerImpl exoPlayerListener = new ExoPlayerEventListenerImpl(); + private final SimpleExoPlayer.VideoListener videoListener = new SimpleExoPlayer.VideoListener() { + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); + } + + @Override + public void onRenderedFirstFrame() { + } + }; + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.leanback"); + } + + /** + * Constructor. + * Users are responsible for managing {@link SimpleExoPlayer} lifecycle. You must + * stop/release the player once you're done playing the media. + * + * @param context The current context (activity). + * @param player Instance of your exoplayer that needs to be configured. + */ + public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, int updatePeriod) { + this.context = context; + this.player = player; + this.handler = new Handler(); + this.updatePeriod = updatePeriod; + } + + @Override + public void onAttachedToHost(PlaybackGlueHost host) { + if (host instanceof SurfaceHolderGlueHost) { + surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); + surfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback()); + } + initializePlayer(); + } + + private void initializePlayer() { + notifyListeners(); + this.player.addListener(exoPlayerListener); + this.player.setVideoListener(videoListener); + } + + private void notifyListeners() { + boolean oldIsPrepared = isPrepared(); + int playbackState = player.getPlaybackState(); + boolean isInitialized = playbackState != ExoPlayer.STATE_IDLE; + isBuffering = playbackState == ExoPlayer.STATE_BUFFERING; + boolean hasEnded = playbackState == ExoPlayer.STATE_ENDED; + + initialized = isInitialized; + if (oldIsPrepared != isPrepared()) { + getCallback().onPreparedStateChanged(LeanbackPlayerAdapter.this); + } + + getCallback().onPlayStateChanged(this); + notifyBufferingState(); + + if (hasEnded) { + getCallback().onPlayCompleted(this); + } + } + + /** + * Sets the optional {@link ErrorMessageProvider}. + * + * @param errorMessageProvider The {@link ErrorMessageProvider}. + */ + public void setErrorMessageProvider( + ErrorMessageProvider errorMessageProvider) { + this.errorMessageProvider = errorMessageProvider; + } + + private void uninitializePlayer() { + if (initialized) { + initialized = false; + notifyBufferingState(); + if (hasDisplay) { + getCallback().onPlayStateChanged(LeanbackPlayerAdapter.this); + getCallback().onPreparedStateChanged(LeanbackPlayerAdapter.this); + } + + player.removeListener(exoPlayerListener); + player.clearVideoListener(videoListener); + } + } + + /** + * Notify the state of buffering. For example, an app may enable/disable a loading figure + * according to the state of buffering. + */ + private void notifyBufferingState() { + getCallback().onBufferingStateChanged(LeanbackPlayerAdapter.this, + isBuffering || !initialized); + } + + @Override + public void onDetachedFromHost() { + if (surfaceHolderGlueHost != null) { + surfaceHolderGlueHost.setSurfaceHolderCallback(null); + surfaceHolderGlueHost = null; + } + uninitializePlayer(); + hasDisplay = false; + } + + @Override + public void setProgressUpdatingEnabled(final boolean enabled) { + handler.removeCallbacks(updatePlayerRunnable); + if (!enabled) { + return; + } + handler.postDelayed(updatePlayerRunnable, updatePeriod); + } + + @Override + public boolean isPlaying() { + return initialized && player.getPlayWhenReady(); + } + + @Override + public long getDuration() { + long duration = player.getDuration(); + return duration != C.TIME_UNSET ? duration : -1; + } + + @Override + public long getCurrentPosition() { + return initialized ? player.getCurrentPosition() : -1; + } + + @Override + public void play() { + if (player.getPlaybackState() == ExoPlayer.STATE_ENDED) { + seekTo(0); + } + player.setPlayWhenReady(true); + getCallback().onPlayStateChanged(this); + } + + @Override + public void pause() { + player.setPlayWhenReady(false); + getCallback().onPlayStateChanged(this); + } + + @Override + public void seekTo(long newPosition) { + player.seekTo(newPosition); + } + + @Override + public long getBufferedPosition() { + return player.getBufferedPosition(); + } + + /** + * @return True if ExoPlayer is ready and got a SurfaceHolder if + * {@link PlaybackGlueHost} provides SurfaceHolder. + */ + @Override + public boolean isPrepared() { + return initialized && (surfaceHolderGlueHost == null || hasDisplay); + } + + /** + * @see SimpleExoPlayer#setVideoSurfaceHolder(SurfaceHolder) + */ + private void setDisplay(SurfaceHolder surfaceHolder) { + hasDisplay = surfaceHolder != null; + player.setVideoSurface(surfaceHolder.getSurface()); + getCallback().onPreparedStateChanged(this); + } + + /** + * Implements {@link SurfaceHolder.Callback} that can then be set on the + * {@link PlaybackGlueHost}. + */ + private final class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + setDisplay(surfaceHolder); + } + + @Override + public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + setDisplay(null); + } + } + + private final class ExoPlayerEventListenerImpl implements ExoPlayer.EventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + LeanbackPlayerAdapter.this.notifyListeners(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + String errMsg = ""; + if (errorMessageProvider != null) { + Pair message = errorMessageProvider.getErrorMessage(exception); + if (message != null) { + getCallback().onError(LeanbackPlayerAdapter.this, + message.first, + message.second); + return; + } + } + getCallback().onError(LeanbackPlayerAdapter.this, + exception.type, + context.getString(R.string.lb_media_player_error, + exception.type, + exception.rendererIndex)); + } + + @Override + public void onLoadingChanged(boolean isLoading) { + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + getCallback().onDurationChanged(LeanbackPlayerAdapter.this); + getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); + getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + } + + @Override + public void onPositionDiscontinuity() { + getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); + getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters params) { + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + } + } +} +