--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -100,18 +100,20 @@ android {
srcDir "${topsrcdir}/mobile/android/stumbler/java"
}
if (!mozconfig.substs.MOZ_CRASHREPORTER) {
exclude 'org/mozilla/gecko/CrashReporter.java'
}
if (!mozconfig.substs.MOZ_NATIVE_DEVICES) {
- exclude 'org/mozilla/gecko/ChromeCast.java'
+ exclude 'org/mozilla/gecko/ChromeCastDisplay.java'
+ exclude 'org/mozilla/gecko/ChromeCastPlayer.java'
exclude 'org/mozilla/gecko/GeckoMediaPlayer.java'
+ exclude 'org/mozilla/gecko/GeckoPresentationDisplay.java'
exclude 'org/mozilla/gecko/MediaPlayerManager.java'
}
if (mozconfig.substs.MOZ_WEBRTC) {
srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
+++ /dev/null
@@ -1,509 +0,0 @@
-/* -*- 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();
-
- switch (mediaStatus.getPlayerState()) {
- case MediaStatus.PLAYER_STATE_PLAYING:
- GeckoAppShell.notifyObservers("MediaPlayer:Playing", null);
- break;
- case MediaStatus.PLAYER_STATE_PAUSED:
- GeckoAppShell.notifyObservers("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) {
- GeckoAppShell.notifyObservers("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 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.notifyObservers("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.notifyObservers("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);
- }
- }
-
-}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
@@ -0,0 +1,66 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * 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 org.json.JSONObject;
+import org.json.JSONException;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.EventCallback;
+
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.CastRemoteDisplayLocalService;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.api.Status;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.util.Log;
+
+public class ChromeCastDisplay implements GeckoPresentationDisplay {
+
+ static final String REMOTE_DISPLAY_APP_ID = "4574A331";
+
+ private static final String LOGTAG = "GeckoChromeCastDisplay";
+ private final RouteInfo route;
+ private CastDevice castDevice;
+
+ public ChromeCastDisplay(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.route = route;
+ this.castDevice = CastDevice.getFromBundle(route.getExtras());
+ }
+
+ public JSONObject toJSON() {
+ final JSONObject obj = new JSONObject();
+ try {
+ if (castDevice == null) {
+ return null;
+ }
+ obj.put("uuid", route.getId());
+ obj.put("friendlyName", castDevice.getFriendlyName());
+ obj.put("type", "chromecast");
+ } catch (JSONException ex) {
+ Log.d(LOGTAG, "Error building route", ex);
+ }
+
+ return obj;
+ }
+
+ @Override
+ public void start(EventCallback callback) { }
+
+ @Override
+ public void stop(EventCallback callback) { }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java
@@ -0,0 +1,509 @@
+/* -*- 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 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:
+ GeckoAppShell.notifyObservers("MediaPlayer:Playing", null);
+ break;
+ case MediaStatus.PLAYER_STATE_PAUSED:
+ GeckoAppShell.notifyObservers("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) {
+ GeckoAppShell.notifyObservers("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
+ 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.notifyObservers("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.notifyObservers("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 = "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);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java
@@ -0,0 +1,22 @@
+/* -*- 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 org.json.JSONObject;
+import org.mozilla.gecko.util.EventCallback;
+
+/**
+ * Wrapper for MediaRouter types supported by Android to use for
+ * Presentation API, such as Chromecast, Miracast, etc.
+ */
+interface GeckoPresentationDisplay {
+ /**
+ * Can return null.
+ */
+ JSONObject toJSON();
+ void start(EventCallback callback);
+ void stop(EventCallback callback);
+}
--- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
@@ -59,17 +59,18 @@ public class MediaPlayerManager extends
private static void debug(String msg) {
if (SHOW_DEBUG) {
Log.d(LOGTAG, msg);
}
}
protected MediaRouter mediaRouter = null;
- protected final Map<String, GeckoMediaPlayer> displays = new HashMap<String, GeckoMediaPlayer>();
+ protected final Map<String, GeckoMediaPlayer> players = new HashMap<String, GeckoMediaPlayer>();
+ protected final Map<String, GeckoPresentationDisplay> displays = new HashMap<String, GeckoPresentationDisplay>(); // used for Presentation API
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"MediaPlayer:Load",
"MediaPlayer:Start",
"MediaPlayer:Stop",
@@ -95,55 +96,61 @@ public class MediaPlayerManager extends
"MediaPlayer:Message");
}
// GeckoEventListener implementation
@Override
public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
debug(event);
- final GeckoMediaPlayer display = displays.get(message.getString("id"));
- if (display == null) {
- Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
+ final GeckoMediaPlayer player = players.get(message.getString("id"));
+ if (player == null) {
+ Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event);
if (callback != null) {
callback.sendError(null);
}
return;
}
if ("MediaPlayer:Play".equals(event)) {
- display.play(callback);
+ player.play(callback);
} else if ("MediaPlayer:Start".equals(event)) {
- display.start(callback);
+ player.start(callback);
} else if ("MediaPlayer:Stop".equals(event)) {
- display.stop(callback);
+ player.stop(callback);
} else if ("MediaPlayer:Pause".equals(event)) {
- display.pause(callback);
+ player.pause(callback);
} else if ("MediaPlayer:End".equals(event)) {
- display.end(callback);
+ player.end(callback);
} else if ("MediaPlayer:Mirror".equals(event)) {
- display.mirror(callback);
+ player.mirror(callback);
} else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
- display.message(message.getString("data"), callback);
+ player.message(message.getString("data"), callback);
} else if ("MediaPlayer:Load".equals(event)) {
final String url = message.optString("source", "");
final String type = message.optString("type", "video/mp4");
final String title = message.optString("title", "");
- display.load(title, url, type, callback);
+ player.load(title, url, type, callback);
}
}
private final MediaRouter.Callback callback =
new MediaRouter.Callback() {
@Override
public void onRouteRemoved(MediaRouter router, RouteInfo route) {
debug("onRouteRemoved: route=" + route);
- displays.remove(route.getId());
+
+ // Remove from media player list.
+ players.remove(route.getId());
GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId());
updatePresentation();
+
+ // Remove from presentation display list.
+ displays.remove(route.getId());
+ GeckoAppShell.notifyObservers("AndroidCastDevice:Removed", route.getId());
}
@SuppressWarnings("unused")
public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
updatePresentation();
}
// These methods aren't used by the support version Media Router
@@ -159,31 +166,54 @@ public class MediaPlayerManager extends
@Override
public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
}
@Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
debug("onRouteAdded: route=" + route);
- final GeckoMediaPlayer display = getMediaPlayerForRoute(route);
- saveAndNotifyOfDisplay("MediaPlayer:Added", route, display);
+ final GeckoMediaPlayer player = getMediaPlayerForRoute(route);
+ saveAndNotifyOfPlayer("MediaPlayer:Added", route, player);
updatePresentation();
+
+ final GeckoPresentationDisplay display = getPresentationDisplayForRoute(route);
+ saveAndNotifyOfDisplay("AndroidCastDevice:Added", route, display);
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
debug("onRouteChanged: route=" + route);
- final GeckoMediaPlayer display = displays.get(route.getId());
- saveAndNotifyOfDisplay("MediaPlayer:Changed", route, display);
+ final GeckoMediaPlayer player = players.get(route.getId());
+ saveAndNotifyOfPlayer("MediaPlayer:Changed", route, player);
updatePresentation();
+
+ final GeckoPresentationDisplay display = displays.get(route.getId());
+ saveAndNotifyOfDisplay("AndroidCastDevice:Changed", route, display);
+ }
+
+ private void saveAndNotifyOfPlayer(final String eventName,
+ MediaRouter.RouteInfo route,
+ final GeckoMediaPlayer player) {
+ if (player == null) {
+ return;
+ }
+
+ final JSONObject json = player.toJSON();
+ if (json == null) {
+ return;
+ }
+
+ players.put(route.getId(), player);
+ GeckoAppShell.notifyObservers(eventName, json.toString());
}
private void saveAndNotifyOfDisplay(final String eventName,
- MediaRouter.RouteInfo route, final GeckoMediaPlayer display) {
+ MediaRouter.RouteInfo route,
+ final GeckoPresentationDisplay display) {
if (display == null) {
return;
}
final JSONObject json = display.toJSON();
if (json == null) {
return;
}
@@ -191,25 +221,36 @@ public class MediaPlayerManager extends
displays.put(route.getId(), display);
GeckoAppShell.notifyObservers(eventName, json.toString());
}
};
private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
try {
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
- return new ChromeCast(getActivity(), route);
+ return new ChromeCastPlayer(getActivity(), route);
}
} catch (Exception ex) {
debug("Error handling presentation", ex);
}
return null;
}
+ private GeckoPresentationDisplay getPresentationDisplayForRoute(MediaRouter.RouteInfo route) {
+ try {
+ if (route.supportsControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))) {
+ return new ChromeCastDisplay(getActivity(), route);
+ }
+ } catch (Exception ex) {
+ debug("Error handling presentation", ex);
+ }
+ return null;
+ }
+
@Override
public void onPause() {
super.onPause();
mediaRouter.removeCallback(callback);
mediaRouter = null;
}
@Override
@@ -220,15 +261,16 @@ public class MediaPlayerManager extends
if (mediaRouter != null) {
return;
}
mediaRouter = MediaRouter.getInstance(getActivity());
final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
- .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCast.MIRROR_RECEIVER_APP_ID))
+ .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastPlayer.MIRROR_RECEIVER_APP_ID))
+ .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))
.build();
mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
protected void updatePresentation() { /* Overridden in sub-classes. */ }
}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -827,18 +827,20 @@ gbjar.extra_jars += [
moz_native_devices_jars = [
CONFIG['ANDROID_MEDIAROUTER_V7_AAR_LIB'],
CONFIG['ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_LIB'],
]
moz_native_devices_sources = ['java/org/mozilla/gecko/' + x for x in [
- 'ChromeCast.java',
+ 'ChromeCastDisplay.java',
+ 'ChromeCastPlayer.java',
'GeckoMediaPlayer.java',
+ 'GeckoPresentationDisplay.java',
'MediaPlayerManager.java',
'PresentationMediaPlayerManager.java',
]]
if CONFIG['MOZ_NATIVE_DEVICES']:
gbjar.extra_jars += moz_native_devices_jars
gbjar.sources += moz_native_devices_sources
if CONFIG['ANDROID_MEDIAROUTER_V7_AAR']: