fune/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java
Jim Chen 1294cccf48 Bug 1337467 - Convert observers to bundle events; r=rbarker r=sebastian
Bug 1337467 - 1. Convert "Window:Resize" observer to event; r=rbarker

Bug 1337467 - 2. Convert "ScrollTo:FocusedInput" observer to event; r=rbarker

Bug 1337467 - 3. Convert "Update:CheckResult" observer to event; r=sebastian

Also remove notifyCheckUpdateResult from GeckoInterface.

Bug 1337467 - 4. Convert "GeckoView:ImportScript" observer to event; r=sebastian

Bug 1337467 - 5. Convert accessibility observers to events; r=sebastian

Bug 1337467 - 6. Convert media/casting observers to events; r=sebastian

Bug 1337467 - 7. Convert "Sanitize:ClearData" observer to event; r=sebastian

Bug 1337467 - 8. Convert "Notification:Event" observer to event; r=sebastian

Bug 1337467 - 9. Convert BrowserApp observers to events; r=sebastian

Bug 1337467 - 10. Convert Tab observers to events; r=sebastian

Bug 1337467 - 11. Convert "Passwords:Init" and "FormHistory:Init" observers to events; r=sebastian

Bug 1337467 - 12. Convert Reader observers to events; r=sebastian

Bug 1337467 - 13. Convert Distribution observers to events; r=sebastian

Bug 1337467 - 14. Convert "Fonts:Reload" observer to event; r=sebastian

Bug 1337467 - 15. Convert RecentTabsAdapter observers to events; r=sebastian

Bug 1337467 - 16. Convert "Session:Prefetch" observer to event; r=sebastian

Bug 1337467 - 17. Convert "Browser:Quit" and "FullScreen:Exit" observers to events; r=sebastian

Bug 1337467 - 18. Convert SessionStore observers to events; r=sebastian

The "Session:NotifyLocationChange" observer is sent by browser.js and
requires passing a browser reference, so it's left as an observer.

Bug 1337467 - 19. Remove unused "Tab:Screenshot:Cancel" notifyObserver call; r=me

Bug 1337467 - 20. Convert "Session:Navigate" observer to event; r=sebastian

Bug 1337467 - 21. Convert "Locale:*" observers to events; r=sebastian

Bug 1337467 - Add log for unhandled events; r=me

Add back the log indicating no listener for an event, which can be
useful when reading logcat. r=me for trivial change.

Bug 1337467 - Don't return error from EventDispatcher when OnEvent fails; r=me

When a listener's OnEvent method returns an error, continue to dispatch
to other listeners and don't return an error from the dispatch function.
This avoids unexpected errors when dispatching events. r=me for trivial
patch.
2017-03-07 12:34:04 -05:00

507 lines
20 KiB
Java

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import java.io.IOException;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import com.google.android.gms.cast.Cast.MessageReceivedCallback;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.RemoteMediaPlayer;
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.GooglePlayServicesUtil;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
/* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */
class ChromeCastPlayer implements GeckoMediaPlayer {
private static final boolean SHOW_DEBUG = false;
static final String MIRROR_RECEIVER_APP_ID = "08FF1091";
private final Context context;
private final RouteInfo route;
private GoogleApiClient apiClient;
private RemoteMediaPlayer remoteMediaPlayer;
private final boolean canMirror;
private String mSessionId;
private MirrorChannel mMirrorChannel;
private boolean mApplicationStarted = false;
// EventCallback which is actually a GeckoEventCallback is sometimes being invoked more
// than once. That causes the IllegalStateException to be thrown. To prevent a crash,
// catch the exception and report it as an error to the log.
private static void sendSuccess(final EventCallback callback, final String msg) {
try {
callback.sendSuccess(msg);
} catch (final IllegalStateException e) {
Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e);
}
}
private static void sendError(final EventCallback callback, final String msg) {
try {
callback.sendError(msg);
} catch (final IllegalStateException e) {
Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e);
}
}
// Callback to start playback of a url on a remote device
private class VideoPlayCallback implements ResultCallback<ApplicationConnectionResult>,
RemoteMediaPlayer.OnStatusUpdatedListener,
RemoteMediaPlayer.OnMetadataUpdatedListener {
private final String url;
private final String type;
private final String title;
private final EventCallback callback;
public VideoPlayCallback(String url, String type, String title, EventCallback callback) {
this.url = url;
this.type = type;
this.title = title;
this.callback = callback;
}
@Override
public void onStatusUpdated() {
MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
switch (mediaStatus.getPlayerState()) {
case MediaStatus.PLAYER_STATE_PLAYING:
EventDispatcher.getInstance().dispatch("MediaPlayer:Playing", null);
break;
case MediaStatus.PLAYER_STATE_PAUSED:
EventDispatcher.getInstance().dispatch("MediaPlayer:Paused", null);
break;
case MediaStatus.PLAYER_STATE_IDLE:
// TODO: Do we want to shutdown when there are errors?
if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
EventDispatcher.getInstance().dispatch("Casting:Stop", null);
}
break;
default:
// TODO: Do we need to handle other status such as buffering / unknown?
break;
}
}
@Override
public void onMetadataUpdated() { }
@Override
public void onResult(ApplicationConnectionResult result) {
Status status = result.getStatus();
debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
if (status.isSuccess()) {
remoteMediaPlayer = new RemoteMediaPlayer();
remoteMediaPlayer.setOnStatusUpdatedListener(this);
remoteMediaPlayer.setOnMetadataUpdatedListener(this);
mSessionId = result.getSessionId();
if (!verifySession(callback)) {
return;
}
try {
Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
} catch (IOException e) {
debug("Exception while creating media channel", e);
}
startPlayback();
} else {
sendError(callback, status.toString());
}
}
private void startPlayback() {
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
MediaInfo mediaInfo = new MediaInfo.Builder(url)
.setContentType(type)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mediaMetadata)
.build();
try {
remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
sendSuccess(callback, null);
debug("Media loaded successfully");
return;
}
debug("Media load failed " + result.getStatus());
sendError(callback, result.getStatus().toString());
}
});
return;
} catch (IllegalStateException e) {
debug("Problem occurred with media during loading", e);
} catch (Exception e) {
debug("Problem opening media during loading", e);
}
sendError(callback, "");
}
}
public ChromeCastPlayer(Context context, RouteInfo route) {
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
if (status != ConnectionResult.SUCCESS) {
throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
}
this.context = context;
this.route = route;
this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID));
}
/**
* This dumps everything we can find about the device into JSON. This will hopefully make it
* easier to filter out duplicate devices from different sources in JS.
* Returns null if the device can't be found.
*/
@Override // GeckoMediaPlayer
public GeckoBundle toBundle() {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
if (device == null) {
return null;
}
final GeckoBundle obj = new GeckoBundle(7);
obj.putString("uuid", route.getId());
obj.putString("version", device.getDeviceVersion());
obj.putString("friendlyName", device.getFriendlyName());
obj.putString("location", device.getIpAddress().toString());
obj.putString("modelName", device.getModelName());
obj.putBoolean("mirror", canMirror);
// For now we just assume all of these are Google devices
obj.putString("manufacturer", "Google Inc.");
return obj;
}
@Override
public void load(final String title, final String url, final String type, final EventCallback callback) {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
@Override
public void onApplicationStatusChanged() { }
@Override
public void onVolumeChanged() { }
@Override
public void onApplicationDisconnected(int errorCode) { }
});
apiClient = new GoogleApiClient.Builder(context)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle connectionHint) {
// Sometimes apiClient is null here. See bug 1061032
if (apiClient != null && !apiClient.isConnected()) {
debug("Connection failed");
sendError(callback, "Not connected");
return;
}
// Launch the media player app and launch this url once its loaded
try {
Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
.setResultCallback(new VideoPlayCallback(url, type, title, callback));
} catch (Exception e) {
debug("Failed to launch application", e);
}
}
@Override
public void onConnectionSuspended(int cause) {
debug("suspended");
}
}).build();
apiClient.connect();
}
@Override
public void start(final EventCallback callback) {
// Nothing to be done here
sendSuccess(callback, null);
}
@Override
public void stop(final EventCallback callback) {
// Nothing to be done here
sendSuccess(callback, null);
}
public boolean verifySession(final EventCallback callback) {
String msg = null;
if (apiClient == null || !apiClient.isConnected()) {
msg = "Not connected";
}
if (mSessionId == null) {
msg = "No session";
}
if (msg != null) {
debug(msg);
if (callback != null) {
sendError(callback, msg);
}
return false;
}
return true;
}
@Override
public void play(final EventCallback callback) {
if (!verifySession(callback)) {
return;
}
try {
remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
debug("Unable to play: " + status.getStatusCode());
sendError(callback, status.toString());
} else {
sendSuccess(callback, null);
}
}
});
} catch (IllegalStateException ex) {
// The media player may throw if the session has been killed. For now, we're just catching this here.
sendError(callback, "Error playing");
}
}
@Override
public void pause(final EventCallback callback) {
if (!verifySession(callback)) {
return;
}
try {
remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
debug("Unable to pause: " + status.getStatusCode());
sendError(callback, status.toString());
} else {
sendSuccess(callback, null);
}
}
});
} catch (IllegalStateException ex) {
// The media player may throw if the session has been killed. For now, we're just catching this here.
sendError(callback, "Error pausing");
}
}
@Override
public void end(final EventCallback callback) {
if (!verifySession(callback)) {
return;
}
try {
Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
if (result.isSuccess()) {
try {
Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
remoteMediaPlayer = null;
mSessionId = null;
apiClient.disconnect();
apiClient = null;
if (callback != null) {
sendSuccess(callback, null);
}
return;
} catch (Exception ex) {
debug("Error ending", ex);
}
}
if (callback != null) {
sendError(callback, result.getStatus().toString());
}
}
});
} catch (IllegalStateException ex) {
// The media player may throw if the session has been killed. For now, we're just catching this here.
sendError(callback, "Error stopping");
}
}
class MirrorChannel implements MessageReceivedCallback {
/**
* @return custom namespace
*/
public String getNamespace() {
return "urn:x-cast:org.mozilla.mirror";
}
/*
* Receive message from the receiver app
*/
@Override
public void onMessageReceived(CastDevice castDevice, String namespace,
String message) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("message", message);
EventDispatcher.getInstance().dispatch("MediaPlayer:Response", data);
}
public void sendMessage(String message) {
if (apiClient != null && mMirrorChannel != null) {
try {
Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message)
.setResultCallback(
new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
}
});
} catch (Exception e) {
Log.e(LOGTAG, "Exception while sending message", e);
}
}
}
}
private class MirrorCallback implements ResultCallback<ApplicationConnectionResult> {
final EventCallback callback;
MirrorCallback(final EventCallback callback) {
this.callback = callback;
}
@Override
public void onResult(ApplicationConnectionResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
mSessionId = result.getSessionId();
String applicationStatus = result.getApplicationStatus();
boolean wasLaunched = result.getWasLaunched();
mApplicationStarted = true;
// Create the custom message
// channel
mMirrorChannel = new MirrorChannel();
try {
Cast.CastApi.setMessageReceivedCallbacks(apiClient,
mMirrorChannel
.getNamespace(),
mMirrorChannel);
sendSuccess(callback, null);
} catch (IOException e) {
Log.e(LOGTAG, "Exception while creating channel", e);
}
final GeckoBundle message = new GeckoBundle(1);
message.putString("id", route.getId());
EventDispatcher.getInstance().dispatch("Casting:Mirror", message);
} else {
sendError(callback, status.toString());
}
}
}
@Override
public void message(String msg, final EventCallback callback) {
if (mMirrorChannel != null) {
mMirrorChannel.sendMessage(msg);
}
}
@Override
public void mirror(final EventCallback callback) {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
@Override
public void onApplicationStatusChanged() { }
@Override
public void onVolumeChanged() { }
@Override
public void onApplicationDisconnected(int errorCode) { }
});
apiClient = new GoogleApiClient.Builder(context)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle connectionHint) {
// Sometimes apiClient is null here. See bug 1061032
if (apiClient == null || !apiClient.isConnected()) {
return;
}
// Launch the media player app and launch this url once its loaded
try {
Cast.CastApi.launchApplication(apiClient, MIRROR_RECEIVER_APP_ID, true)
.setResultCallback(new MirrorCallback(callback));
} catch (Exception e) {
debug("Failed to launch application", e);
}
}
@Override
public void onConnectionSuspended(int cause) {
debug("suspended");
}
}).build();
apiClient.connect();
}
private static final String LOGTAG = "GeckoChromeCastPlayer";
private void debug(String msg, Exception e) {
if (SHOW_DEBUG) {
Log.e(LOGTAG, msg, e);
}
}
private void debug(String msg) {
if (SHOW_DEBUG) {
Log.d(LOGTAG, msg);
}
}
}