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 super ExoPlaybackException> 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 super ExoPlaybackException> 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) {
+ }
+ }
+}
+