// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media.remote;
import android.graphics.Bitmap;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
/**
* Acts as a proxy between the remotely playing video and the HTMLMediaElement.
*/
@JNINamespace("remote_media")
public class RemoteMediaPlayerBridge {
private long mStartPositionMillis;
private long mNativeRemoteMediaPlayerBridge;
/**
* The route controller for the video, null if no appropriate route controller.
*/
private final MediaRouteController mRouteController;
private final String mOriginalSourceUrl;
private final String mOriginalFrameUrl;
private String mFrameUrl;
private String mSourceUrl;
private final String mUserAgent;
private Bitmap mPosterBitmap;
private String mCookies;
private boolean mPauseRequested;
private boolean mSeekRequested;
private long mSeekLocation;
private boolean mIsPlayable;
private boolean mRouteIsAvailable;
// mActive is true when the Chrome is playing, or preparing to play, this player's video
// remotely.
private boolean mActive = false;
private static final String TAG = "MediaFling";
private final MediaRouteController.MediaStateListener mMediaStateListener =
new MediaRouteController.MediaStateListener() {
@Override
public void onRouteAvailabilityChanged(boolean available) {
mRouteIsAvailable = available;
onRouteAvailabilityChange();
}
@Override
public void onRouteDialogCancelled() {
if (mNativeRemoteMediaPlayerBridge == 0) return;
nativeOnCancelledRemotePlaybackRequest(mNativeRemoteMediaPlayerBridge);
}
@Override
public void onError() {
if (mActive && mNativeRemoteMediaPlayerBridge != 0) {
nativeOnError(mNativeRemoteMediaPlayerBridge);
}
}
@Override
public void onSeekCompleted() {
mSeekRequested = false;
if (mActive && mNativeRemoteMediaPlayerBridge != 0) {
nativeOnSeekCompleted(mNativeRemoteMediaPlayerBridge);
}
}
@Override
public void onRouteUnselected() {
if (mNativeRemoteMediaPlayerBridge == 0) return;
nativeOnRouteUnselected(mNativeRemoteMediaPlayerBridge);
}
@Override
public void onPlaybackStateChanged(PlayerState newState) {
if (mNativeRemoteMediaPlayerBridge == 0) return;
if (newState == PlayerState.FINISHED || newState == PlayerState.INVALIDATED) {
nativeOnPlaybackFinished(mNativeRemoteMediaPlayerBridge);
} else if (newState == PlayerState.PLAYING) {
nativeOnPlaying(mNativeRemoteMediaPlayerBridge);
} else if (newState == PlayerState.PAUSED) {
mPauseRequested = false;
nativeOnPaused(mNativeRemoteMediaPlayerBridge);
}
}
@Override
public String getTitle() {
if (mNativeRemoteMediaPlayerBridge == 0) return null;
return nativeGetTitle(mNativeRemoteMediaPlayerBridge);
}
@Override
public Bitmap getPosterBitmap() {
return mPosterBitmap;
}
@Override
public void pauseLocal() {
if (mNativeRemoteMediaPlayerBridge == 0) return;
nativePauseLocal(mNativeRemoteMediaPlayerBridge);
}
@Override
public long getLocalPosition() {
if (mNativeRemoteMediaPlayerBridge == 0) return 0L;
return nativeGetLocalPosition(mNativeRemoteMediaPlayerBridge);
}
@Override
public void onCastStarting(String routeName) {
if (mNativeRemoteMediaPlayerBridge != 0) {
nativeOnCastStarting(mNativeRemoteMediaPlayerBridge,
RemoteMediaPlayerController.instance().getCastingMessage(routeName));
}
mActive = true;
}
@Override
public void onCastStopping() {
if (mNativeRemoteMediaPlayerBridge != 0) {
nativeOnCastStopping(mNativeRemoteMediaPlayerBridge);
}
mActive = false;
// Free the poster bitmap to save memory
mPosterBitmap = null;
}
@Override
public String getSourceUrl() {
return mSourceUrl;
}
@Override
public String getCookies() {
return mCookies;
}
@Override
public String getFrameUrl() {
return mFrameUrl;
}
@Override
public long getStartPositionMillis() {
return mStartPositionMillis;
}
@Override
public boolean isPauseRequested() {
return mPauseRequested;
}
@Override
public boolean isSeekRequested() {
return mSeekRequested;
}
@Override
public long getSeekLocation() {
return mSeekLocation;
}
};
private RemoteMediaPlayerBridge(long nativeRemoteMediaPlayerBridge, String sourceUrl,
String frameUrl, String userAgent) {
Log.d(TAG, "Creating RemoteMediaPlayerBridge");
mNativeRemoteMediaPlayerBridge = nativeRemoteMediaPlayerBridge;
mOriginalSourceUrl = sourceUrl;
mOriginalFrameUrl = frameUrl;
mUserAgent = userAgent;
// This will get null if there isn't a mediaRouteController that can play this media.
mRouteController = RemoteMediaPlayerController.instance()
.getMediaRouteController(sourceUrl, frameUrl);
}
@CalledByNative
private static RemoteMediaPlayerBridge create(long nativeRemoteMediaPlayerBridge,
String sourceUrl, String frameUrl, String userAgent) {
return new RemoteMediaPlayerBridge(
nativeRemoteMediaPlayerBridge, sourceUrl, frameUrl, userAgent);
}
/**
* Called when a lower layer requests that a video be cast. This will typically be a request
* from Blink when the cast button is pressed on the default video controls.
*/
@CalledByNative
private void requestRemotePlayback(long startPositionMillis) {
Log.d(TAG, "requestRemotePlayback at t=%d", startPositionMillis);
if (mRouteController == null) return;
// Clear out the state
mPauseRequested = false;
mSeekRequested = false;
mStartPositionMillis = startPositionMillis;
RemoteMediaPlayerController.instance().requestRemotePlayback(
mMediaStateListener, mRouteController);
}
/**
* Called when a lower layer requests control of a video that is being cast.
*/
@CalledByNative
private void requestRemotePlaybackControl() {
Log.d(TAG, "requestRemotePlaybackControl");
RemoteMediaPlayerController.instance().requestRemotePlaybackControl(mMediaStateListener);
}
@CalledByNative
private void setNativePlayer() {
Log.d(TAG, "setNativePlayer");
if (mRouteController == null) return;
mActive = true;
}
@CalledByNative
private void onPlayerCreated() {
Log.d(TAG, "onPlayerCreated");
if (mRouteController == null) return;
mRouteController.addMediaStateListener(mMediaStateListener);
}
@CalledByNative
private void onPlayerDestroyed() {
Log.d(TAG, "onPlayerDestroyed");
if (mRouteController == null) return;
mRouteController.removeMediaStateListener(mMediaStateListener);
}
/**
* @param bitmap The bitmap of the poster for the video, null if no poster image exists.
*
* TODO(cimamoglu): Notify the clients (probably through MediaRouteController.Listener)
* of the poster image change. This is necessary for when a web page changes the poster
* while the client (i.e. only ExpandedControllerActivity for now) is active.
*/
@CalledByNative
private void setPosterBitmap(Bitmap bitmap) {
if (mRouteController == null) return;
mPosterBitmap = bitmap;
}
@CalledByNative
protected boolean isPlaying() {
if (mRouteController == null) return false;
return mRouteController.isPlaying();
}
@CalledByNative
protected int getCurrentPosition() {
if (mRouteController == null) return 0;
return (int) mRouteController.getPosition();
}
@CalledByNative
protected int getDuration() {
if (mRouteController == null) return 0;
return (int) mRouteController.getDuration();
}
@CalledByNative
protected void release() {
// Remove the state change listeners. Release does mean that Chrome is no longer interested
// in events from the media player.
if (mRouteController != null) mRouteController.setMediaStateListener(null);
mActive = false;
}
@CalledByNative
protected void setVolume(double volume) {
}
@CalledByNative
protected void start() throws IllegalStateException {
mPauseRequested = false;
if (mRouteController != null && mRouteController.isBeingCast()) mRouteController.resume();
}
@CalledByNative
protected void pause() throws IllegalStateException {
if (mRouteController != null && mRouteController.isBeingCast()) {
mRouteController.pause();
} else {
mPauseRequested = true;
}
}
@CalledByNative
protected void seekTo(int msec) throws IllegalStateException {
if (mRouteController != null && mRouteController.isBeingCast()) {
mRouteController.seekTo(msec);
} else {
mSeekRequested = true;
mSeekLocation = msec;
}
}
private void onRouteAvailabilityChange() {
if (mNativeRemoteMediaPlayerBridge == 0) return;
boolean usable = mRouteIsAvailable && mIsPlayable;
nativeOnRouteAvailabilityChanged(mNativeRemoteMediaPlayerBridge, usable);
}
@CalledByNative
protected void destroy() {
Log.d(TAG, "destroy");
if (mRouteController != null) {
mRouteController.removeMediaStateListener(mMediaStateListener);
}
mNativeRemoteMediaPlayerBridge = 0;
}
@CalledByNative
private void setCookies(String cookies) {
if (mRouteController == null) return;
mCookies = cookies;
mRouteController.checkIfPlayableRemotely(mOriginalSourceUrl, mOriginalFrameUrl, cookies,
mUserAgent, new MediaRouteController.MediaValidationCallback() {
@Override
public void onResult(
boolean isPlayable, String revisedSourceUrl, String revisedFrameUrl) {
mIsPlayable = isPlayable;
mSourceUrl = revisedSourceUrl;
mFrameUrl = revisedFrameUrl;
onRouteAvailabilityChange();
}
});
}
private native void nativeOnPlaying(long nativeRemoteMediaPlayerBridge);
private native void nativeOnPaused(long nativeRemoteMediaPlayerBridge);
private native void nativeOnRouteUnselected(long nativeRemoteMediaPlayerBridge);
private native void nativeOnPlaybackFinished(long nativeRemoteMediaPlayerBridge);
private native void nativeOnRouteAvailabilityChanged(long nativeRemoteMediaPlayerBridge,
boolean available);
private native void nativeOnCancelledRemotePlaybackRequest(long nativeRemoteMediaPlayerBridge);
private native String nativeGetTitle(long nativeRemoteMediaPlayerBridge);
private native void nativePauseLocal(long nativeRemoteMediaPlayerBridge);
private native int nativeGetLocalPosition(long nativeRemoteMediaPlayerBridge);
private native void nativeOnCastStarting(long nativeRemoteMediaPlayerBridge,
String castingMessage);
private native void nativeOnCastStopping(long nativeRemoteMediaPlayerBridge);
private native void nativeOnError(long nativeRemoteMediaPlayerBridge);
private native void nativeOnSeekCompleted(long nativeRemoteMediaPlayerBridge);
}