Bug 1305351 - Add ChromeCastDisplay for Presentation API. r?mfinkle draft
authorKuoE0 <kuoe0.tw@gmail.com>
Wed, 21 Sep 2016 20:24:57 +0800
changeset 420084 48a477d3305e1f3e18dc96cb4fef52103a3ba888
parent 420083 66578592cb0b40e2d9d5defb75e558017a34db41
child 420085 da0f8ded9708766cf1235fcf95cbac54a560aea9
child 420090 287631996d98a4dcfb49965bfb0057e351ada04b
push id31085
push userbmo:kuoe0@mozilla.com
push dateMon, 03 Oct 2016 08:27:53 +0000
reviewersmfinkle
bugs1305351
milestone52.0a1
Bug 1305351 - Add ChromeCastDisplay for Presentation API. r?mfinkle MozReview-Commit-ID: A9yXeADOA0Y
mobile/android/app/build.gradle
mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java
mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java
mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
mobile/android/base/moz.build
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -99,18 +99,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 = "743AF7F7";
+
+    private static final String LOGTAG = "GeckoChromeCastDisplay";
+    private final RouteInfo mRoute;
+    private CastDevice mCastDevice;
+
+    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 + ")");
+        }
+
+        mRoute = route;
+        mCastDevice = CastDevice.getFromBundle(mRoute.getExtras());
+    }
+
+    public JSONObject toJSON() {
+        final JSONObject obj = new JSONObject();
+        try {
+            if (mCastDevice == null) {
+                return null;
+            }
+            obj.put("uuid", mRoute.getId());
+            obj.put("friendlyName", mCastDevice.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);
         EventDispatcher.getInstance().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'tplayerisplay 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("PresentationDevice: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("PresentationDevice: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("PresentationDevice: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
@@ -826,18 +826,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']: