mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
500 lines
19 KiB
Java
500 lines
19 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.json.JSONObject;
|
|
import org.json.JSONException;
|
|
|
|
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 ChromeCast 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();
|
|
boolean isPlaying = mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING;
|
|
|
|
// TODO: Do we want to shutdown when there are errors?
|
|
if (mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_IDLE &&
|
|
mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
|
|
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Stop", null));
|
|
}
|
|
}
|
|
|
|
@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 ChromeCast(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
|
|
public JSONObject toJSON() {
|
|
final JSONObject obj = new JSONObject();
|
|
try {
|
|
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
|
if (device == null) {
|
|
return null;
|
|
}
|
|
|
|
obj.put("uuid", route.getId());
|
|
obj.put("version", device.getDeviceVersion());
|
|
obj.put("friendlyName", device.getFriendlyName());
|
|
obj.put("location", device.getIpAddress().toString());
|
|
obj.put("modelName", device.getModelName());
|
|
obj.put("mirror", canMirror);
|
|
// For now we just assume all of these are Google devices
|
|
obj.put("manufacturer", "Google Inc.");
|
|
} catch (JSONException ex) {
|
|
debug("Error building route", ex);
|
|
}
|
|
|
|
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) {
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("MediaPlayer:Response", message));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Mirror", route.getId()));
|
|
} 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 = "GeckoChromeCast";
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|