├── tests
├── README.md
└── tests.js
├── src
└── android
│ ├── ChromecastException.java
│ ├── ChromecastOnMediaUpdatedListener.java
│ ├── ChromecastSessionCallback.java
│ ├── ChromecastOnSessionUpdatedListener.java
│ ├── ChromecastMediaRouterCallback.java
│ ├── ChromecastMediaController.java
│ ├── ChromecastSession.java
│ └── Chromecast.java
├── README.md
├── plugin.xml
├── .gitignore
├── EventEmitter.js
└── chrome.cast.js
/tests/README.md:
--------------------------------------------------------------------------------
1 | See cordova-labs cdvtest branch if interested in autotests
2 |
--------------------------------------------------------------------------------
/src/android/ChromecastException.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | public class ChromecastException extends Exception {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/src/android/ChromecastOnMediaUpdatedListener.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | import org.json.JSONObject;
4 |
5 | public interface ChromecastOnMediaUpdatedListener {
6 | void onMediaLoaded(JSONObject media);
7 | void onMediaUpdated(boolean isAlive, JSONObject media);
8 | }
--------------------------------------------------------------------------------
/src/android/ChromecastSessionCallback.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | public abstract class ChromecastSessionCallback {
4 | public void onSuccess() {
5 | onSuccess(null);
6 | }
7 | abstract void onSuccess(Object object);
8 | abstract void onError(String reason);
9 | }
--------------------------------------------------------------------------------
/src/android/ChromecastOnSessionUpdatedListener.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | import org.json.JSONObject;
4 |
5 | public interface ChromecastOnSessionUpdatedListener {
6 | void onSessionUpdated(boolean isAlive, JSONObject properties);
7 | void onMessage(ChromecastSession session, String namespace, String message);
8 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | cordova-chromecast
2 | ==================
3 |
4 | Chromecast in Cordova
5 |
6 | ##Installation
7 | For now, add the plugin from this repository, we'll publish soon with more progress.
8 |
9 | ```
10 | cordova plugin add https://github.com/GetVideostream/cordova-chromecast.git
11 | ```
12 |
13 | If you have NodeJS installed, the dependencies should be automatically copied.
14 |
15 | - `http://nodejs.org/`
16 |
17 | If not you will need to import the following projects as Library Projects in order for this plugin to work:
18 |
19 | - `adt-bundle\sdk\extras\google\google_play_services\libproject\google-play-services_lib`
20 | - `adt-bundle\sdk\extras\android\support\v7\appcompat`
21 | - `adt-bundle\sdk\extras\android\support\v7\mediarouter`
22 |
23 | ##Usage
24 |
25 | This project attempts to implement the official Google Cast SDK for Chrome... in Cordova. We've made a lot of progress in making this possible, check out the offical docs for examples: https://developers.google.com/cast/docs/chrome_sender
26 |
27 | When you call `chrome.cast.requestSession()` an ugly popup will be displayed to select a Chromecast. If you're not cool with this - you can call: `chrome.cast.getRouteListElement()` which will return a `
` tag that contains the Chromecasts in a list. All you have to do is style that bad boy and you're off to the races!
28 |
29 |
30 | ##Status
31 |
32 | The project is now pretty much feature complete - the only things that probably break will be missing parameters. We haven't done any checking for optional paramaters. When using it, make sure your constructors and function calls have every parameter specified in the API.
33 |
--------------------------------------------------------------------------------
/src/android/ChromecastMediaRouterCallback.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collection;
5 |
6 | import android.support.v7.media.MediaRouter;
7 | import android.support.v7.media.MediaRouter.RouteInfo;
8 |
9 | public class ChromecastMediaRouterCallback extends MediaRouter.Callback {
10 | private volatile ArrayList routes = new ArrayList();
11 |
12 | private Chromecast callback = null;
13 |
14 | public void registerCallbacks(Chromecast instance) {
15 | this.callback = instance;
16 | }
17 |
18 | public synchronized RouteInfo getRoute(String id) {
19 | for (RouteInfo i : this.routes) {
20 | if (i.getId().equals(id)) {
21 | return i;
22 | }
23 | }
24 | return null;
25 | }
26 |
27 | public synchronized RouteInfo getRoute(int index) {
28 | return routes.get(index);
29 | }
30 |
31 | public synchronized Collection getRoutes() {
32 | return routes;
33 | }
34 |
35 | @Override
36 | public synchronized void onRouteAdded(MediaRouter router, RouteInfo route) {
37 | routes.add(route);
38 | if (this.callback != null) {
39 | this.callback.onRouteAdded(router, route);
40 | }
41 | }
42 |
43 | @Override
44 | public void onRouteRemoved(MediaRouter router, RouteInfo route) {
45 | routes.remove(route);
46 | if (this.callback != null) {
47 | this.callback.onRouteRemoved(router, route);
48 | }
49 | }
50 |
51 | @Override
52 | public void onRouteSelected(MediaRouter router, RouteInfo info) {
53 | if (this.callback != null) {
54 | this.callback.onRouteSelected(router, info);
55 | }
56 | }
57 |
58 | @Override
59 | public void onRouteUnselected(MediaRouter router, RouteInfo info) {
60 | if (this.callback != null) {
61 | this.callback.onRouteUnselected(router, info);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 | Cordova ChromeCast
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
3 |
4 | *.iml
5 |
6 | ## Directory-based project format:
7 | .idea/
8 | # if you remove the above rule, at least ignore the following:
9 |
10 | # User-specific stuff:
11 | # .idea/workspace.xml
12 | # .idea/tasks.xml
13 | # .idea/dictionaries
14 |
15 | # Sensitive or high-churn files:
16 | # .idea/dataSources.ids
17 | # .idea/dataSources.xml
18 | # .idea/sqlDataSources.xml
19 | # .idea/dynamic.xml
20 | # .idea/uiDesigner.xml
21 |
22 | # Gradle:
23 | # .idea/gradle.xml
24 | # .idea/libraries
25 |
26 | # Mongo Explorer plugin:
27 | # .idea/mongoSettings.xml
28 |
29 | ## File-based project format:
30 | *.ipr
31 | *.iws
32 |
33 | ## Plugin-specific files:
34 |
35 | # IntelliJ
36 | /out/
37 |
38 | # mpeltonen/sbt-idea plugin
39 | .idea_modules/
40 |
41 | # JIRA plugin
42 | atlassian-ide-plugin.xml
43 |
44 | # Crashlytics plugin (for Android Studio and IntelliJ)
45 | com_crashlytics_export_strings.xml
46 | crashlytics.properties
47 | crashlytics-build.properties
48 | ### Android template
49 | # Built application files
50 | *.apk
51 | *.ap_
52 |
53 | # Files for the Dalvik VM
54 | *.dex
55 |
56 | # Java class files
57 | *.class
58 |
59 | # Generated files
60 | bin/
61 | gen/
62 |
63 | # Gradle files
64 | .gradle/
65 | build/
66 |
67 | # Local configuration file (sdk path, etc)
68 | local.properties
69 |
70 | # Proguard folder generated by Eclipse
71 | proguard/
72 |
73 | # Log Files
74 | *.log
75 |
76 | # Android Studio Navigation editor temp files
77 | .navigation/
78 | ### Windows template
79 | # Windows image file caches
80 | Thumbs.db
81 | ehthumbs.db
82 |
83 | # Folder config file
84 | Desktop.ini
85 |
86 | # Recycle Bin used on file shares
87 | $RECYCLE.BIN/
88 |
89 | # Windows Installer files
90 | *.cab
91 | *.msi
92 | *.msm
93 | *.msp
94 |
95 | # Windows shortcuts
96 | *.lnk
97 | ### Java template
98 | *.class
99 |
100 | # Mobile Tools for Java (J2ME)
101 | .mtj.tmp/
102 |
103 | # Package Files #
104 | *.jar
105 | *.war
106 | *.ear
107 |
108 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
109 | hs_err_pid*
110 | ### OSX template
111 | .DS_Store
112 | .AppleDouble
113 | .LSOverride
114 |
115 | # Icon must end with two \r
116 | Icon
117 |
118 | # Thumbnails
119 | ._*
120 |
121 | # Files that might appear in the root of a volume
122 | .DocumentRevisions-V100
123 | .fseventsd
124 | .Spotlight-V100
125 | .TemporaryItems
126 | .Trashes
127 | .VolumeIcon.icns
128 |
129 | # Directories potentially created on remote AFP share
130 | .AppleDB
131 | .AppleDesktop
132 | Network Trash Folder
133 | Temporary Items
134 | .apdisk
135 |
136 | # Created by .ignore support plugin (hsz.mobi)
137 |
--------------------------------------------------------------------------------
/src/android/ChromecastMediaController.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | import com.google.android.gms.cast.MediaInfo;
4 | import com.google.android.gms.cast.MediaMetadata;
5 | import com.google.android.gms.cast.RemoteMediaPlayer;
6 | import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
7 | import com.google.android.gms.common.api.GoogleApiClient;
8 | import com.google.android.gms.common.api.PendingResult;
9 | import com.google.android.gms.common.api.ResultCallback;
10 | import com.google.android.gms.common.images.WebImage;
11 |
12 | import org.json.JSONArray;
13 | import org.json.JSONException;
14 | import org.json.JSONObject;
15 |
16 | import android.net.Uri;
17 | import android.util.Log;
18 |
19 | public class ChromecastMediaController {
20 | private RemoteMediaPlayer remote = null;
21 | public ChromecastMediaController(RemoteMediaPlayer mRemoteMediaPlayer) {
22 | this.remote = mRemoteMediaPlayer;
23 | }
24 |
25 | public MediaInfo createLoadUrlRequest(String contentId, String contentType, long duration, String streamType, JSONObject metadata) {
26 |
27 | // Try creating a GENERIC MediaMetadata obj
28 | MediaMetadata mediaMetadata = new MediaMetadata();
29 | try {
30 |
31 | int metadataType = metadata.has("metadataType") ? metadata.getInt("metadataType") : MediaMetadata.MEDIA_TYPE_MOVIE;
32 |
33 | // GENERIC
34 | if (metadataType == MediaMetadata.MEDIA_TYPE_GENERIC) {
35 | mediaMetadata = new MediaMetadata(); // Creates GENERIC MediaMetaData
36 | mediaMetadata.putString(MediaMetadata.KEY_TITLE, (metadata.has("title")) ? metadata.getString("title") : "[Title not set]" ); // TODO: What should it default to?
37 | mediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, (metadata.has("title")) ? metadata.getString("subtitle") : "[Subtitle not set]" ); // TODO: What should it default to?
38 | mediaMetadata = addImages(metadata, mediaMetadata);
39 | }
40 |
41 | } catch(Exception e) {
42 | e.printStackTrace();
43 | // Fallback
44 | mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
45 | }
46 |
47 | int _streamType = MediaInfo.STREAM_TYPE_BUFFERED;
48 | if (streamType.equals("buffered")) {
49 |
50 | } else if (streamType.equals("live")) {
51 | _streamType = MediaInfo.STREAM_TYPE_LIVE;
52 | } else if (streamType.equals("other")) {
53 | _streamType = MediaInfo.STREAM_TYPE_NONE;
54 | }
55 |
56 | MediaInfo mediaInfo = new MediaInfo.Builder(contentId)
57 | .setContentType(contentType)
58 | .setStreamType(_streamType)
59 | .setStreamDuration(duration)
60 | .setMetadata(mediaMetadata)
61 | .build();
62 |
63 | return mediaInfo;
64 | }
65 |
66 | public void play(GoogleApiClient apiClient, ChromecastSessionCallback callback) {
67 | PendingResult res = this.remote.play(apiClient);
68 | res.setResultCallback(this.createMediaCallback(callback));
69 | }
70 |
71 | public void pause(GoogleApiClient apiClient, ChromecastSessionCallback callback) {
72 | PendingResult res = this.remote.pause(apiClient);
73 | res.setResultCallback(this.createMediaCallback(callback));
74 | }
75 |
76 | public void stop(GoogleApiClient apiClient, ChromecastSessionCallback callback) {
77 | PendingResult res = this.remote.stop(apiClient);
78 | res.setResultCallback(this.createMediaCallback(callback));
79 | }
80 |
81 | public void seek(long seekPosition, String resumeState, GoogleApiClient apiClient, final ChromecastSessionCallback callback) {
82 | PendingResult res = null;
83 | if (resumeState != null && !resumeState.equals("")) {
84 | if (resumeState.equals("PLAYBACK_PAUSE")) {
85 | res = this.remote.seek(apiClient, seekPosition, RemoteMediaPlayer.RESUME_STATE_PAUSE);
86 | } else if (resumeState.equals("PLAYBACK_START")) {
87 | res = this.remote.seek(apiClient, seekPosition, RemoteMediaPlayer.RESUME_STATE_PLAY);
88 | } else {
89 | res = this.remote.seek(apiClient, seekPosition, RemoteMediaPlayer.RESUME_STATE_UNCHANGED);
90 | }
91 | }
92 |
93 | if (res == null) {
94 | res = this.remote.seek(apiClient, seekPosition);
95 | }
96 |
97 | res.setResultCallback(this.createMediaCallback(callback));
98 | }
99 |
100 | public void setVolume(double volume, GoogleApiClient apiClient, final ChromecastSessionCallback callback) {
101 | PendingResult res = this.remote.setStreamVolume(apiClient, volume);
102 | res.setResultCallback(this.createMediaCallback(callback));
103 | }
104 |
105 | public void setMuted(boolean muted, GoogleApiClient apiClient, final ChromecastSessionCallback callback) {
106 | PendingResult res = this.remote.setStreamMute(apiClient, muted);
107 | res.setResultCallback(this.createMediaCallback(callback));
108 | }
109 |
110 | private ResultCallback createMediaCallback(final ChromecastSessionCallback callback) {
111 | return new ResultCallback() {
112 | @Override
113 | public void onResult(MediaChannelResult result) {
114 | if (result.getStatus().isSuccess()) {
115 | callback.onSuccess();
116 | } else {
117 | callback.onError("channel_error");
118 | }
119 | }
120 | };
121 | }
122 |
123 | private MediaMetadata addImages(JSONObject metadata, MediaMetadata mediaMetadata) throws JSONException {
124 | if (metadata.has("images")) {
125 | JSONArray imageUrls = metadata.getJSONArray("images");
126 | for (int i=0; i appImages;
55 | private volatile String sessionId = null;
56 | private volatile String lastSessionId = null;
57 | private boolean isConnected = false;
58 |
59 | private ChromecastSessionCallback launchCallback;
60 | private ChromecastSessionCallback joinSessionCallback;
61 |
62 | private boolean joinInsteadOfConnecting = false;
63 | private HashSet messageNamespaces = new HashSet();
64 |
65 | public ChromecastSession(RouteInfo routeInfo, CordovaInterface cordovaInterface,
66 | ChromecastOnMediaUpdatedListener onMediaUpdatedListener, ChromecastOnSessionUpdatedListener onSessionUpdatedListener) {
67 | this.cordova = cordovaInterface;
68 | this.onMediaUpdatedListener = onMediaUpdatedListener;
69 | this.onSessionUpdatedListener = onSessionUpdatedListener;
70 | this.routeInfo = routeInfo;
71 | this.device = CastDevice.getFromBundle(this.routeInfo.getExtras());
72 |
73 | this.mRemoteMediaPlayer = new RemoteMediaPlayer();
74 | this.mRemoteMediaPlayer.setOnMetadataUpdatedListener(this);
75 | this.mRemoteMediaPlayer.setOnStatusUpdatedListener(this);
76 |
77 | this.chromecastMediaController = new ChromecastMediaController(mRemoteMediaPlayer);
78 | }
79 |
80 |
81 | /**
82 | * Sets the wheels in motion - connects to the Chromecast and launches the given app
83 | * @param appId
84 | */
85 | public void launch(String appId, ChromecastSessionCallback launchCallback) {
86 | this.appId = appId;
87 | this.launchCallback = launchCallback;
88 | this.connectToDevice();
89 | }
90 |
91 | public boolean isConnected() { return this.isConnected; }
92 |
93 | /**
94 | * Adds a message listener if one does not already exist
95 | * @param namespace
96 | */
97 | public void addMessageListener(String namespace) {
98 | if (messageNamespaces.contains(namespace) == false) {
99 | try {
100 | Cast.CastApi.setMessageReceivedCallbacks(mApiClient, namespace, this);
101 | messageNamespaces.add(namespace);
102 | } catch(Exception e) {
103 |
104 | }
105 | }
106 | }
107 |
108 | /**
109 | * Sends a message to a specified namespace
110 | * @param namespace
111 | * @param message
112 | * @param callback
113 | */
114 | public void sendMessage(String namespace, String message, final ChromecastSessionCallback callback) {
115 | try {
116 | Cast.CastApi.sendMessage(mApiClient, namespace, message).setResultCallback(new ResultCallback() {
117 | @Override
118 | public void onResult(Status result) {
119 | if (!result.isSuccess()) {
120 | callback.onSuccess();
121 | } else {
122 | callback.onError(result.toString());
123 | }
124 | }
125 | });
126 | } catch(Exception e) {
127 | callback.onError(e.getMessage());
128 | }
129 | }
130 |
131 | /**
132 | * Join a currently running app with an appId and a session
133 | * @param appId
134 | * @param sessionId
135 | * @param joinSessionCallback
136 | */
137 | public void join (String appId, String sessionId, ChromecastSessionCallback joinSessionCallback) {
138 | this.appId = appId;
139 | this.joinSessionCallback = joinSessionCallback;
140 | this.joinInsteadOfConnecting = true;
141 | this.lastSessionId = sessionId;
142 | this.connectToDevice();
143 | }
144 |
145 | /**
146 | * Kills a session and it's underlying media player
147 | * @param callback
148 | */
149 | public void kill (final ChromecastSessionCallback callback) {
150 | // this.mRemoteMediaPlayer.stop(mApiClient).setResultCallback(new ResultCallback() {
151 | // @Override
152 | // public void onResult(MediaChannelResult result) {
153 | // try {
154 | // Cast.CastApi.stopApplication(mApiClient);
155 | // mApiClient.disconnect();
156 | // } catch(Exception e) {
157 | //
158 | // }
159 | //
160 | // callback.onSuccess();
161 | // }
162 | // });
163 | try {
164 | Cast.CastApi.stopApplication(mApiClient);
165 | mApiClient.disconnect();
166 | } catch(Exception e) {
167 |
168 | }
169 |
170 | callback.onSuccess();
171 | // Cast.CastApi.stopApplication(mApiClient);
172 | }
173 |
174 | /**
175 | * Leaves the session.
176 | * @param callback
177 | */
178 | public void leave (final ChromecastSessionCallback callback) {
179 | try {
180 | Cast.CastApi.leaveApplication(mApiClient);
181 | } catch(Exception e) {
182 |
183 | }
184 |
185 | callback.onSuccess();
186 | }
187 |
188 | /**
189 | * Loads media over the media API
190 | * @param contentId - The URL of the content
191 | * @param contentType - The MIME type of the content
192 | * @param duration - The length of the video (if known)
193 | * @param streamType
194 | * @param autoPlay - Whether or not to start the video playing or not
195 | * @param currentTime - Where in the video to begin playing from
196 | * @param callback
197 | * @return
198 | */
199 | public boolean loadMedia(String contentId, String contentType, long duration, String streamType, boolean autoPlay, double currentTime, JSONObject metadata, final ChromecastSessionCallback callback) {
200 | try {
201 | MediaInfo mediaInfo = chromecastMediaController.createLoadUrlRequest(contentId, contentType, duration, streamType, metadata);
202 |
203 | mRemoteMediaPlayer.load(mApiClient, mediaInfo, autoPlay, (long)(currentTime * 1000))
204 | .setResultCallback(new ResultCallback() {
205 | @Override
206 | public void onResult(MediaChannelResult result) {
207 | if (result.getStatus().isSuccess()) {
208 | System.out.println("Media loaded successfully");
209 |
210 | ChromecastSession.this.onMediaUpdatedListener.onMediaLoaded(ChromecastSession.this.createMediaObject());
211 | callback.onSuccess(ChromecastSession.this.createMediaObject());
212 |
213 | } else {
214 | callback.onError("session_error");
215 | }
216 | }
217 | });
218 | } catch (IllegalStateException e) {
219 | e.printStackTrace();
220 | System.out.println("Problem occurred with media during loading");
221 | callback.onError("session_error");
222 | return false;
223 | } catch (Exception e) {
224 | e.printStackTrace();
225 | callback.onError("session_error");
226 | System.out.println("Problem opening media during loading");
227 | return false;
228 | }
229 | return true;
230 | }
231 |
232 | /**
233 | * Media API - Calls play on the current media
234 | * @param callback
235 | */
236 | public void mediaPlay(ChromecastSessionCallback callback) {
237 | chromecastMediaController.play(mApiClient, callback);
238 | }
239 |
240 | /**
241 | * Media API - Calls pause on the current media
242 | * @param callback
243 | */
244 | public void mediaPause(ChromecastSessionCallback callback) {
245 | chromecastMediaController.pause(mApiClient, callback);
246 | }
247 |
248 | /**
249 | * Media API - Seeks the current playing media
250 | * @param seekPosition - Seconds to seek to
251 | * @param resumeState - Resume state once seeking is complete: PLAYBACK_PAUSE or PLAYBACK_START
252 | * @param callback
253 | */
254 | public void mediaSeek(long seekPosition, String resumeState, ChromecastSessionCallback callback) {
255 | chromecastMediaController.seek(seekPosition, resumeState, mApiClient, callback);
256 | }
257 |
258 | /**
259 | * Media API - Sets the volume on the current playing media object NOT ON THE CHROMECAST DIRECTLY
260 | * @param level
261 | * @param callback
262 | */
263 | public void mediaSetVolume(double level, ChromecastSessionCallback callback) {
264 | chromecastMediaController.setVolume(level, mApiClient, callback);
265 | }
266 |
267 | /**
268 | * Media API - Sets the muted state on the current playing media NOT THE CHROMECAST DIRECTLY
269 | * @param muted
270 | * @param callback
271 | */
272 | public void mediaSetMuted(boolean muted, ChromecastSessionCallback callback) {
273 | chromecastMediaController.setMuted(muted, mApiClient, callback);
274 | }
275 |
276 | /**
277 | * Media API - Stops and unloads the current playing media
278 | * @param callback
279 | */
280 | public void mediaStop(ChromecastSessionCallback callback) {
281 | chromecastMediaController.stop(mApiClient, callback);
282 | }
283 |
284 |
285 | /**
286 | * Sets the receiver volume level
287 | * @param volume
288 | * @param callback
289 | */
290 | public void setVolume(double volume, ChromecastSessionCallback callback) {
291 | try {
292 | Cast.CastApi.setVolume(mApiClient, volume);
293 | callback.onSuccess();
294 | } catch (Exception e) {
295 | e.printStackTrace();
296 | callback.onError(e.getMessage());
297 | }
298 | }
299 |
300 | /**
301 | * Mutes the receiver
302 | * @param muted
303 | * @param callback
304 | */
305 | public void setMute(boolean muted, ChromecastSessionCallback callback) {
306 | try{
307 | Cast.CastApi.setMute(mApiClient, muted);
308 | callback.onSuccess();
309 | } catch (Exception e) {
310 | e.printStackTrace();
311 | callback.onError(e.getMessage());
312 | }
313 | }
314 |
315 |
316 | /**
317 | * Connects to the device with all callbacks and things
318 | */
319 | private void connectToDevice() {
320 | try {
321 | Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(this.device, this);
322 |
323 | this.mApiClient = new GoogleApiClient.Builder(this.cordova.getActivity().getApplicationContext())
324 | .addApi(Cast.API, apiOptionsBuilder.build())
325 | .addConnectionCallbacks(this)
326 | .addOnConnectionFailedListener(this)
327 | .build();
328 |
329 | this.mApiClient.connect();
330 | } catch(Exception e) {
331 | e.printStackTrace();
332 | }
333 | }
334 |
335 | /**
336 | * Launches the application and gets a new session
337 | */
338 | private void launchApplication() {
339 | Cast.CastApi.launchApplication(mApiClient, this.appId, false)
340 | .setResultCallback(launchApplicationResultCallback);
341 | }
342 |
343 | /**
344 | * Attemps to join an already running session
345 | */
346 | private void joinApplication() {
347 | Cast.CastApi.joinApplication(this.mApiClient, this.appId, this.lastSessionId)
348 | .setResultCallback(joinApplicationResultCallback);
349 | }
350 |
351 | /**
352 | * Connects to the remote media player on the receiver
353 | * @throws IllegalStateException
354 | * @throws IOException
355 | */
356 | private void connectRemoteMediaPlayer() throws IllegalStateException, IOException {
357 | Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer);
358 | mRemoteMediaPlayer.requestStatus(mApiClient)
359 | .setResultCallback(connectRemoteMediaPlayerCallback);
360 | }
361 |
362 |
363 | /**
364 | * launchApplication callback
365 | */
366 | private ResultCallback launchApplicationResultCallback = new ResultCallback() {
367 | @Override
368 | public void onResult(ApplicationConnectionResult result) {
369 |
370 | ApplicationMetadata metadata = result.getApplicationMetadata();
371 | ChromecastSession.this.sessionId = result.getSessionId();
372 | ChromecastSession.this.displayName = metadata.getName();
373 | ChromecastSession.this.appImages = metadata.getImages();
374 |
375 | Status status = result.getStatus();
376 |
377 | if (status.isSuccess()) {
378 | try {
379 | ChromecastSession.this.launchCallback.onSuccess(ChromecastSession.this);
380 | connectRemoteMediaPlayer();
381 | ChromecastSession.this.isConnected = true;
382 | } catch (IllegalStateException e) {
383 | e.printStackTrace();
384 | } catch (IOException e) {
385 | e.printStackTrace();
386 | }
387 | } else {
388 | ChromecastSession.this.isConnected = false;
389 | }
390 | }
391 | };
392 |
393 | /**
394 | * joinApplication callback
395 | */
396 | private ResultCallback joinApplicationResultCallback = new ResultCallback() {
397 | @Override
398 | public void onResult(ApplicationConnectionResult result) {
399 |
400 | Status status = result.getStatus();
401 |
402 | if (status.isSuccess()) {
403 | try {
404 | ApplicationMetadata metadata = result.getApplicationMetadata();
405 | ChromecastSession.this.sessionId = result.getSessionId();
406 | ChromecastSession.this.displayName = metadata.getName();
407 | ChromecastSession.this.appImages = metadata.getImages();
408 |
409 | ChromecastSession.this.joinSessionCallback.onSuccess(ChromecastSession.this);
410 | connectRemoteMediaPlayer();
411 | ChromecastSession.this.isConnected = true;
412 | } catch (IllegalStateException e) {
413 | e.printStackTrace();
414 | } catch (IOException e) {
415 | e.printStackTrace();
416 | }
417 | } else {
418 | ChromecastSession.this.joinSessionCallback.onError(status.toString());
419 | ChromecastSession.this.isConnected = false;
420 | }
421 | }
422 | };
423 |
424 | /**
425 | * connectRemoteMediaPlayer callback
426 | */
427 | private ResultCallback connectRemoteMediaPlayerCallback = new ResultCallback() {
428 | @Override
429 | public void onResult(MediaChannelResult result) {
430 | if (result.getStatus().isSuccess()) {
431 | ChromecastSession.this.onMediaUpdatedListener.onMediaUpdated(true, ChromecastSession.this.createMediaObject());
432 | /*ChromecastSession.this.onMediaUpdatedListener.onMediaLoaded(ChromecastSession.this.createMediaObject());*/
433 | } else {
434 | System.out.println("Failed to request status.");
435 | }
436 | }
437 | };
438 |
439 | /**
440 | * Creates a JSON representation of this session
441 | * @return
442 | */
443 | public JSONObject createSessionObject() {
444 | JSONObject out = new JSONObject();
445 | try {
446 | out.put("appId", this.appId);
447 | out.put("media", createMediaObject());
448 |
449 | if (this.appImages != null) {
450 | JSONArray appImages = new JSONArray();
451 | for(WebImage o : this.appImages) {
452 | appImages.put(o.toString());
453 | }
454 | }
455 |
456 | out.put("appImages", appImages);
457 | out.put("sessionId", this.sessionId);
458 | out.put("displayName", this.displayName);
459 |
460 | JSONObject receiver = new JSONObject();
461 | receiver.put("friendlyName", this.device.getFriendlyName());
462 | receiver.put("label", this.device.getDeviceId());
463 |
464 | JSONObject volume = new JSONObject();
465 | try {
466 | volume.put("level", Cast.CastApi.getVolume(mApiClient));
467 | volume.put("muted", Cast.CastApi.isMute(mApiClient));
468 | } catch(Exception e) {
469 |
470 | }
471 |
472 | receiver.put("volume", volume);
473 |
474 | out.put("receiver", receiver);
475 |
476 | } catch(JSONException e) {
477 | e.printStackTrace();
478 | }
479 |
480 | return out;
481 | }
482 |
483 | /**
484 | * Creates a JSON representation of the current playing media
485 | * @return
486 | */
487 | private JSONObject createMediaObject() {
488 | JSONObject out = new JSONObject();
489 | JSONObject objInfo = new JSONObject();
490 |
491 | MediaStatus mediaStatus = mRemoteMediaPlayer.getMediaStatus();
492 | if (mediaStatus == null) {
493 | return out;
494 | }
495 |
496 | MediaInfo mediaInfo = mediaStatus.getMediaInfo();
497 | try {
498 | out.put("media", objInfo);
499 | out.put("mediaSessionId", 1);
500 | out.put("sessionId", this.sessionId);
501 | out.put("currentTime", mediaStatus.getStreamPosition() / 1000.0);
502 | out.put("playbackRate", mediaStatus.getPlaybackRate());
503 | out.put("customData", mediaStatus.getCustomData());
504 |
505 | switch(mediaStatus.getPlayerState()) {
506 | case MediaStatus.PLAYER_STATE_BUFFERING:
507 | out.put("playerState", "BUFFERING"); break;
508 | case MediaStatus.PLAYER_STATE_IDLE:
509 | out.put("playerState", "IDLE"); break;
510 | case MediaStatus.PLAYER_STATE_PAUSED:
511 | out.put("playerState", "PAUSED"); break;
512 | case MediaStatus.PLAYER_STATE_PLAYING:
513 | out.put("playerState", "PLAYING"); break;
514 | case MediaStatus.PLAYER_STATE_UNKNOWN:
515 | out.put("playerState", "UNKNOWN"); break;
516 | }
517 |
518 | switch(mediaStatus.getIdleReason()) {
519 | case MediaStatus.IDLE_REASON_CANCELED:
520 | out.put("idleReason", "canceled"); break;
521 | case MediaStatus.IDLE_REASON_ERROR:
522 | out.put("idleReason", "error"); break;
523 | case MediaStatus.IDLE_REASON_FINISHED:
524 | out.put("idleReason", "finished"); break;
525 | case MediaStatus.IDLE_REASON_INTERRUPTED:
526 | out.put("idleReason", "iterrupted"); break;
527 | case MediaStatus.IDLE_REASON_NONE:
528 | out.put("idleReason", "none"); break;
529 | }
530 |
531 | JSONObject volume = new JSONObject();
532 | volume.put("level", mediaStatus.getStreamVolume());
533 | volume.put("muted", mediaStatus.isMute());
534 |
535 | out.put("volume", volume);
536 |
537 | try {
538 | objInfo.put("duration", mediaInfo.getStreamDuration() / 1000.0);
539 | switch(mediaInfo.getStreamType()) {
540 | case MediaInfo.STREAM_TYPE_BUFFERED:
541 | objInfo.put("streamType", "buffered"); break;
542 | case MediaInfo.STREAM_TYPE_LIVE:
543 | objInfo.put("streamType", "live"); break;
544 | case MediaInfo.STREAM_TYPE_NONE:
545 | objInfo.put("streamType", "other"); break;
546 | }
547 | } catch (Exception e) {
548 |
549 | }
550 |
551 | } catch(JSONException e) {
552 |
553 | }
554 |
555 | return out;
556 | }
557 |
558 |
559 |
560 | /* GoogleApiClient.ConnectionCallbacks implementation
561 | * Called when we successfully connect to the API
562 | * (non-Javadoc)
563 | * @see com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks#onConnected(android.os.Bundle)
564 | */
565 | @Override
566 | public void onConnected(Bundle connectionHint) {
567 | if (this.joinInsteadOfConnecting) {
568 | this.joinApplication();
569 | } else {
570 | this.launchApplication();
571 | }
572 | }
573 |
574 |
575 | /* GoogleApiClient.ConnectionCallbacks implementation
576 | * (non-Javadoc)
577 | * @see com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks#onConnectionSuspended(android.os.Bundle)
578 | */
579 | @Override
580 | public void onConnectionSuspended(int cause) {
581 | if (this.onSessionUpdatedListener != null) {
582 | this.isConnected = false;
583 | this.onSessionUpdatedListener.onSessionUpdated(false, this.createSessionObject());
584 | }
585 | }
586 |
587 | /*
588 | * GoogleApiClient.OnConnectionFailedListener implementation
589 | * When Google API fails to connect.
590 | * (non-Javadoc)
591 | * @see com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener#onConnectionFailed(com.google.android.gms.common.ConnectionResult)
592 | */
593 | @Override
594 | public void onConnectionFailed(ConnectionResult result) {
595 | if (this.launchCallback != null) {
596 | this.isConnected = false;
597 | this.launchCallback.onError("channel_error");
598 | }
599 | }
600 |
601 | /**
602 | * Cast.Listener implementation
603 | * When Chromecast application status changed
604 | */
605 | @Override
606 | public void onApplicationStatusChanged() {
607 | if (this.onSessionUpdatedListener != null) {
608 | ChromecastSession.this.isConnected = true;
609 | this.onSessionUpdatedListener.onSessionUpdated(true, createSessionObject());
610 | }
611 | }
612 |
613 | /**
614 | * Cast.Listener implementation
615 | * When the volume is changed on the Chromecast
616 | */
617 | @Override
618 | public void onVolumeChanged() {
619 | if (this.onSessionUpdatedListener != null) {
620 | this.onSessionUpdatedListener.onSessionUpdated(true, createSessionObject());
621 | }
622 | }
623 |
624 | /**
625 | * Cast.Listener implementation
626 | * When the application is disconnected
627 | */
628 | @Override
629 | public void onApplicationDisconnected(int errorCode) {
630 | if (this.onSessionUpdatedListener != null) {
631 | this.isConnected = false;
632 | this.onSessionUpdatedListener.onSessionUpdated(false, this.createSessionObject());
633 | }
634 | }
635 |
636 |
637 | @Override
638 | public void onMetadataUpdated() {
639 | if (this.onMediaUpdatedListener != null) {
640 | this.onMediaUpdatedListener.onMediaUpdated(true, this.createMediaObject());
641 | }
642 | }
643 |
644 |
645 | @Override
646 | public void onStatusUpdated() {
647 | if (this.onMediaUpdatedListener != null) {
648 | this.onMediaUpdatedListener.onMediaUpdated(true, this.createMediaObject());
649 | }
650 | }
651 |
652 |
653 | /// GETTERS
654 | public String getSessionId() {
655 | return this.sessionId;
656 | }
657 |
658 |
659 | @Override
660 | public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
661 | if (this.onSessionUpdatedListener != null) {
662 | this.onSessionUpdatedListener.onMessage(this, namespace, message);
663 | }
664 | }
665 | }
666 |
--------------------------------------------------------------------------------
/src/android/Chromecast.java:
--------------------------------------------------------------------------------
1 | package acidhax.cordova.chromecast;
2 |
3 | import android.annotation.TargetApi;
4 | import android.os.Build;
5 | import java.lang.reflect.InvocationTargetException;
6 | import java.lang.reflect.Method;
7 | import java.lang.reflect.Type;
8 | import java.util.HashMap;
9 | import java.util.List;
10 | import java.util.ArrayList;
11 |
12 | import com.google.android.gms.cast.CastMediaControlIntent;
13 |
14 | import org.apache.cordova.CordovaPlugin;
15 | import org.apache.cordova.CordovaWebView;
16 | import org.apache.cordova.CallbackContext;
17 | import org.apache.cordova.CordovaInterface;
18 | import org.json.JSONArray;
19 | import org.json.JSONException;
20 | import org.json.JSONObject;
21 |
22 | import android.app.Activity;
23 | import android.app.AlertDialog;
24 | import android.content.DialogInterface;
25 | import android.content.SharedPreferences;
26 | import android.support.v7.media.MediaRouter;
27 | import android.support.v7.media.MediaRouteSelector;
28 | import android.support.v7.media.MediaRouter.RouteInfo;
29 | import android.util.Log;
30 | import android.widget.ArrayAdapter;
31 |
32 | public class Chromecast extends CordovaPlugin implements ChromecastOnMediaUpdatedListener, ChromecastOnSessionUpdatedListener {
33 |
34 | private static final String SETTINGS_NAME= "CordovaChromecastSettings";
35 |
36 | private MediaRouter mMediaRouter;
37 | private MediaRouteSelector mMediaRouteSelector;
38 | private volatile ChromecastMediaRouterCallback mMediaRouterCallback = new ChromecastMediaRouterCallback();
39 | private String appId;
40 |
41 | private boolean autoConnect = false;
42 | private String lastSessionId = null;
43 | private String lastAppId = null;
44 |
45 | private SharedPreferences settings;
46 |
47 |
48 | private volatile ChromecastSession currentSession;
49 |
50 | private void log(String s) {
51 | sendJavascript("console.log('" + s + "');");
52 | }
53 |
54 |
55 | public void initialize(final CordovaInterface cordova, CordovaWebView webView) {
56 | super.initialize(cordova, webView);
57 |
58 | // Restore preferences
59 | this.settings = this.cordova.getActivity().getSharedPreferences(SETTINGS_NAME, 0);
60 | this.lastSessionId = settings.getString("lastSessionId", "");
61 | this.lastAppId = settings.getString("lastAppId", "");
62 | }
63 |
64 | public void onDestroy() {
65 | super.onDestroy();
66 |
67 | if (this.currentSession != null) {
68 | // this.currentSession.kill(new ChromecastSessionCallback() {
69 | // void onSuccess(Object object) { }
70 | // void onError(String reason) {}
71 | // });
72 | }
73 | }
74 |
75 | @Override
76 | public boolean execute(String action, JSONArray args, CallbackContext cbContext) throws JSONException {
77 | try {
78 | Method[] list = this.getClass().getMethods();
79 | Method methodToExecute = null;
80 | for (Method method : list) {
81 | if (method.getName().equals(action)) {
82 | Type[] types = method.getGenericParameterTypes();
83 | if (args.length() + 1 == types.length) { // +1 is the cbContext
84 | boolean isValid = true;
85 | for (int i = 0; i < args.length(); i++) {
86 | Class arg = args.get(i).getClass();
87 | if (types[i] == arg) {
88 | isValid = true;
89 | } else {
90 | isValid = false;
91 | break;
92 | }
93 | }
94 | if (isValid) {
95 | methodToExecute = method;
96 | break;
97 | }
98 | }
99 | }
100 | }
101 | if (methodToExecute != null) {
102 | Type[] types = methodToExecute.getGenericParameterTypes();
103 | Object[] variableArgs = new Object[types.length];
104 | for (int i = 0; i < args.length(); i++) {
105 | variableArgs[i] = args.get(i);
106 | }
107 | variableArgs[variableArgs.length-1] = cbContext;
108 | Class> r = methodToExecute.getReturnType();
109 | if (r == boolean.class) {
110 | return (Boolean) methodToExecute.invoke(this, variableArgs);
111 | } else {
112 | methodToExecute.invoke(this, variableArgs);
113 | return true;
114 | }
115 | } else {
116 | return false;
117 | }
118 | } catch (IllegalAccessException e) {
119 | e.printStackTrace();
120 | return false;
121 | } catch (IllegalArgumentException e) {
122 | e.printStackTrace();
123 | return false;
124 | } catch (InvocationTargetException e) {
125 | e.printStackTrace();
126 | return false;
127 | }
128 | }
129 |
130 | private void setLastSessionId(String sessionId) {
131 | this.lastSessionId = sessionId;
132 | this.settings.edit().putString("lastSessionId", sessionId).apply();
133 | }
134 |
135 |
136 | /**
137 | * Do everything you need to for "setup" - calling back sets the isAvailable and lets every function on the
138 | * javascript side actually do stuff.
139 | * @param callbackContext
140 | */
141 | public boolean setup (CallbackContext callbackContext) {
142 | callbackContext.success();
143 |
144 | return true;
145 | }
146 |
147 | /**
148 | * Initialize all of the MediaRouter stuff with the AppId
149 | * For now, ignore the autoJoinPolicy and defaultActionPolicy; those will come later
150 | * @param appId The appId we're going to use for ALL session requests
151 | * @param autoJoinPolicy tab_and_origin_scoped | origin_scoped | page_scoped
152 | * @param defaultActionPolicy create_session | cast_this_tab
153 | * @param callbackContext
154 | */
155 | public boolean initialize (final String appId, String autoJoinPolicy, String defaultActionPolicy, final CallbackContext callbackContext) {
156 | final Activity activity = cordova.getActivity();
157 | final Chromecast that = this;
158 | this.appId = appId;
159 |
160 | log("initialize " + autoJoinPolicy + " " + appId + " " + this.lastAppId);
161 | if (autoJoinPolicy.equals("origin_scoped") && appId.equals(this.lastAppId)) {
162 | log("lastAppId " + lastAppId);
163 | autoConnect = true;
164 | } else if (autoJoinPolicy.equals("origin_scoped")) {
165 | log("setting lastAppId " + lastAppId);
166 | this.settings.edit().putString("lastAppId", appId).apply();
167 | }
168 |
169 | activity.runOnUiThread(new Runnable() {
170 | public void run() {
171 | mMediaRouter = MediaRouter.getInstance(activity.getApplicationContext());
172 | mMediaRouteSelector = new MediaRouteSelector.Builder()
173 | .addControlCategory(CastMediaControlIntent.categoryForCast(appId))
174 | .build();
175 | mMediaRouterCallback.registerCallbacks(that);
176 | mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
177 | callbackContext.success();
178 |
179 | Chromecast.this.checkReceiverAvailable();
180 | Chromecast.this.emitAllRoutes(null);
181 | }
182 | });
183 |
184 | return true;
185 | }
186 |
187 | /**
188 | * Request the session for the previously sent appId
189 | * THIS IS WHAT LAUNCHES THE CHROMECAST PICKER
190 | * NOTE: Make a request session that is automatic - it'll do most of this code - refactor will be required
191 | * @param callbackContext
192 | */
193 | public boolean requestSession (final CallbackContext callbackContext) {
194 | if (this.currentSession != null) {
195 | callbackContext.success(this.currentSession.createSessionObject());
196 | return true;
197 | }
198 |
199 | this.setLastSessionId("");
200 |
201 | final Activity activity = cordova.getActivity();
202 | activity.runOnUiThread(new Runnable() {
203 | public void run() {
204 | mMediaRouter = MediaRouter.getInstance(activity.getApplicationContext());
205 | final List routeList = mMediaRouter.getRoutes();
206 |
207 | AlertDialog.Builder builder = new AlertDialog.Builder(activity);
208 | builder.setTitle("Choose a Chromecast");
209 | //CharSequence[] seq = new CharSequence[routeList.size() -1];
210 | ArrayList seq_tmp1 = new ArrayList();
211 |
212 | final ArrayList seq_tmp_cnt_final = new ArrayList();
213 |
214 | for (int n = 1; n < routeList.size(); n++) {
215 | RouteInfo route = routeList.get(n);
216 | if (!route.getName().equals("Phone") && route.getId().indexOf("Cast") > -1) {
217 | seq_tmp1.add(route.getName());
218 | seq_tmp_cnt_final.add(n);
219 | //seq[n-1] = route.getName();
220 | }
221 | }
222 |
223 | CharSequence[] seq;
224 | seq = seq_tmp1.toArray(new CharSequence[seq_tmp1.size()]);
225 |
226 | builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
227 | @Override
228 | public void onClick(DialogInterface dialog, int which) {
229 | dialog.dismiss();
230 | callbackContext.error("cancel");
231 | }
232 | });
233 |
234 | builder.setItems(seq, new DialogInterface.OnClickListener() {
235 | @Override
236 | public void onClick(DialogInterface dialog, int which) {
237 | which = seq_tmp_cnt_final.get(which);
238 | RouteInfo selectedRoute = routeList.get(which);
239 | //RouteInfo selectedRoute = routeList.get(which + 1);
240 | Chromecast.this.createSession(selectedRoute, callbackContext);
241 | }
242 | });
243 |
244 | builder.show();
245 |
246 | }
247 | });
248 |
249 | return true;
250 | }
251 |
252 |
253 | /**
254 | * Selects a route by its id
255 | * @param routeId
256 | * @param callbackContext
257 | * @return
258 | */
259 | public boolean selectRoute (final String routeId, final CallbackContext callbackContext) {
260 | if (this.currentSession != null) {
261 | callbackContext.success(this.currentSession.createSessionObject());
262 | return true;
263 | }
264 |
265 | this.setLastSessionId("");
266 |
267 | final Activity activity = cordova.getActivity();
268 | activity.runOnUiThread(new Runnable() {
269 | public void run() {
270 | mMediaRouter = MediaRouter.getInstance(activity.getApplicationContext());
271 | final List routeList = mMediaRouter.getRoutes();
272 |
273 | for (RouteInfo route : routeList) {
274 | if (route.getId().equals(routeId)) {
275 | Chromecast.this.createSession(route, callbackContext);
276 | return;
277 | }
278 | }
279 |
280 | callbackContext.error("No route found");
281 |
282 | }
283 | });
284 |
285 | return true;
286 | }
287 |
288 | /**
289 | * Helper for the creating of a session! The user-selected RouteInfo needs to be passed to a new ChromecastSession
290 | * @param routeInfo
291 | * @param callbackContext
292 | */
293 | private void createSession(RouteInfo routeInfo, final CallbackContext callbackContext) {
294 | this.currentSession = new ChromecastSession(routeInfo, this.cordova, this, this);
295 |
296 | // Launch the app.
297 | this.currentSession.launch(this.appId, new ChromecastSessionCallback() {
298 |
299 | @Override
300 | void onSuccess(Object object) {
301 | ChromecastSession session = (ChromecastSession) object;
302 | if (object == null) {
303 | onError("unknown");
304 | } else if (session == Chromecast.this.currentSession){
305 | Chromecast.this.setLastSessionId(Chromecast.this.currentSession.getSessionId());
306 |
307 | if (callbackContext != null) {
308 | callbackContext.success(session.createSessionObject());
309 | } else {
310 | sendJavascript("chrome.cast._.sessionJoined(" + Chromecast.this.currentSession.createSessionObject().toString() + ");");
311 | }
312 | }
313 | }
314 |
315 | @Override
316 | void onError(String reason) {
317 | if (reason != null) {
318 | Chromecast.this.log("createSession onError " + reason);
319 | if (callbackContext != null) {
320 | callbackContext.error(reason);
321 | }
322 | } else {
323 | if (callbackContext != null) {
324 | callbackContext.error("unknown");
325 | }
326 | }
327 | }
328 |
329 | });
330 | }
331 |
332 | private void joinSession(RouteInfo routeInfo) {
333 | ChromecastSession sessionJoinAttempt = new ChromecastSession(routeInfo, this.cordova, this, this);
334 | sessionJoinAttempt.join(this.appId, this.lastSessionId, new ChromecastSessionCallback() {
335 |
336 | @Override
337 | void onSuccess(Object object) {
338 | if (Chromecast.this.currentSession == null) {
339 | try {
340 | Chromecast.this.currentSession = (ChromecastSession) object;
341 | Chromecast.this.setLastSessionId(Chromecast.this.currentSession.getSessionId());
342 | sendJavascript("chrome.cast._.sessionJoined(" + Chromecast.this.currentSession.createSessionObject().toString() + ");");
343 | } catch (Exception e) {
344 | log("wut.... " + e.getMessage() + e.getStackTrace());
345 | }
346 | }
347 | }
348 |
349 | @Override
350 | void onError(String reason) {
351 | log("sessionJoinAttempt error " +reason);
352 | }
353 |
354 | });
355 | }
356 |
357 | /**
358 | * Set the volume level on the receiver - this is a Chromecast volume, not a Media volume
359 | * @param newLevel
360 | */
361 | public boolean setReceiverVolumeLevel (Double newLevel, CallbackContext callbackContext) {
362 | if (this.currentSession != null) {
363 | this.currentSession.setVolume(newLevel, genericCallback(callbackContext));
364 | } else {
365 | callbackContext.error("session_error");
366 | }
367 | return true;
368 | }
369 |
370 | public boolean setReceiverVolumeLevel (Integer newLevel, CallbackContext callbackContext) {
371 | return this.setReceiverVolumeLevel(newLevel.doubleValue(), callbackContext);
372 | }
373 |
374 | /**
375 | * Sets the muted boolean on the receiver - this is a Chromecast mute, not a Media mute
376 | * @param muted
377 | * @param callbackContext
378 | */
379 | public boolean setReceiverMuted (Boolean muted, CallbackContext callbackContext) {
380 | if (this.currentSession != null) {
381 | this.currentSession.setMute(muted, genericCallback(callbackContext));
382 | } else {
383 | callbackContext.error("session_error");
384 | }
385 | return true;
386 | }
387 |
388 | /**
389 | * Stop the session! Disconnect! All of that jazz!
390 | * @param callbackContext [description]
391 | */
392 | public boolean stopSession(CallbackContext callbackContext) {
393 | callbackContext.error("not_implemented");
394 | return true;
395 | }
396 |
397 | /**
398 | * Send a custom message to the receiver - we don't need this just yet... it was just simple to implement on the js side
399 | * @param namespace
400 | * @param message
401 | * @param callbackContext
402 | */
403 | public boolean sendMessage (String namespace, String message, final CallbackContext callbackContext) {
404 | if (this.currentSession != null) {
405 | this.currentSession.sendMessage(namespace, message, new ChromecastSessionCallback() {
406 |
407 | @Override
408 | void onSuccess(Object object) {
409 | callbackContext.success();
410 | }
411 |
412 | @Override
413 | void onError(String reason) {
414 | callbackContext.error(reason);
415 | }
416 | });
417 | }
418 | return true;
419 | }
420 |
421 |
422 | /**
423 | * Adds a listener to a specific namespace
424 | * @param namespace
425 | * @param callbackContext
426 | * @return
427 | */
428 | public boolean addMessageListener(String namespace, CallbackContext callbackContext) {
429 | if (this.currentSession != null) {
430 | this.currentSession.addMessageListener(namespace);
431 | callbackContext.success();
432 | }
433 | return true;
434 | }
435 |
436 | /**
437 | * Loads some media on the Chromecast using the media APIs
438 | * @param contentId The URL of the media item
439 | * @param contentType MIME type of the content
440 | * @param duration Duration of the content
441 | * @param streamType buffered | live | other
442 | * @param loadRequest.autoPlay Whether or not to automatically start playing the media
443 | * @param loadRequest.currentTime Where to begin playing from
444 | * @param callbackContext
445 | */
446 | public boolean loadMedia (String contentId, String contentType, Integer duration, String streamType, Boolean autoPlay, Double currentTime, JSONObject metadata, final CallbackContext callbackContext) {
447 |
448 | if (this.currentSession != null) {
449 | return this.currentSession.loadMedia(contentId, contentType, duration, streamType, autoPlay, currentTime, metadata,
450 | new ChromecastSessionCallback() {
451 |
452 | @Override
453 | void onSuccess(Object object) {
454 | if (object == null) {
455 | onError("unknown");
456 | } else {
457 | callbackContext.success((JSONObject) object);
458 | }
459 | }
460 |
461 | @Override
462 | void onError(String reason) {
463 | callbackContext.error(reason);
464 | }
465 |
466 | });
467 | } else {
468 | callbackContext.error("session_error");
469 | return false;
470 | }
471 | }
472 | public boolean loadMedia (String contentId, String contentType, Integer duration, String streamType, Boolean autoPlay, Integer currentTime, JSONObject metadata, final CallbackContext callbackContext) {
473 | return this.loadMedia (contentId, contentType, duration, streamType, autoPlay, new Double(currentTime.doubleValue()), metadata, callbackContext);
474 | }
475 |
476 | /**
477 | * Play on the current media in the current session
478 | * @param callbackContext
479 | * @return
480 | */
481 | public boolean mediaPlay(CallbackContext callbackContext) {
482 | if (currentSession != null) {
483 | currentSession.mediaPlay(genericCallback(callbackContext));
484 | } else {
485 | callbackContext.error("session_error");
486 | }
487 | return true;
488 | }
489 |
490 | /**
491 | * Pause on the current media in the current session
492 | * @param callbackContext
493 | * @return
494 | */
495 | public boolean mediaPause(CallbackContext callbackContext) {
496 | if (currentSession != null) {
497 | currentSession.mediaPause(genericCallback(callbackContext));
498 | } else {
499 | callbackContext.error("session_error");
500 | }
501 | return true;
502 | }
503 |
504 |
505 | /**
506 | * Seeks the current media in the current session
507 | * @param seekTime
508 | * @param resumeState
509 | * @param callbackContext
510 | * @return
511 | */
512 | public boolean mediaSeek(Integer seekTime, String resumeState, CallbackContext callbackContext) {
513 | if (currentSession != null) {
514 | currentSession.mediaSeek(seekTime.longValue() * 1000, resumeState, genericCallback(callbackContext));
515 | } else {
516 | callbackContext.error("session_error");
517 | }
518 | return true;
519 | }
520 |
521 |
522 | /**
523 | * Set the volume on the media
524 | * @param level
525 | * @param callbackContext
526 | * @return
527 | */
528 | public boolean setMediaVolume(Double level, CallbackContext callbackContext) {
529 | if (currentSession != null) {
530 | currentSession.mediaSetVolume(level, genericCallback(callbackContext));
531 | } else {
532 | callbackContext.error("session_error");
533 | }
534 |
535 | return true;
536 | }
537 |
538 | /**
539 | * Set the muted on the media
540 | * @param muted
541 | * @param callbackContext
542 | * @return
543 | */
544 | public boolean setMediaMuted(Boolean muted, CallbackContext callbackContext) {
545 | if (currentSession != null) {
546 | currentSession.mediaSetMuted(muted, genericCallback(callbackContext));
547 | } else {
548 | callbackContext.error("session_error");
549 | }
550 |
551 | return true;
552 | }
553 |
554 | /**
555 | * Stops the current media!
556 | * @param callbackContext
557 | * @return
558 | */
559 | public boolean mediaStop(CallbackContext callbackContext) {
560 | if (currentSession != null) {
561 | currentSession.mediaStop(genericCallback(callbackContext));
562 | } else {
563 | callbackContext.error("session_error");
564 | }
565 |
566 | return true;
567 | }
568 |
569 | /**
570 | * Stops the session
571 | * @param callbackContext
572 | * @return
573 | */
574 | public boolean sessionStop (CallbackContext callbackContext) {
575 | if (this.currentSession != null) {
576 | this.currentSession.kill(genericCallback(callbackContext));
577 | this.currentSession = null;
578 | this.setLastSessionId("");
579 | } else {
580 | callbackContext.success();
581 | }
582 |
583 | return true;
584 | }
585 |
586 | /**
587 | * Stops the session
588 | * @param callbackContext
589 | * @return
590 | */
591 | public boolean sessionLeave (CallbackContext callbackContext) {
592 | if (this.currentSession != null) {
593 | this.currentSession.leave(genericCallback(callbackContext));
594 | this.currentSession = null;
595 | this.setLastSessionId("");
596 | } else {
597 | callbackContext.success();
598 | }
599 |
600 | return true;
601 | }
602 |
603 | public boolean emitAllRoutes(CallbackContext callbackContext) {
604 | final Activity activity = cordova.getActivity();
605 |
606 | activity.runOnUiThread(new Runnable() {
607 | public void run() {
608 | mMediaRouter = MediaRouter.getInstance(activity.getApplicationContext());
609 | List routeList = mMediaRouter.getRoutes();
610 |
611 | for (RouteInfo route : routeList) {
612 | if (!route.getName().equals("Phone") && route.getId().indexOf("Cast") > -1) {
613 | sendJavascript("chrome.cast._.routeAdded(" + routeToJSON(route) + ")");
614 | }
615 | }
616 | }
617 | });
618 |
619 | if (callbackContext != null) {
620 | callbackContext.success();
621 | }
622 |
623 | return true;
624 | }
625 |
626 | /**
627 | * Checks to see how many receivers are available - emits the receiver status down to Javascript
628 | */
629 | private void checkReceiverAvailable() {
630 | final Activity activity = cordova.getActivity();
631 |
632 | activity.runOnUiThread(new Runnable() {
633 | public void run() {
634 | mMediaRouter = MediaRouter.getInstance(activity.getApplicationContext());
635 | List routeList = mMediaRouter.getRoutes();
636 | boolean available = false;
637 |
638 | for (RouteInfo route: routeList) {
639 | if (!route.getName().equals("Phone") && route.getId().indexOf("Cast") > -1) {
640 | available = true;
641 | break;
642 | }
643 | }
644 | if (available || (Chromecast.this.currentSession != null && Chromecast.this.currentSession.isConnected())) {
645 | sendJavascript("chrome.cast._.receiverAvailable()");
646 | } else {
647 | sendJavascript("chrome.cast._.receiverUnavailable()");
648 | }
649 | }
650 | });
651 | }
652 |
653 | /**
654 | * Creates a ChromecastSessionCallback that's generic for a CallbackContext
655 | * @param callbackContext
656 | * @return
657 | */
658 | private ChromecastSessionCallback genericCallback (final CallbackContext callbackContext) {
659 | return new ChromecastSessionCallback() {
660 |
661 | @Override
662 | public void onSuccess(Object object) {
663 | callbackContext.success();
664 | }
665 |
666 | @Override
667 | public void onError(String reason) {
668 | callbackContext.error(reason);
669 | }
670 |
671 | };
672 | };
673 |
674 | /**
675 | * Called when a route is discovered
676 | * @param router
677 | * @param route
678 | */
679 | protected void onRouteAdded(MediaRouter router, final RouteInfo route) {
680 | if (this.autoConnect && this.currentSession == null && !route.getName().equals("Phone")) {
681 | log("Attempting to join route " + route.getName());
682 | this.joinSession(route);
683 | } else {
684 | log("For some reason, not attempting to join route " + route.getName() + ", " + this.currentSession + ", " + this.autoConnect);
685 | }
686 | if (!route.getName().equals("Phone") && route.getId().indexOf("Cast") > -1) {
687 | sendJavascript("chrome.cast._.routeAdded(" + routeToJSON(route) + ")");
688 | }
689 | this.checkReceiverAvailable();
690 | }
691 |
692 | /**
693 | * Called when a discovered route is lost
694 | * @param router
695 | * @param route
696 | */
697 | protected void onRouteRemoved(MediaRouter router, RouteInfo route) {
698 | this.checkReceiverAvailable();
699 | if (!route.getName().equals("Phone") && route.getId().indexOf("Cast") > -1) {
700 | sendJavascript("chrome.cast._.routeRemoved(" + routeToJSON(route) + ")");
701 | }
702 | }
703 |
704 | /**
705 | * Called when a route is selected through the MediaRouter
706 | * @param router
707 | * @param route
708 | */
709 | protected void onRouteSelected(MediaRouter router, RouteInfo route) {
710 | this.createSession(route, null);
711 | }
712 |
713 | /**
714 | * Called when a route is unselected through the MediaRouter
715 | * @param router
716 | * @param route
717 | */
718 | protected void onRouteUnselected(MediaRouter router, RouteInfo route) {}
719 |
720 | /**
721 | * Simple helper to convert a route to JSON for passing down to the javascript side
722 | * @param route
723 | * @return
724 | */
725 | private JSONObject routeToJSON(RouteInfo route) {
726 | JSONObject obj = new JSONObject();
727 |
728 | try {
729 | obj.put("name", route.getName());
730 | obj.put("id", route.getId());
731 | } catch (JSONException e) {
732 | e.printStackTrace();
733 | }
734 |
735 | return obj;
736 | }
737 |
738 | @Override
739 | public void onMediaUpdated(boolean isAlive, JSONObject media) {
740 | if (isAlive) {
741 | sendJavascript("chrome.cast._.mediaUpdated(true, " + media.toString() +");");
742 | } else {
743 | sendJavascript("chrome.cast._.mediaUpdated(false, " + media.toString() +");");
744 | }
745 | }
746 |
747 | @Override
748 | public void onSessionUpdated(boolean isAlive, JSONObject session) {
749 | if (isAlive) {
750 | sendJavascript("chrome.cast._.sessionUpdated(true, " + session.toString() + ");");
751 | } else {
752 | log("SESSION DESTROYYYY");
753 | sendJavascript("chrome.cast._.sessionUpdated(false, " + session.toString() + ");");
754 | this.currentSession = null;
755 | }
756 | }
757 |
758 | @Override
759 | public void onMediaLoaded(JSONObject media) {
760 | sendJavascript("chrome.cast._.mediaLoaded(true, " + media.toString() +");");
761 | }
762 |
763 | @Override
764 | public void onMessage(ChromecastSession session, String namespace, String message) {
765 | sendJavascript("chrome.cast._.onMessage('" + session.getSessionId() +"', '" + namespace + "', '" + message + "')");
766 | }
767 |
768 | //Change all @deprecated this.webView.sendJavascript(String) to this local function sendJavascript(String)
769 | @TargetApi(Build.VERSION_CODES.KITKAT)
770 | private void sendJavascript(final String javascript) {
771 |
772 | webView.getView().post(new Runnable() {
773 | @Override
774 | public void run() {
775 | // See: https://github.com/GoogleChrome/chromium-webview-samples/blob/master/jsinterface-example/app/src/main/java/jsinterfacesample/android/chrome/google/com/jsinterface_example/MainFragment.java
776 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
777 | webView.sendJavascript(javascript);
778 | } else {
779 | webView.loadUrl("javascript:" + javascript);
780 | }
781 | }
782 | });
783 | }
784 |
785 | }
786 |
--------------------------------------------------------------------------------
/chrome.cast.js:
--------------------------------------------------------------------------------
1 | var EventEmitter = require('acidhax.cordova.chromecast.EventEmitter');
2 |
3 | var chrome = {};
4 | chrome.cast = {
5 |
6 | /**
7 | * The API version.
8 | * @type {Array}
9 | */
10 | VERSION: [1, 1],
11 |
12 | /**
13 | * Describes availability of a Cast receiver.
14 | * AVAILABLE: At least one receiver is available that is compatible with the session request.
15 | * UNAVAILABLE: No receivers are available.
16 | * @type {Object}
17 | */
18 | ReceiverAvailability: { AVAILABLE: "available", UNAVAILABLE: "unavailable" },
19 |
20 | /**
21 | * TODO: Update when the official API docs are finished
22 | * https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ReceiverType
23 | * CAST:
24 | * DIAL:
25 | * CUSTOM:
26 | * @type {Object}
27 | */
28 | ReceiverType: { CAST: "cast", DIAL: "dial", CUSTOM: "custom" },
29 |
30 |
31 | /**
32 | * Describes a sender application platform.
33 | * CHROME:
34 | * IOS:
35 | * ANDROID:
36 | * @type {Object}
37 | */
38 | SenderPlatform: { CHROME: "chrome", IOS: "ios", ANDROID: "android" },
39 |
40 | /**
41 | * Auto-join policy determines when the SDK will automatically connect a sender application to an existing session after API initialization.
42 | * ORIGIN_SCOPED: Automatically connects when the session was started with the same appId and the same page origin (regardless of tab).
43 | * PAGE_SCOPED: No automatic connection.
44 | * TAB_AND_ORIGIN_SCOPED: Automatically connects when the session was started with the same appId, in the same tab and page origin.
45 | * @type {Object}
46 | */
47 | AutoJoinPolicy: { TAB_AND_ORIGIN_SCOPED: "tab_and_origin_scoped", ORIGIN_SCOPED: "origin_scoped", PAGE_SCOPED: "page_scoped" },
48 |
49 | /**
50 | * Capabilities that are supported by the receiver device.
51 | * AUDIO_IN: The receiver supports audio input (microphone).
52 | * AUDIO_OUT: The receiver supports audio output.
53 | * VIDEO_IN: The receiver supports video input (camera).
54 | * VIDEO_OUT: The receiver supports video output.
55 | * @type {Object}
56 | */
57 | Capability: { VIDEO_OUT: "video_out", AUDIO_OUT: "audio_out", VIDEO_IN: "video_in", AUDIO_IN: "audio_in" },
58 |
59 | /**
60 | * Default action policy determines when the SDK will automatically create a session after initializing the API. This also controls the default action for the tab in the extension popup.
61 | * CAST_THIS_TAB: No automatic launch is done after initializing the API, even if the tab is being cast.
62 | * CREATE_SESSION: If the tab containing the app is being casted when the API initializes, the SDK stops tab casting and automatically launches the app.
63 | * @type {Object}
64 | */
65 | DefaultActionPolicy: { CREATE_SESSION: "create_session", CAST_THIS_TAB: "cast_this_tab" },
66 |
67 | /**
68 | * Errors that may be returned by the SDK.
69 | * API_NOT_INITIALIZED: The API is not initialized.
70 | * CANCEL: The operation was canceled by the user.
71 | * CHANNEL_ERROR: A channel to the receiver is not available.
72 | * EXTENSION_MISSING: The Cast extension is not available.
73 | * EXTENSION_NOT_COMPATIBLE: The API script is not compatible with the installed Cast extension.
74 | * INVALID_PARAMETER: The parameters to the operation were not valid.
75 | * LOAD_MEDIA_FAILED: Load media failed.
76 | * RECEIVER_UNAVAILABLE: No receiver was compatible with the session request.
77 | * SESSION_ERROR: A session could not be created, or a session was invalid.
78 | * TIMEOUT: The operation timed out.
79 | * @type {Object}
80 | */
81 | ErrorCode: {
82 | API_NOT_INITIALIZED: "api_not_initialized",
83 | CANCEL: "cancel",
84 | CHANNEL_ERROR: "channel_error",
85 | EXTENSION_MISSING: "extension_missing",
86 | EXTENSION_NOT_COMPATIBLE: "extension_not_compatible",
87 | INVALID_PARAMETER: "invalid_parameter",
88 | LOAD_MEDIA_FAILED: "load_media_failed",
89 | RECEIVER_UNAVAILABLE: "receiver_unavailable",
90 | SESSION_ERROR: "session_error",
91 | TIMEOUT: "timeout",
92 | UNKNOWN: "unknown",
93 | NOT_IMPLEMENTED: "not_implemented"
94 | },
95 |
96 | /**
97 | * TODO: Update when the official API docs are finished
98 | * https://developers.google.com/cast/docs/reference/chrome/chrome.cast.timeout
99 | * @type {Object}
100 | */
101 | timeout: {
102 | requestSession: 10000,
103 | sendCustomMessage: 3000,
104 | setReceiverVolume: 3000,
105 | stopSession: 3000
106 | },
107 |
108 | /**
109 | * Flag for clients to check whether the API is loaded.
110 | * @type {Boolean}
111 | */
112 | isAvailable: false,
113 |
114 |
115 | // CLASSES CLASSES CLASSES CLASSES CLASSES CLASSES CLASSES
116 |
117 | /**
118 | * [ApiConfig description]
119 | * @param {chrome.cast.SessionRequest} sessionRequest Describes the session to launch or the session to connect.
120 | * @param {function} sessionListener Listener invoked when a session is created or connected by the SDK.
121 | * @param {function} receiverListener Function invoked when the availability of a Cast receiver that supports the application in sessionRequest is known or changes.
122 | * @param {chrome.cast.AutoJoinPolicy} autoJoinPolicy Determines whether the SDK will automatically connect to a running session after initialization.
123 | * @param {chrome.cast.DefaultActionPolicy} defaultActionPolicy Requests whether the application should be launched on API initialization when the tab is already being cast.
124 | */
125 | ApiConfig: function (sessionRequest, sessionListener, receiverListener, autoJoinPolicy, defaultActionPolicy) {
126 | this.sessionRequest = sessionRequest;
127 | this.sessionListener = sessionListener;
128 | this.receiverListener = receiverListener;
129 | this.autoJoinPolicy = autoJoinPolicy || chrome.cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
130 | this.defaultActionPolicy = defaultActionPolicy || chrome.cast.DefaultActionPolicy.CREATE_SESSION;
131 | },
132 |
133 | /**
134 | * Describes the receiver running an application. Normally, these objects should not be created by the client.
135 | * @param {string} label An identifier for the receiver that is unique to the browser profile and the origin of the API client.
136 | * @param {string} friendlyName The user given name for the receiver.
137 | * @param {chrome.cast.Capability[]} capabilities The capabilities of the receiver, for example audio and video.
138 | * @param {chrome.cast.Volume} volume The current volume of the receiver.
139 | */
140 | Receiver: function (label, friendlyName, capabilities, volume) {
141 | this.label = label;
142 | this.friendlyName = friendlyName;
143 | this.capabilities = capabilities || [];
144 | this.volume = volume || null;
145 | this.receiverType = chrome.cast.ReceiverType.CAST;
146 | this.isActiveInput = null;
147 | },
148 |
149 | /**
150 | * TODO: Update when the official API docs are finished
151 | * https://developers.google.com/cast/docs/reference/chrome/chrome.cast.DialRequest
152 | * @param {[type]} appName [description]
153 | * @param {[type]} launchParameter [description]
154 | */
155 | DialRequest: function (appName, launchParameter) {
156 | this.appName = appName;
157 | this.launchParameter = launchParameter;
158 | },
159 |
160 | /**
161 | * A request to start or connect to a session.
162 | * @param {string} appId The receiver application id.
163 | * @param {chrome.cast.Capability[]} capabilities Capabilities required of the receiver device.
164 | * @property {chrome.cast.DialRequest} dialRequest If given, the SDK will also discover DIAL devices that support the DIAL application given in the dialRequest.
165 | */
166 | SessionRequest: function (appId, capabilities) {
167 | this.appId = appId;
168 | this.capabilities = capabilities || [chrome.cast.Capability.VIDEO_OUT, chrome.cast.Capability.AUDIO_OUT];
169 | this.dialRequest = null;
170 | },
171 |
172 | /**
173 | * Describes an error returned by the API. Normally, these objects should not be created by the client.
174 | * @param {chrome.cast.ErrorCode} code The error code.
175 | * @param {string} description Human readable description of the error.
176 | * @param {Object} details Details specific to the error.
177 | */
178 | Error: function (code, description, details) {
179 | this.code = code;
180 | this.description = description || null;
181 | this.details = details || null;
182 | },
183 |
184 | /**
185 | * An image that describes a receiver application or media item. This could be an application icon, cover art, or a thumbnail.
186 | * @param {string} url The URL to the image.
187 | * @property {number} height The height of the image
188 | * @property {number} width The width of the image
189 | */
190 | Image: function (url) {
191 | this.url = url;
192 | this.width = this.height = null;
193 | },
194 |
195 | /**
196 | * Describes a sender application. Normally, these objects should not be created by the client.
197 | * @param {chrome.cast.SenderPlatform} platform The supported platform.
198 | * @property {string} packageId The identifier or URL for the application in the respective platform's app store.
199 | * @property {string} url URL or intent to launch the application.
200 | */
201 | SenderApplication: function (platform) {
202 | this.platform = platform;
203 | this.packageId = this.url = null;
204 | },
205 |
206 | /**
207 | * The volume of a device or media stream.
208 | * @param {number} level The current volume level as a value between 0.0 and 1.0.
209 | * @param {boolean} muted Whether the receiver is muted, independent of the volume level.
210 | */
211 | Volume: function (level, muted) {
212 | this.level = level || null;
213 | this.muted = null;
214 | if (muted === true || muted === false) {
215 | this.muted = muted;
216 | }
217 | },
218 |
219 |
220 | // media package
221 | media: {
222 | /**
223 | * The default receiver app.
224 | */
225 | DEFAULT_MEDIA_RECEIVER_APP_ID: 'CC1AD845',
226 |
227 | /**
228 | * Possible states of the media player.
229 | * BUFFERING: Player is in PLAY mode but not actively playing content. currentTime will not change.
230 | * IDLE: No media is loaded into the player.
231 | * PAUSED: The media is not playing.
232 | * PLAYING: The media is playing.
233 | * @type {Object}
234 | */
235 | PlayerState: { IDLE: "IDLE", PLAYING: "PLAYING", PAUSED: "PAUSED", BUFFERING: "BUFFERING" },
236 |
237 | /**
238 | * States of the media player after resuming.
239 | * PLAYBACK_PAUSE: Force media to pause.
240 | * PLAYBACK_START: Force media to start.
241 | * @type {Object}
242 | */
243 | ResumeState: { PLAYBACK_START: "PLAYBACK_START", PLAYBACK_PAUSE: "PLAYBACK_PAUSE" },
244 |
245 | /**
246 | * Possible media commands supported by the receiver application.
247 | * @type {Object}
248 | */
249 | MediaCommand: { PAUSE: "pause", SEEK: "seek", STREAM_VOLUME: "stream_volume", STREAM_MUTE: "stream_mute" },
250 |
251 | /**
252 | * Possible types of media metadata.
253 | * GENERIC: Generic template suitable for most media types. Used by chrome.cast.media.GenericMediaMetadata.
254 | * MOVIE: A full length movie. Used by chrome.cast.media.MovieMediaMetadata.
255 | * MUSIC_TRACK: A music track. Used by chrome.cast.media.MusicTrackMediaMetadata.
256 | * PHOTO: Photo. Used by chrome.cast.media.PhotoMediaMetadata.
257 | * TV_SHOW: An episode of a TV series. Used by chrome.cast.media.TvShowMediaMetadata.
258 | * @type {Object}
259 | */
260 | MetadataType: { GENERIC: 0, TV_SHOW: 1, MOVIE: 2, MUSIC_TRACK: 3, PHOTO: 4 },
261 |
262 | /**
263 | * Possible media stream types.
264 | * BUFFERED: Stored media streamed from an existing data store.
265 | * LIVE: Live media generated on the fly.
266 | * OTHER: None of the above.
267 | * @type {Object}
268 | */
269 | StreamType: { BUFFERED: 'buffered', LIVE: 'live', OTHER: 'other' },
270 |
271 | /**
272 | * TODO: Update when the official API docs are finished
273 | * https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.timeout
274 | * @type {Object}
275 | */
276 | timeout: {
277 | load: 0,
278 | ob: 0,
279 | pause: 0,
280 | play: 0,
281 | seek: 0,
282 | setVolume: 0,
283 | stop: 0
284 | },
285 |
286 |
287 | // CLASSES CLASSES CLASSES CLASSES CLASSES CLASSES CLASSES
288 |
289 | /**
290 | * A request to load new media into the player.
291 | * @param {chrome.cast.media.MediaInfo} media Media description.
292 | * @property {boolean} autoplay Whether the media will automatically play.
293 | * @property {number} currentTime Seconds from the beginning of the media to start playback.
294 | * @property {Object} customData Custom data for the receiver application.
295 | */
296 | LoadRequest: function (media) {
297 | this.type = 'LOAD';
298 | this.sessionId = this.requestId = this.customData = this.currentTime = null;
299 | this.media = media;
300 | this.autoplay = !0;
301 | },
302 |
303 | /**
304 | * A request to play the currently paused media.
305 | * @property {Object} customData Custom data for the receiver application.
306 | */
307 | PlayRequest: function () {
308 | this.customData = null;
309 | },
310 |
311 | /**
312 | * A request to seek the current media.
313 | * @property {number} currentTime The new current time for the media, in seconds after the start of the media.
314 | * @property {chrome.cast.media.ResumeState} resumeState The desired media player state after the seek is complete.
315 | * @property {Object} customData Custom data for the receiver application.
316 | */
317 | SeekRequest: function () {
318 | this.customData = this.resumeState = this.currentTime = null;
319 | },
320 |
321 | /**
322 | * A request to set the stream volume of the playing media.
323 | * @param {chrome.cast.Volume} volume The new volume of the stream.
324 | * @property {Object} customData Custom data for the receiver application.
325 | */
326 | VolumeRequest: function (volume) {
327 | this.volume = volume;
328 | this.customData = null;
329 | },
330 |
331 | /**
332 | * A request to stop the media player.
333 | * @property {Object} customData Custom data for the receiver application.
334 | */
335 | StopRequest: function () {
336 | this.customData = null;
337 | },
338 |
339 | /**
340 | * A request to pause the currently playing media.
341 | * @property {Object} customData Custom data for the receiver application.
342 | */
343 | PauseRequest: function () {
344 | this.customData = null;
345 | },
346 |
347 | /**
348 | * A generic media description.
349 | * @property {chrome.cast.Image[]} images Content images.
350 | * @property {string} releaseDate ISO 8601 date and/or time when the content was released, e.g.
351 | * @property {number} releaseYear Integer year when the content was released.
352 | * @property {string} subtitle Content subtitle.
353 | * @property {string} title Content title.
354 | * @property {chrome.cast.media.MetadataType} type The type of metadata.
355 | */
356 | GenericMediaMetadata: function () {
357 | this.metadataType = this.type = chrome.cast.media.MetadataType.GENERIC;
358 | this.releaseDate = this.releaseYear = this.images = this.subtitle = this.title = null;
359 | },
360 |
361 | /**
362 | * A movie media description.
363 | * @property {chrome.cast.Image[]} images Content images.
364 | * @property {string} releaseDate ISO 8601 date and/or time when the content was released, e.g.
365 | * @property {number} releaseYear Integer year when the content was released.
366 | * @property {string} studio Movie studio
367 | * @property {string} subtitle Content subtitle.
368 | * @property {string} title Content title.
369 | * @property {chrome.cast.media.MetadataType} type The type of metadata.
370 | */
371 | MovieMediaMetadata: function () {
372 | this.metadataType = this.type = chrome.cast.media.MetadataType.MOVIE;
373 | this.releaseDate = this.releaseYear = this.images = this.subtitle = this.studio = this.title = null;
374 | },
375 |
376 | /**
377 | * A music track media description.
378 | * @property {string} albumArtist Album artist name.
379 | * @property {string} albumName Album name.
380 | * @property {string} artist Track artist name.
381 | * @property {string} artistName Track artist name.
382 | * @property {string} composer Track composer name.
383 | * @property {number} discNumber Disc number.
384 | * @property {chrome.cast.Image[]} images Content images.
385 | * @property {string} releaseDate ISO 8601 date when the track was released, e.g.
386 | * @property {number} releaseYear Integer year when the album was released.
387 | * @property {string} songName Track name.
388 | * @property {string} title Track title.
389 | * @property {number} trackNumber Track number in album.
390 | * @property {chrome.cast.media.MetadataType} type The type of metadata.
391 | */
392 | MusicTrackMediaMetadata: function () {
393 | this.metadataType = this.type = chrome.cast.media.MetadataType.MUSIC_TRACK;
394 | this.releaseDate = this.releaseYear = this.images = this.discNumber = this.trackNumber = this.artistName = this.songName = this.composer = this.artist = this.albumArtist = this.title = this.albumName = null;
395 | },
396 |
397 | /**
398 | * A photo media description.
399 | * @property {string} artist Name of the photographer.
400 | * @property {string} creationDateTime ISO 8601 date and time the photo was taken, e.g.
401 | * @property {number} height Photo height, in pixels.
402 | * @property {chrome.cast.Image[]} images Images associated with the content.
403 | * @property {number} latitude Latitude.
404 | * @property {string} location Location where the photo was taken.
405 | * @property {number} longitude Longitude.
406 | * @property {string} title Photo title.
407 | * @property {chrome.cast.media.MetadataType} type The type of metadata.
408 | * @property {number} width Photo width, in pixels.
409 | */
410 | PhotoMediaMetadata: function () {
411 | this.metadataType = this.type = chrome.cast.media.MetadataType.PHOTO;
412 | this.creationDateTime = this.height = this.width = this.longitude = this.latitude = this.images = this.location = this.artist = this.title = null;
413 | },
414 |
415 | /**
416 | * [TvShowMediaMetadata description]
417 | * @property {number} episode TV episode number.
418 | * @property {number} episodeNumber TV episode number.
419 | * @property {string} episodeTitle TV episode title.
420 | * @property {chrome.cast.Image[]} images Content images.
421 | * @property {string} originalAirdate ISO 8601 date when the episode originally aired, e.g.
422 | * @property {number} releaseYear Integer year when the content was released.
423 | * @property {number} season TV episode season.
424 | * @property {number} seasonNumber TV episode season.
425 | * @property {string} seriesTitle TV series title.
426 | * @property {string} title TV episode title.
427 | * @property {chrome.cast.media.MetadataType} type The type of metadata.
428 | */
429 | TvShowMediaMetadata: function () {
430 | this.metadataType = this.type = chrome.cast.media.MetadataType.TV_SHOW;
431 | this.originalAirdate = this.releaseYear = this.images = this.episode = this.episodeNumber = this.season = this.seasonNumber = this.episodeTitle = this.title = this.seriesTitle = null;
432 | },
433 |
434 | /**
435 | * Describes a media item.
436 | * @param {string} contentId Identifies the content.
437 | * @param {string} contentType MIME content type of the media.
438 | * @property {Object} customData Custom data set by the receiver application.
439 | * @property {number} duration Duration of the content, in seconds.
440 | * @property {any type} metadata Describes the media content.
441 | * @property {chrome.cast.media.StreamType} streamType The type of media stream.
442 | */
443 | MediaInfo: function (contentId, contentType) {
444 | this.contentId = contentId;
445 | this.streamType = chrome.cast.media.StreamType.BUFFERED;
446 | this.contentType = contentType;
447 | this.customData = this.duration = this.metadata = null;
448 | }
449 | }
450 | };
451 |
452 | var _sessionRequest = null;
453 | var _autoJoinPolicy = null;
454 | var _defaultActionPolicy = null;
455 | var _receiverListener = null;
456 | var _sessionListener = null;
457 |
458 | var _sessions = {};
459 | var _currentMedia = null;
460 | var _routeListEl = document.createElement('ul');
461 | _routeListEl.classList.add('route-list');
462 | var _routeList = {};
463 | var _routeRefreshInterval = null;
464 |
465 | var _receiverAvailable = false;
466 |
467 | /**
468 | * Initializes the API. Note that either successCallback and errorCallback will be invoked once the API has finished initialization.
469 | * The sessionListener and receiverListener may be invoked at any time afterwards, and possibly more than once.
470 | * @param {chrome.cast.ApiConfig} apiConfig The object with parameters to initialize the API. Must not be null.
471 | * @param {function} successCallback
472 | * @param {function} errorCallback
473 | */
474 | chrome.cast.initialize = function (apiConfig, successCallback, errorCallback) {
475 | if (!chrome.cast.isAvailable) {
476 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
477 | return;
478 | }
479 |
480 | _sessionListener = apiConfig.sessionListener;
481 | _autoJoinPolicy = apiConfig.autoJoinPolicy;
482 | _defaultActionPolicy = apiConfig.defaultActionPolicy;
483 | _receiverListener = apiConfig.receiverListener;
484 | _sessionRequest = apiConfig.sessionRequest;
485 |
486 | execute('initialize', _sessionRequest.appId, _autoJoinPolicy, _defaultActionPolicy, function(err) {
487 | if (!err) {
488 | successCallback();
489 |
490 | clearInterval(_routeRefreshInterval);
491 | _routeRefreshInterval = setInterval(function() {
492 | execute('emitAllRoutes');
493 | }, 15000);
494 |
495 | } else {
496 | handleError(err, errorCallback);
497 | }
498 | });
499 | };
500 |
501 | /**
502 | * Requests that a receiver application session be created or joined.
503 | * By default, the SessionRequest passed to the API at initialization time is used;
504 | * this may be overridden by passing a different session request in opt_sessionRequest.
505 | * @param {function} successCallback
506 | * @param {function} errorCallback The possible errors are TIMEOUT, INVALID_PARAMETER, API_NOT_INITIALIZED, CANCEL, CHANNEL_ERROR, SESSION_ERROR, RECEIVER_UNAVAILABLE, and EXTENSION_MISSING. Note that the timeout timer starts after users select a receiver. Selecting a receiver requires user's action, which has no timeout.
507 | * @param {chrome.cast.SessionRequest} opt_sessionRequest
508 | */
509 | chrome.cast.requestSession = function (successCallback, errorCallback, opt_sessionRequest) {
510 | if (chrome.cast.isAvailable === false) {
511 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
512 | return;
513 | }
514 | if (_receiverAvailable === false) {
515 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.RECEIVER_UNAVAILABLE, 'No receiver was compatible with the session request.', {}));
516 | return;
517 | }
518 |
519 | execute('requestSession', function(err, obj) {
520 | if (!err) {
521 | var sessionId = obj.sessionId;
522 | var appId = obj.appId;
523 | var displayName = obj.displayName;
524 | var appImages = obj.appImages || [];
525 | var receiver = new chrome.cast.Receiver(obj.receiver.label, obj.receiver.friendlyName, obj.receiver.capabilities || [], obj.volume || null);
526 |
527 | var session = _sessions[sessionId] = new chrome.cast.Session(sessionId, appId, displayName, appImages, receiver);
528 |
529 | if (obj.media && obj.media.sessionId)
530 | {
531 | _currentMedia = new chrome.cast.media.Media(sessionId, obj.media.mediaSessionId);
532 | _currentMedia.currentTime = obj.media.currentTime;
533 | _currentMedia.playerState = obj.media.playerState;
534 | _currentMedia.media = obj.media.media;
535 | session.media[0] = _currentMedia;
536 | }
537 |
538 | successCallback(session);
539 | _sessionListener(session); /*Fix - Already has a sessionListener*/
540 | } else {
541 | handleError(err, errorCallback);
542 | }
543 | });
544 | };
545 |
546 | /**
547 | * Sets custom receiver list
548 | * @param {chrome.cast.Receiver[]} receivers The new list. Must not be null.
549 | * @param {function} successCallback
550 | * @param {function} errorCallback
551 | */
552 | chrome.cast.setCustomReceivers = function (receivers, successCallback, errorCallback) {
553 | // TODO: Implement
554 | };
555 |
556 |
557 |
558 |
559 | /** SESSION SESSION SESSION SESSION SESSION SESSION SESSION SESSION **/
560 |
561 |
562 |
563 |
564 | /**
565 | * Describes the state of a currently running Cast application. Normally, these objects should not be created by the client.
566 | * @param {string} sessionId Uniquely identifies this instance of the receiver application.
567 | * @param {string} appId The identifer of the Cast application.
568 | * @param {string} displayName The human-readable name of the Cast application, for example, "YouTube".
569 | * @param {chrome.cast.Image[]} appImages Array of images available describing the application.
570 | * @param {chrome.cast.Receiver} receiver The receiver that is running the application.
571 | *
572 | * @property {Object} customData Custom data set by the receiver application.
573 | * @property {chrome.cast.media.Media} media The media that belong to this Cast session, including those loaded by other senders.
574 | * @property {Object[]} namespaces A list of the namespaces supported by the receiver application.
575 | * @property {chrome.cast.SenderApplication} senderApps The sender applications supported by the receiver application.
576 | * @property {string} statusText Descriptive text for the current application content, for example “My Wedding Slideshow”.
577 | */
578 | chrome.cast.Session = function(sessionId, appId, displayName, appImages, receiver) {
579 | EventEmitter.call(this);
580 | this.sessionId = sessionId;
581 | this.appId = appId;
582 | this.displayName = displayName;
583 | this.appImages = appImages || [];
584 | this.receiver = receiver;
585 | this.media = [];
586 | };
587 | chrome.cast.Session.prototype = Object.create(EventEmitter.prototype);
588 |
589 | /**
590 | * Sets the receiver volume.
591 | * @param {number} newLevel The new volume level between 0.0 and 1.0.
592 | * @param {function} successCallback
593 | * @param {function} errorCallback The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, and EXTENSION_MISSING.
594 | */
595 | chrome.cast.Session.prototype.setReceiverVolumeLevel = function (newLevel, successCallback, errorCallback) {
596 | if (chrome.cast.isAvailable === false) {
597 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
598 | return;
599 | }
600 |
601 | execute('setReceiverVolumeLevel', newLevel, function(err) {
602 | if (!err) {
603 | successCallback && successCallback();
604 | } else {
605 | handleError(err, errorCallback);
606 | }
607 | });
608 | };
609 |
610 |
611 | /**
612 | * Sets the receiver volume.
613 | * @param {boolean} muted The new muted status.
614 | * @param {function} successCallback
615 | * @param {function} errorCallback The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, and EXTENSION_MISSING.
616 | */
617 | chrome.cast.Session.prototype.setReceiverMuted = function (muted, successCallback, errorCallback) {
618 | if (chrome.cast.isAvailable === false) {
619 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
620 | return;
621 | }
622 |
623 | execute('setReceiverMuted', muted, function(err) {
624 | if (!err) {
625 | successCallback && successCallback();
626 | } else {
627 | handleError(err, errorCallback);
628 | }
629 | });
630 | };
631 |
632 | /**
633 | * Stops the running receiver application associated with the session.
634 | * @param {function} successCallback
635 | * @param {function} errorCallback The possible errors are TIMEOUT, API_NOT_INITIALIZED, CHANNEL_ERROR, and EXTENSION_MISSING.
636 | */
637 | chrome.cast.Session.prototype.stop = function (successCallback, errorCallback) {
638 | if (chrome.cast.isAvailable === false) {
639 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
640 | return;
641 | }
642 |
643 | execute('sessionStop', function(err) {
644 | if (!err) {
645 | successCallback && successCallback();
646 | } else {
647 | handleError(err, errorCallback);
648 | }
649 | });
650 | };
651 |
652 | /**
653 | * Leaves the current session.
654 | * @param {function} successCallback
655 | * @param {function} errorCallback The possible errors are TIMEOUT, API_NOT_INITIALIZED, CHANNEL_ERROR, and EXTENSION_MISSING.
656 | */
657 | chrome.cast.Session.prototype.leave = function (successCallback, errorCallback) {
658 | if (chrome.cast.isAvailable === false) {
659 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
660 | return;
661 | }
662 |
663 | execute('sessionLeave', function(err) {
664 | if (!err) {
665 | successCallback && successCallback();
666 | } else {
667 | handleError(err, errorCallback);
668 | }
669 | });
670 | };
671 |
672 | /**
673 | * Sends a message to the receiver application on the given namespace.
674 | * The successCallback is invoked when the message has been submitted to the messaging channel.
675 | * Delivery to the receiver application is best effort and not guaranteed.
676 | * @param {string} namespace
677 | * @param {Object or string} message Must not be null
678 | * @param {[type]} successCallback Invoked when the message has been sent. Must not be null.
679 | * @param {[type]} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING
680 | */
681 | chrome.cast.Session.prototype.sendMessage = function (namespace, message, successCallback, errorCallback) {
682 | if (chrome.cast.isAvailable === false) {
683 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
684 | return;
685 | }
686 |
687 | if (typeof message === 'object') {
688 | message = JSON.stringify(message);
689 | }
690 | execute('sendMessage', namespace, message, function(err) {
691 | if (!err) {
692 | successCallback && successCallback();
693 | } else {
694 | handleError(err, errorCallback);
695 | }
696 | });
697 | };
698 |
699 | /**
700 | * Request to load media. Must not be null.
701 | * @param {chrome.cast.media.LoadRequest} loadRequest Request to load media. Must not be null.
702 | * @param {function} successCallback Invoked with the loaded Media on success.
703 | * @param {function} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING.
704 | */
705 | chrome.cast.Session.prototype.loadMedia = function (loadRequest, successCallback, errorCallback) {
706 | if (chrome.cast.isAvailable === false) {
707 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
708 | return;
709 | }
710 |
711 | var self = this;
712 |
713 | var mediaInfo = loadRequest.media;
714 | execute('loadMedia', mediaInfo.contentId, mediaInfo.contentType, mediaInfo.duration || 0.0, mediaInfo.streamType, loadRequest.autoplay || false, loadRequest.currentTime || 0, mediaInfo.metadata, function(err, obj) {
715 | if (!err) {
716 | _currentMedia = new chrome.cast.media.Media(self.sessionId, obj.mediaSessionId);
717 | _currentMedia.media = mediaInfo;
718 | _currentMedia.media.duration = obj.media.duration;
719 | _currentMedia.currentTime = obj.currentTime /* ??? */
720 |
721 | // TODO: Fill in the rest of the media properties
722 |
723 | successCallback(_currentMedia);
724 |
725 | } else {
726 | handleError(err, errorCallback);
727 | }
728 | });
729 | };
730 |
731 | /**
732 | * Adds a listener that is invoked when the status of the Session has changed.
733 | * Changes to the following properties will trigger the listener: statusText, namespaces, customData, and the volume of the receiver.
734 | * The callback will be invoked with 'true' (isAlive) parameter.
735 | * When this session is ended, the callback will be invoked with 'false' (isAlive);
736 | * @param {function} listener The listener to add.
737 | */
738 | chrome.cast.Session.prototype.addUpdateListener = function (listener) {
739 | this.on('_sessionUpdated', listener);
740 | };
741 |
742 | /**
743 | * Removes a previously added listener for this Session.
744 | * @param {function} listener The listener to remove.
745 | */
746 | chrome.cast.Session.prototype.removeUpdateListener = function (listener) {
747 | this.removeListener('_sessionUpdated', listener);
748 | };
749 |
750 | /**
751 | * Adds a listener that is invoked when a message is received from the receiver application.
752 | * The listener is invoked with the the namespace as the first argument and the message as the second argument.
753 | * @param {string} namespace The namespace to listen on.
754 | * @param {function} listener The listener to add.
755 | */
756 | chrome.cast.Session.prototype.addMessageListener = function (namespace, listener) {
757 | execute('addMessageListener', namespace);
758 | this.on('message:' + namespace, listener);
759 | };
760 |
761 | /**
762 | * Removes a previously added listener for messages.
763 | * @param {string} namespace The namespace that is listened to.
764 | * @param {function} listener The listener to remove.
765 | */
766 | chrome.cast.Session.prototype.removeMessageListener = function (namespace, listener) {
767 | this.removeListener('message:' + namespace, listener);
768 | };
769 |
770 | /**
771 | * Adds a listener that is invoked when a media session is created by another sender.
772 | * @param {function} listener The listener to add.
773 | */
774 | chrome.cast.Session.prototype.addMediaListener = function (listener) {
775 | this.on('_mediaListener', listener);
776 | };
777 |
778 | /**
779 | * Removes a listener that was previously added with addMediaListener.
780 | * @param {function} listener The listener to remove.
781 | */
782 | chrome.cast.Session.prototype.removeMediaListener = function (listener) {
783 | this.removeListener('_mediaListener', listener);
784 | };
785 |
786 |
787 | chrome.cast.Session.prototype._update = function(isAlive, obj) {
788 |
789 | this.appId = obj.appId;
790 | this.appImages = obj.appImages;
791 | this.displayName = obj.displayName;
792 | if (obj.receiver) {
793 | if (!this.receiver) {
794 | this.receiver = new chrome.cast.Receiver(null, null, null, null);
795 | }
796 | this.receiver.friendlyName = obj.receiver.friendlyName;
797 | this.receiver.label = obj.receiver.label;
798 |
799 | if (obj.receiver.volume) {
800 | this.receiver.volume = new chrome.cast.Volume(obj.receiver.volume.level, obj.receiver.volume.muted);
801 | }
802 | }
803 |
804 | this.emit('_sessionUpdated', isAlive);
805 | };
806 |
807 |
808 |
809 |
810 |
811 | /* MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA MEDIA */
812 | /**
813 | * Represents a media item that has been loaded into the receiver application.
814 | * @param {string} sessionId Identifies the session that is hosting the media.
815 | * @param {number} mediaSessionId Identifies the media item.
816 | *
817 | * @property {Object} customData Custom data set by the receiver application.
818 | * @property {number} currentTime The current playback position in seconds since the start of the media.
819 | * @property {chrome.cast.media.MediaInfo} media Media description.
820 | * @property {number} playbackRate The playback rate.
821 | * @property {chrome.cast.media.PlayerState} playerState The player state.
822 | * @property {chrome.cast.media.MediaCommand[]} supportedMediaCommands The media commands supported by the media player.
823 | * @property {chrome.cast.Volume} volume The media stream volume.
824 | * @property {string} idleReason Reason for idling
825 | */
826 | chrome.cast.media.Media = function(sessionId, mediaSessionId) {
827 | EventEmitter.call(this);
828 | this.sessionId = sessionId;
829 | this.mediaSessionId = mediaSessionId;
830 | this.currentTime = 0;
831 | this.playbackRate = 1;
832 | this.playerState = chrome.cast.media.PlayerState.BUFFERING;
833 | this.supportedMediaCommands = [
834 | chrome.cast.media.MediaCommand.PAUSE,
835 | chrome.cast.media.MediaCommand.SEEK,
836 | chrome.cast.media.MediaCommand.STREAM_VOLUME,
837 | chrome.cast.media.MediaCommand.STREAM_MUTE
838 | ];
839 | this.volume = new chrome.cast.Volume(1, false);
840 | this._lastUpdatedTime = Date.now();
841 | this.media = {};
842 | };
843 | chrome.cast.media.Media.prototype = Object.create(EventEmitter.prototype);
844 |
845 | /**
846 | * Plays the media item.
847 | * @param {chrome.cast.media.PlayRequest} playRequest The optional media play request.
848 | * @param {function} successCallback Invoked on success.
849 | * @param {function} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING.
850 | */
851 | chrome.cast.media.Media.prototype.play = function (playRequest, successCallback, errorCallback) {
852 | if (chrome.cast.isAvailable === false) {
853 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
854 | return;
855 | }
856 |
857 | execute('mediaPlay', function(err) {
858 | if (!err) {
859 | successCallback && successCallback();
860 | } else {
861 | handleError(err, errorCallback);
862 | }
863 | });
864 | };
865 |
866 | /**
867 | * Pauses the media item.
868 | * @param {chrome.cast.media.PauseRequest} pauseRequest The optional media pause request.
869 | * @param {function} successCallback Invoked on success.
870 | * @param {function} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING.
871 | */
872 | chrome.cast.media.Media.prototype.pause = function (pauseRequest, successCallback, errorCallback) {
873 | if (chrome.cast.isAvailable === false) {
874 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
875 | return;
876 | }
877 |
878 | execute('mediaPause', function(err) {
879 | if (!err) {
880 | successCallback && successCallback();
881 | } else {
882 | handleError(err, errorCallback);
883 | }
884 | });
885 | };
886 |
887 | /**
888 | * Seeks the media item.
889 | * @param {chrome.cast.media.SeekRequest} seekRequest The media seek request. Must not be null.
890 | * @param {function} successCallback Invoked on success.
891 | * @param {function} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING.
892 | */
893 | chrome.cast.media.Media.prototype.seek = function (seekRequest, successCallback, errorCallback) {
894 | if (chrome.cast.isAvailable === false) {
895 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
896 | return;
897 | }
898 |
899 | execute('mediaSeek', seekRequest.currentTime, seekRequest.resumeState || "", function(err) {
900 | if (!err) {
901 | successCallback && successCallback();
902 | } else {
903 | handleError(err, errorCallback);
904 | }
905 | })
906 | };
907 |
908 | /**
909 | * Stops the media player.
910 | * @param {chrome.cast.media.StopRequest} stopRequest The media stop request.
911 | * @param {function} successCallback Invoked on success.
912 | * @param {function} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING.
913 | */
914 | chrome.cast.media.Media.prototype.stop = function (stopRequest, successCallback, errorCallback) {
915 | if (chrome.cast.isAvailable === false) {
916 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
917 | return;
918 | }
919 |
920 | execute('mediaStop', function(err) {
921 | if (!err) {
922 | successCallback && successCallback();
923 | } else {
924 | handleError(err, errorCallback);
925 | }
926 | })
927 | };
928 |
929 | /**
930 | * Sets the media stream volume. At least one of volumeRequest.level or volumeRequest.muted must be set. Changing the mute state does not affect the volume level, and vice versa.
931 | * @param {chrome.cast.media.VolumeRequest} volumeRequest The set volume request. Must not be null.
932 | * @param {function} successCallback Invoked on success.
933 | * @param {function} errorCallback Invoked on error. The possible errors are TIMEOUT, API_NOT_INITIALIZED, INVALID_PARAMETER, CHANNEL_ERROR, SESSION_ERROR, and EXTENSION_MISSING.
934 | */
935 | chrome.cast.media.Media.prototype.setVolume = function (volumeRequest, successCallback, errorCallback) {
936 | if (chrome.cast.isAvailable === false) {
937 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.API_NOT_INITIALIZED), 'The API is not initialized.', {});
938 | return;
939 | }
940 | var args = [];
941 |
942 | if (volumeRequest.volume.level !== null) {
943 | args.push('setMediaVolume');
944 | args.push(volumeRequest.volume.level);
945 | } else if (volumeRequest.volume.muted !== null) {
946 | args.push('setMediaMuted');
947 | args.push(volumeRequest.volume.muted);
948 | }
949 |
950 | if (args.length < 2) {
951 | errorCallback(new chrome.cast.Error(chrome.cast.ErrorCode.INVALID_PARAMETER), 'Invalid request.', {});
952 | } else {
953 | args.push(function(err) {
954 | if (!err) {
955 | successCallback && successCallback();
956 | } else {
957 | handleError(err, errorCallback);
958 | }
959 | });
960 |
961 | execute.apply(null, args);
962 | }
963 | };
964 |
965 | /**
966 | * Determines whether the media player supports the given media command.
967 | * @param {chrome.cast.media.MediaCommand} command The command to query. Must not be null.
968 | * @returns {boolean} True if the player supports the command.
969 | */
970 | chrome.cast.media.Media.prototype.supportsCommand = function (command) {
971 | return this.supportsCommands.indexOf(command) > -1;
972 | };
973 |
974 | /**
975 | * Estimates the current playback position.
976 | * @returns {number} number An estimate of the current playback position in seconds since the start of the media.
977 | */
978 | chrome.cast.media.Media.prototype.getEstimatedTime = function () {
979 | if (this.playerState === chrome.cast.media.PlayerState.PLAYING) {
980 | var elapsed = (Date.now() - this._lastUpdatedTime) / 1000;
981 | var estimatedTime = this.currentTime + elapsed;
982 |
983 | return estimatedTime;
984 | } else {
985 | return this.currentTime;
986 | }
987 | };
988 |
989 | /**
990 | * Adds a listener that is invoked when the status of the media has changed.
991 | * Changes to the following properties will trigger the listener: currentTime, volume, metadata, playbackRate, playerState, customData.
992 | * @param {function} listener The listener to add. The parameter indicates whether the Media object is still alive.
993 | */
994 | chrome.cast.media.Media.prototype.addUpdateListener = function (listener) {
995 | this.on('_mediaUpdated', listener);
996 | };
997 |
998 | /**
999 | * Removes a previously added listener for this Media.
1000 | * @param {function} listener The listener to remove.
1001 | */
1002 | chrome.cast.media.Media.prototype.removeUpdateListener = function (listener) {
1003 | this.removeListener('_mediaUpdated', listener);
1004 | };
1005 |
1006 | chrome.cast.media.Media.prototype._update = function(isAlive, obj) {
1007 | this.currentTime = obj.currentTime || this.currentTime;
1008 | this.idleReason = obj.idleReason || this.idleReason;
1009 | this.sessionId = obj.sessionId || this.sessionId;
1010 | this.mediaSessionId = obj.mediaSessionId || this.mediaSessionId;
1011 | this.playbackRate = obj.playbackRate || this.playbackRate;
1012 | this.playerState = obj.playerState || this.playerState;
1013 |
1014 | if (obj.media && obj.media.duration) {
1015 | this.media.duration = obj.media.duration || this.media.duration;
1016 | this.media.streamType = obj.media.streamType || this.media.streamType;
1017 | }
1018 |
1019 | this.volume.level = obj.volume.level;
1020 | this.volume.muted = obj.volume.muted;
1021 |
1022 | this._lastUpdatedTime = Date.now();
1023 |
1024 | this.emit('_mediaUpdated', isAlive);
1025 | };
1026 |
1027 |
1028 | function createRouteElement(route) {
1029 | var el = document.createElement('li');
1030 | el.classList.add('route');
1031 | el.addEventListener('touchstart', onRouteClick);
1032 | el.textContent = route.name;
1033 | el.setAttribute('data-routeid', route.id);
1034 | return el;
1035 | }
1036 |
1037 | function onRouteClick() {
1038 | var id = this.getAttribute('data-routeid');
1039 |
1040 | if (id) {
1041 | try {
1042 | chrome.cast._emitConnecting();
1043 | } catch(e) {
1044 | console.error('Error in connectingListener', e);
1045 | }
1046 |
1047 | execute('selectRoute', id, function(err, obj) {
1048 | var sessionId = obj.sessionId;
1049 | var appId = obj.appId;
1050 | var displayName = obj.displayName;
1051 | var appImages = obj.appImages || [];
1052 | var receiver = new chrome.cast.Receiver(obj.receiver.label, obj.receiver.friendlyName, obj.receiver.capabilities || [], obj.volume || null);
1053 |
1054 | var session = _sessions[sessionId] = new chrome.cast.Session(sessionId, appId, displayName, appImages, receiver);
1055 |
1056 | _sessionListener && _sessionListener(session);
1057 | });
1058 | }
1059 | }
1060 |
1061 | chrome.cast.getRouteListElement = function() {
1062 | return _routeListEl;
1063 | };
1064 |
1065 |
1066 | var _connectingListeners = [];
1067 | chrome.cast.addConnectingListener = function(cb) {
1068 | _connectingListeners.push(cb);
1069 | };
1070 |
1071 | chrome.cast.removeConnectingListener = function(cb) {
1072 | if (_connectingListeners.indexOf(cb) > -1) {
1073 | _connectingListeners.splice(_connectingListeners.indexOf(cb), 1);
1074 | }
1075 | };
1076 |
1077 | chrome.cast._emitConnecting = function() {
1078 | for (var n = 0; n < _connectingListeners.length; n++) {
1079 | _connectingListeners[n]();
1080 | }
1081 | };
1082 |
1083 | chrome.cast._ = {
1084 | receiverUnavailable: function() {
1085 | _receiverListener(chrome.cast.ReceiverAvailability.UNAVAILABLE);
1086 | _receiverAvailable = false;
1087 | },
1088 | receiverAvailable: function() {
1089 | _receiverListener(chrome.cast.ReceiverAvailability.AVAILABLE);
1090 | _receiverAvailable = true;
1091 | },
1092 | routeAdded: function(route) {
1093 | if (!_routeList[route.id]) {
1094 | route.el = createRouteElement(route);
1095 | _routeList[route.id] = route;
1096 |
1097 | _routeListEl.appendChild(route.el);
1098 | }
1099 | },
1100 | routeRemoved: function(route) {
1101 | if (_routeList[route.id]) {
1102 | _routeList[route.id].el.remove();
1103 | delete _routeList[route.id];
1104 | }
1105 | },
1106 | sessionUpdated: function(isAlive, session) {
1107 | if (session && session.sessionId && _sessions[session.sessionId]) {
1108 | _sessions[session.sessionId]._update(isAlive, session);
1109 | }
1110 | },
1111 | mediaUpdated: function(isAlive, media) {
1112 |
1113 | if (media && media.mediaSessionId !== undefined)
1114 | {
1115 | if (_currentMedia) {
1116 | _currentMedia._update(isAlive, media);
1117 | } else {
1118 | _currentMedia = new chrome.cast.media.Media(media.sessionId, media.mediaSessionId);
1119 | _currentMedia.currentTime = media.currentTime;
1120 | _currentMedia.playerState = media.playerState;
1121 | _currentMedia.media = media.media;
1122 |
1123 | _sessions[media.sessionId].media[0] = _currentMedia;
1124 | _sessionListener && _sessionListener(_sessions[media.sessionId]);
1125 | }
1126 | }
1127 | },
1128 | mediaLoaded: function(isAlive, media) {
1129 | if (_sessions[media.sessionId]) {
1130 |
1131 | if (!_currentMedia)
1132 | {
1133 | _currentMedia = new chrome.cast.media.Media(media.sessionId, media.mediaSessionId);
1134 | }
1135 | _currentMedia._update(isAlive, media);
1136 | _sessions[media.sessionId].emit('_mediaListener', _currentMedia);
1137 | } else {
1138 | console.log('mediaLoaded --- but there is no session tied to it', media);
1139 | }
1140 | },
1141 | sessionJoined: function(obj) {
1142 | var sessionId = obj.sessionId;
1143 | var appId = obj.appId;
1144 | var displayName = obj.displayName;
1145 | var appImages = obj.appImages || [];
1146 | var receiver = new chrome.cast.Receiver(obj.receiver.label, obj.receiver.friendlyName, obj.receiver.capabilities || [], obj.volume || null);
1147 |
1148 | var session = _sessions[sessionId] = new chrome.cast.Session(sessionId, appId, displayName, appImages, receiver);
1149 |
1150 | if (obj.media && obj.media.sessionId)
1151 | {
1152 | _currentMedia = new chrome.cast.media.Media(sessionId, obj.media.mediaSessionId);
1153 | _currentMedia.currentTime = obj.media.currentTime;
1154 | _currentMedia.playerState = obj.media.playerState;
1155 | _currentMedia.media = obj.media.media;
1156 | session.media[0] = _currentMedia;
1157 | }
1158 |
1159 | _sessionListener && _sessionListener(session);
1160 | },
1161 | onMessage: function(sessionId, namespace, message) {
1162 | if (_sessions[sessionId]) {
1163 | _sessions[sessionId].emit('message:' + namespace, namespace, message);
1164 | }
1165 | }
1166 | };
1167 |
1168 |
1169 | module.exports = chrome.cast;
1170 |
1171 | function execute (action) {
1172 | var args = [].slice.call(arguments);
1173 | args.shift();
1174 | var callback;
1175 | if (args[args.length-1] instanceof Function) {
1176 | callback = args.pop();
1177 | }
1178 | cordova.exec(function (result) { callback && callback(null, result); }, function(err) { callback && callback(err); }, "Chromecast", action, args);
1179 | }
1180 |
1181 | function handleError(err, callback) {
1182 | var errorCode = chrome.cast.ErrorCode.UNKNOWN;
1183 | var errorDescription = err;
1184 | var errorData = {};
1185 |
1186 | err = err || "";
1187 | if (err.toUpperCase() === 'TIMEOUT') {
1188 | errorCode = chrome.cast.ErrorCode.TIMEOUT;
1189 | errorDescription = 'The operation timed out.';
1190 | } else if (err.toUpperCase() === 'INVALID_PARAMETER') {
1191 | errorCode = chrome.cast.ErrorCode.INVALID_PARAMETER;
1192 | errorDescription = 'The parameters to the operation were not valid.';
1193 | } else if (err.toUpperCase() === 'RECEIVER_UNAVAILABLE') {
1194 | errorCode = chrome.cast.ErrorCode.RECEIVER_UNAVAILABLE;
1195 | errorDescription = 'No receiver was compatible with the session request.';
1196 | } else if (err.toUpperCase() === 'CANCEL') {
1197 | errorCode = chrome.cast.ErrorCode.CANCEL;
1198 | errorDescription = 'The operation was canceled by the user.';
1199 | } else if (err.toUpperCase() === 'CHANNEL_ERROR') {
1200 | errorCode = chrome.cast.ErrorCode.CHANNEL_ERROR;
1201 | errorDescription = 'A channel to the receiver is not available.';
1202 | } else if (err.toUpperCase() === 'SESSION_ERROR') {
1203 | errorCode = chrome.cast.ErrorCode.SESSION_ERROR;
1204 | errorDescription = 'A session could not be created, or a session was invalid.';
1205 | }
1206 |
1207 | var error = new Error(errorCode, errorDescription, errorData);
1208 | if (callback) {
1209 | callback(error);
1210 | }
1211 | }
1212 |
1213 |
1214 | execute('setup', function(err) {
1215 | if (!err) {
1216 | chrome.cast.isAvailable = true;
1217 | } else {
1218 | throw new Error('Unable to setup chrome.cast API' + err);
1219 | }
1220 | });
--------------------------------------------------------------------------------