diff --git a/README.md b/README.md index 776105b..b94a014 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,64 @@

cordova-plugin-chromecast

Control Chromecast from your Cordova app

+--- + +### NOTICE: This isn't really actively mainted, if you would like be the maintainer of **cordova-plugin-chromecast**, please fork and submit a PR to change this notice to point to your fork! + +--- + # Installation ``` cordova plugin add https://github.com/jellyfin/cordova-plugin-chromecast.git ``` +If you have trouble installing the plugin or running the project for iOS, from `/platforms/ios/` try running: +```bash +sudo gem install cocoapods +pod repo update +pod install +``` + ### Additional iOS Installation Instructions To **distribute** an iOS app with this plugin you must add usage descriptions to your project's `config.xml`. -These strings will be used when asking the user for permission to use the microphone and bluetooth. +The "*Description" key strings will be used when asking the user for permission to use the microphone/bluetooth/local network. ```xml - - + + Bluetooth is required to scan for nearby Chromecast devices with guest mode enabled. - + Bluetooth is required to scan for nearby Chromecast devices with guest mode enabled. - + The microphone is required to pair with nearby Chromecast devices with guest mode enabled. + + + The local network permission is required to discover Cast-enabled devices on your WiFi network. + + + + _googlecast._tcp + + _CC1AD845._googlecast._tcp + + + + ``` +## Chromecast Icon Assets +[chromecast-assets.zip](https://github.com/jellyfin/cordova-plugin-chromecast/wiki/chromecast-assets.zip) + # Supports -**Android** 4.4+ (7.x highest confirmed) (may support lower, untested) -**iOS** 9.0+ (13.2.1 highest confirmed) +**Android** 4.4+ (may support lower, untested) +**iOS** 10.0+ (The [Google Cast iOS Sender SDK 4.5.0](https://developers.google.com/cast/docs/release-notes#september-14,-2020) says iOS 10+ but all tests on the plugin work fine for iOS 9.3.5, so it appears to work on iOs 9 anyways. :/) ## Quirks * Android 4.4 (maybe 5.x and 6.x) are not able automatically rejoin/resume a chromecast session after an app restart. @@ -46,68 +75,78 @@ The most significant usage difference between the [cast API](https://developers. In **Chrome desktop** you would do: ```js window['__onGCastApiAvailable'] = function(isAvailable, err) { - if (isAvailable) { - // start using the api! - } + if (isAvailable) { + // start using the api! + } }; ``` But in **cordova-plugin-chromecast** you do: ```js document.addEventListener("deviceready", function () { - // start using the api! + // start using the api! }); ``` -### Example +### Example Usage Here is a simple [example](doc/example.js) that loads a video, pauses it, and ends the session. +If you want more detailed code examples, please ctrl+f for the function of interest in [tests_auto.js](tests/www/js/tests_auto.js). +The other test files may contain code examples of interest as well: [[tests_manual_primary_1.js](tests/www/js/tests_manual_primary_1.js), [tests_manual_primary_2.js](tests/www/js/tests_manual_primary_2.js), [tests_manual_secondary.js](tests/www/js/tests_manual_secondary.js)] + ## API -Here are the support [Chromecast API]((https://developers.google.com/cast/docs/reference/chrome#chrome.cast)) methods. Any object types required by any of these methods are also supported. (eg. chrome.cast.ApiConfig) - -[chrome.cast.initialize](https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.initialize) -[chrome.cast.requestSession](https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.requestSession) -[chrome.cast.setCustomReceivers](https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.setCustomReceivers) -[chrome.cast.Session.setReceiverVolumeLevel](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#setReceiverVolumeLevel) -[chrome.cast.Session.setReceiverMuted](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#setReceiverMuted) -[chrome.cast.Session.stop](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#stop) -[chrome.cast.Session.leave](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#leave) -[chrome.cast.Session.sendMessage](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#sendMessage) -[chrome.cast.Session.loadMedia](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#loadMedia) -[chrome.cast.Session.queueLoad](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#queueLoad) -[chrome.cast.Session.addUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#addUpdateListener) -[chrome.cast.Session.removeUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#removeUpdateListener) -[chrome.cast.Session.addMessageListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#addMessageListener) -[chrome.cast.Session.removeMessageListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#removeMessageListener) -[chrome.cast.Session.addMediaListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#addMediaListener) -[chrome.cast.Session.removeMediaListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#removeMediaListener) -[chrome.cast.media.Media.play](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#play) -[chrome.cast.media.Media.pause](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#pause) -[chrome.cast.media.Media.seek](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#seek) -[chrome.cast.media.Media.stop](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#stop) -[chrome.cast.media.Media.setVolume](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#setVolume) -[chrome.cast.media.Media.supportsCommand](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#supportsCommand) -[chrome.cast.media.Media.getEstimatedTime](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#getEstimatedTime) -[chrome.cast.media.Media.editTracksInfo](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#editTracksInfo) -[chrome.cast.media.Media.queueJumpToItem](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#queueJumpToItem) -[chrome.cast.media.Media.addUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#addUpdateListener) -[chrome.cast.media.Media.removeUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#removeUpdateListener) +Here are the supported [Chromecast API]((https://developers.google.com/cast/docs/reference/chrome#chrome.cast)) methods. Any object types required by any of these methods are also supported. (eg. [chrome.cast.ApiConfig](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ApiConfig)). You can search [chrome.cast.js](www/chrome.cast.js) to check if an API is supported. + +* [chrome.cast.initialize](https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.initialize) +* [chrome.cast.requestSession](https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.requestSession) + +[chrome.cast.Session](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session) +Most *Properties* Supported. +Supported *Methods*: +* [setReceiverVolumeLevel](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#setReceiverVolumeLevel) +* [setReceiverMuted](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#setReceiverMuted) +* [stop](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#stop) +* [leave](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#leave) +* [sendMessage](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#sendMessage) +* [loadMedia](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#loadMedia) +* [queueLoad](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#queueLoad) +* [addUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#addUpdateListener) +* [removeUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#removeUpdateListener) +* [addMessageListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#addMessageListener) +* [removeMessageListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#removeMessageListener) +* [addMediaListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#addMediaListener) +* [removeMediaListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#removeMediaListener) + +[chrome.cast.media.Media](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media) +Most *Properties* Supported. +Supported *Methods*: +* [play](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#play) +* [pause](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#pause) +* [seek](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#seek) +* [stop](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#stop) +* [setVolume](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#setVolume) +* [supportsCommand](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#supportsCommand) +* [getEstimatedTime](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#getEstimatedTime) +* [editTracksInfo](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#editTracksInfo) +* [queueJumpToItem](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#queueJumpToItem) +* [addUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#addUpdateListener) +* [removeUpdateListener](https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media.html#removeUpdateListener) ### Specific to this plugin -We have added some additional methods unique to this plugin. +We have added some additional methods that are unique to this plugin (that *do not* exist in the chrome cast API). They can all be found in the `chrome.cast.cordova` object. To make your own **custom route selector** use this: ```js // This will begin an active scan for routes chrome.cast.cordova.scanForRoutes(function (routes) { - // Here is where you should update your route selector view with the current routes - // This will called each time the routes change - // routes is an array of "Route" objects (see below) + // Here is where you should update your route selector view with the current routes + // This will called each time the routes change + // routes is an array of "Route" objects (see below) }, function (err) { - // Will return with err.code === chrome.cast.ErrorCode.CANCEL when the scan has been ended + // Will return with err.code === chrome.cast.ErrorCode.CANCEL when the scan has been ended }); // When the user selects a route @@ -116,9 +155,9 @@ chrome.cast.cordova.stopScan(); // and use the selected route.id to join the route chrome.cast.cordova.selectRoute(route.id, function (session) { - // Save the session for your use + // Save the session for your use }, function (err) { - // Failed to connect to the route + // Failed to connect to the route }); ``` @@ -162,7 +201,15 @@ Run `npm test` to ensure your code fits the styling. It will also find some err * If errors are found, you can try running `npm run style`, this will attempt to automatically fix the errors. +You can view what the plug tests should look like here: +* [Auto Tests - Desktop Chrome](https://youtu.be/CdUwFrEht_A) +* [Auto Tests - Android or iOS](https://youtu.be/VUtiXee6m_8) +* [Manual Tests - Android or iOS](https://youtu.be/cgyOpBRXdEI) +* [Interaction Tests - Android & iOS](https://youtu.be/rphp_s5ruzM) +* [Interaction Tests - Android (or iOS) & Desktop Chrome](https://youtu.be/1ccBHqeMLhs) + ### Tests Mobile + Requirements: * A chromecast device @@ -179,7 +226,8 @@ Manual tests: * Interaction between 2 devices connected to the same session * You will need to be able to run the tests from 2 different devices (preferred) or between a device and chrome desktop browser * To use the chrome desktop browser see [Tests Chrome](#tests-chrome) - + * [What a successful manual run looks like](https://github.com/jellyfin/cordova-plugin-chromecast/wiki/img/manual-tests-success.jpg) + [Why we chose a non-standard test framework](https://github.com/jellyfin/cordova-plugin-chromecast/issues/50) ### Tests Chrome @@ -189,7 +237,7 @@ They use the google provided cast_sender.js. These are particularly useful for ensuring we are following the [official Google Cast API for Chrome](https://developers.google.com/cast/docs/reference/chrome#chrome.cast) correctly. To run the tests: * run: `npm run host-chrome-tests [port default=8432]` -* Navigate to: `http://localhost:8432/chrome/tests_chrome.html` +* Navigate to: [http://localhost:8432/html/tests.html](http://localhost:8432/html/tests.html) ## Contributing diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 0000000..b0c973a --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,47 @@ + +## Release Notes for cordova-plugin-chromecast + +### 2.0.1 (2020-11-28) + +* (ios) Bug Fix - media loaded without any metadata caused crash + +### 2.0.0 (2020-11-07) + +* (ios) BREAKING - Update Google Cast SDK (iOS Sender -> 4.5.2) + * Google Cast SDK - [iOS sender 4.5.0+](https://developers.google.com/cast/docs/release-notes#september-14,-2020) has minimum iOS 10 + * But, all tests on the plugin work fine for iOS 9.3.5, so it appears to work on iOS 9 anyways. :/ + * But, since cordova@6.x.x no longer supports iOS 9+10 we will only be testing on iOS 11+. + * With the update, additional entries are required in `config.xml` for cast to work on iOs 14 (if built with Xcode 12+) (see README.md) + +### 1.1.0 (2020-11-1) + +* Update Google Cast SDKs (iOS -> 4.4.8, android -> 19.0.0) + * New SDK supports casting to Android TV (untested) +* (android) simulate mediaSessionId +* Add Audiobook chapter metadata +* (android) Fix queue bug: media returned with no items +* (android) [Issue #73] Fix Push Notification stop casting button +* [Live stream issue](https://github.com/miloproductionsinc/cordova-plugin-chromecast/issues/11) Fix for live stream media + +### 1.0.0 (2020-01-24) + +* For full list of changes, see PR #54 diff --git a/doc/example.js b/doc/example.js index 3b9a37e..49cfd24 100644 --- a/doc/example.js +++ b/doc/example.js @@ -1,6 +1,8 @@ -document.addEventListener("deviceready", function () { +document.addEventListener('deviceready', function () { // Must wait for deviceready before using chromecast + var chrome = window.chrome; + // File globals var _session; var _media; @@ -14,14 +16,14 @@ document.addEventListener("deviceready", function () { // The session listener is only called under the following conditions: // * will be called shortly chrome.cast.initialize is run // * if the device is already connected to a cast session - // Basically, this is what allows you to re-use the same cast session + // Basically, this is what allows you to re-use the same cast session // across different pages and after app restarts - }, function receiverListener (receiverAvailable) { + }, function receiverListener (receiverAvailable) { // receiverAvailable is a boolean. // True = at least one chromecast device is available // False = No chromecast devices available // You can use this to determine if you want to show your chromecast icon - }); + }); // initialize chromecast, this must be done before using other chromecast features chrome.cast.initialize(apiConfig, function () { @@ -30,22 +32,23 @@ document.addEventListener("deviceready", function () { requestSession(); }, function (err) { // Initialize failure + console.log(err); }); } - function requestSession () { - // This will open a native dialog that will let + // This will open a native dialog that will let // the user choose a chromecast to connect to // (Or will let you disconnect if you are already connected) chrome.cast.requestSession(function (session) { // Got a session! _session = session; - // Load a video + // Load a video loadMedia(); }, function (err) { // Failed, or if err is cancel, the dialog closed + console.log(err); }); } @@ -66,21 +69,23 @@ document.addEventListener("deviceready", function () { }, function (err) { // Failed (check that the video works in your browser) + console.log(err); }); } function pauseMedia () { _media.pause({}, function () { // Success - + // Wait a couple seconds setTimeout(function () { // stop the session stopSession(); - }, 2000) + }, 2000); }, function (err) { // Fail + console.log(err); }); } @@ -90,7 +95,8 @@ document.addEventListener("deviceready", function () { // Success }, function (err) { // Fail + console.log(err); }); } -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index c272b70..fe5baa1 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "cordova-plugin-chromecast", - "version": "1.0.0", + "version": "2.0.1", "scripts": { "host-chrome-tests": "node tests/www/chrome/host-tests.js", - "style-fix-js": "node node_modules/eslint/bin/eslint --fix src && node node_modules/eslint/bin/eslint --fix www && node node_modules/eslint/bin/eslint --ignore-pattern tests/www/lib --fix tests/www", - "test": "node node_modules/eslint/bin/eslint src && node node_modules/eslint/bin/eslint www && node node_modules/eslint/bin/eslint --ignore-pattern tests/www/lib tests/www && node ./node_modules/java-checkstyle/bin/index.js ./src/android/ -c ./check_style.xml", - "style": "npm run style-fix-js && npm run test" + "style-js": "npx eslint --ignore-pattern tests/www/vendor www tests/www doc/example.js", + "style-js-fix": "npx eslint --ignore-pattern tests/www/vendor/ --fix www/ tests/www/ doc/example.js", + "style-java": "npx java-checkstyle -c ./check_style.xml ./src/android/", + "style": "npm run style-js-fix && npm run style-java", + "test": "npm run style-js && npm run style-java" }, "author": "", "license": "dual GPLv3/MPLv2", @@ -20,7 +22,7 @@ "eslint-plugin-promise": "~3.5.0", "eslint-plugin-standard": "~3.0.1", "express": "^4.17.1", - "java-checkstyle": "0.0.1", + "java-checkstyle": "0.1.0", "path": "^0.12.7" } } diff --git a/plugin.xml b/plugin.xml index 6fe7a68..fe27132 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="2.0.1"> @@ -34,10 +34,7 @@ - - - - + @@ -56,20 +53,13 @@ - + - + - - - - - - - diff --git a/src/android/ChromecastConnection.java b/src/android/ChromecastConnection.java index 42d939a..a475ecd 100644 --- a/src/android/ChromecastConnection.java +++ b/src/android/ChromecastConnection.java @@ -34,11 +34,15 @@ public class ChromecastConnection { private Activity activity; /** settings object. */ private SharedPreferences settings; - /** Controls the media. */ - private ChromecastSession media; + /** Controls the chromecastSession. */ + private ChromecastSession chromecastSession; /** Lifetime variable. */ private SessionListener newConnectionListener; + /** Indicates whether we left the session or stopped it. */ + private boolean sessionEndBecauseOfLeave = false; + /** Any callback to call after sessionEnd. */ + private CallbackContext sessionEndCallback = null; /** The Listener callback. */ private Listener listener; @@ -55,7 +59,7 @@ public class ChromecastConnection { this.settings = activity.getSharedPreferences("CORDOVA-PLUGIN-CHROMECAST_ChromecastConnection", 0); this.appId = settings.getString("appId", CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID); this.listener = connectionListener; - this.media = new ChromecastSession(activity, listener); + this.chromecastSession = new ChromecastSession(activity, listener); // Set the initial appId CastOptionsProvider.setAppId(appId); @@ -64,6 +68,19 @@ public class ChromecastConnection { // CastContext and prep it for searching for a session to rejoin // Also adds the receiver update callback getContext().addCastStateListener(listener); + getSessionManager().addSessionManagerListener(new SessionListener() { + @Override + public void onSessionEnded(CastSession castSession, int errCode) { + chromecastSession.setSession(null); + if (sessionEndCallback != null) { + sessionEndCallback.success(); + } + listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, sessionEndBecauseOfLeave ? "disconnected" : "stopped")); + // Reset + sessionEndBecauseOfLeave = false; + sessionEndCallback = null; + } + }, CastSession.class); } /** @@ -71,7 +88,7 @@ public class ChromecastConnection { * @return the ChromecastSession object */ ChromecastSession getChromecastSession() { - return this.media; + return chromecastSession; } /** @@ -114,7 +131,7 @@ void onRouteUpdate(List routes) { // If we do have a session if (session != null) { // Let the client know - media.setSession(session); + chromecastSession.setSession(session); listener.onSessionRejoin(ChromecastUtilities.createSessionObject(session)); } } @@ -199,7 +216,7 @@ public void run() { // getMediaRouter().getRoutes() which will result in "Ignoring attempt to select // removed route: ", even if that route *should* be available. This state could // happen because routes are periodically "removed" and "added", and if the last - // time media router was scanning ended when the route was temporarily removed the + // time chromecastSession router was scanning ended when the route was temporarily removed the // getRoutes() fn will have no record of the route. We need the active scan to // avoid this situation as well. PS. Just running the scan non-stop is a poor idea // since it will drain battery power quickly. @@ -372,7 +389,7 @@ private void listenForConnection(ConnectionCallback callback) { @Override public void onSessionStarted(CastSession castSession, String sessionId) { getSessionManager().removeSessionManagerListener(this, CastSession.class); - media.setSession(castSession); + chromecastSession.setSession(castSession); callback.onJoin(ChromecastUtilities.createSessionObject(castSession)); } @Override @@ -472,18 +489,8 @@ public void run() { void endSession(boolean stopCasting, CallbackContext callback) { activity.runOnUiThread(new Runnable() { public void run() { - getSessionManager().addSessionManagerListener(new SessionListener() { - @Override - public void onSessionEnded(CastSession castSession, int error) { - getSessionManager().removeSessionManagerListener(this, CastSession.class); - media.setSession(null); - if (callback != null) { - callback.success(); - } - listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, stopCasting ? "stopped" : "disconnected")); - } - }, CastSession.class); - + sessionEndCallback = callback; + sessionEndBecauseOfLeave = !stopCasting; getSessionManager().endCurrentSession(stopCasting); } }); diff --git a/src/android/ChromecastSession.java b/src/android/ChromecastSession.java index cdefe04..99a1c48 100644 --- a/src/android/ChromecastSession.java +++ b/src/android/ChromecastSession.java @@ -42,7 +42,12 @@ public class ChromecastSession { private boolean requestingMedia = false; /** Handles and used to trigger queue updates. **/ private MediaQueueController mediaQueueCallback; - /** Stores a callback that should be called when the queue is loaded. **/ + /** + * Stores a callback that should be called when the queue is loaded. + * See https://github.com/jellyfin/cordova-plugin-chromecast/wiki/img/queueReloadCallback.jpg + * For how queueReloadCallback is used with multiple devices connected to the same session, and + * the primary device loads the media. + **/ private Runnable queueReloadCallback; /** Stores a callback that should be called when the queue status is updated. **/ private Runnable queueStatusUpdatedCallback; @@ -436,6 +441,19 @@ private void setQueueReloadCallback(Runnable callback) { this.queueReloadCallback = callback; } + /** + * This is called only when new media has been loaded. + * Media has been loaded via loadMedia or queueLoad by this sender or an external sender. + */ + private void runQueueReloadCallback() { + if (this.queueReloadCallback != null) { + // TODO incrementMediaSessionId is a hack to simulate changing mediaSessionId + // (for some reason this is available on iOS and desktop chrome, but not Android.) + ChromecastUtilities.incrementMediaSessionId(); + this.queueReloadCallback.run(); + } + } + private void setQueueStatusUpdatedCallback(Runnable callback) { this.queueStatusUpdatedCallback = callback; } @@ -474,13 +492,24 @@ void refreshQueueItems() { // Reset lookingForIndexes lookingForIndexes = new ArrayList<>(); - // Only add indexes to look for it the currentItemIndex is valid - if (index != -1) { - // init i-1, i, i+1 (exclude items out of range), so always 2-3 items - for (int i = index - 1; i <= index + 1; i++) { - if (i >= 0 && i < len) { - lookingForIndexes.add(i); + // If we don't know the currentItemIndex, retry on queueStatusUpdated + // To be careful, only when we are expecting a queueRelodCallback and + // queueStatusUpdatedCallback is not already in use. + // (2nd+3rd conditions may be unnecessary) + if (index == -1 && queueReloadCallback != null && queueStatusUpdatedCallback == null) { + setQueueStatusUpdatedCallback(new Runnable() { + @Override + public void run() { + refreshQueueItems(); } + }); + return; + } + // Else, we can get the 2-3 items that are around the currentItem index! + // init i-1, i, i+1 (exclude items out of range), so always 2-3 items + for (int i = index - 1; i <= index + 1; i++) { + if (i >= 0 && i < len) { + lookingForIndexes.add(i); } } checkLookingForIndexes(); @@ -518,7 +547,7 @@ private void updateFinished() { // Update the queueItems ChromecastUtilities.setQueueItems(queueItems); if (queueReloadCallback != null && queue.getItemCount() > 0) { - queueReloadCallback.run(); + runQueueReloadCallback(); setQueueReloadCallback(null); } clientListener.onMediaUpdate(createMediaObject()); diff --git a/src/android/ChromecastUtilities.java b/src/android/ChromecastUtilities.java index 7039db0..17ea3fa 100644 --- a/src/android/ChromecastUtilities.java +++ b/src/android/ChromecastUtilities.java @@ -30,6 +30,8 @@ final class ChromecastUtilities { /** Stores a cache of the queueItems for building Media Objects. */ private static JSONArray queueItems = null; + /** We have to make up our own mediaSessionId since Android does not give us access to it. */ + private static int mediaSessionId = 0; private ChromecastUtilities() { //not called @@ -44,6 +46,14 @@ static void setQueueItems(JSONArray items) { queueItems = items; } + /** + * Should be called whenever new media/queue is detected. + * Aka: When media is loaded via loadMedia or queueLoad by this sender or an external sender. + */ + static void incrementMediaSessionId() { + mediaSessionId++; + } + static String getMediaIdleReason(int idleReason) { switch (idleReason) { case MediaStatus.IDLE_REASON_CANCELED: @@ -226,8 +236,6 @@ static String getAndroidMetadataName(String clientName) { return MediaMetadata.KEY_ARTIST; case "bookTitle": return MediaMetadata.KEY_BOOK_TITLE; - case "broadcastDate": - return MediaMetadata.KEY_BROADCAST_DATE; case "chapterNumber": return MediaMetadata.KEY_CHAPTER_NUMBER; case "chapterTitle": @@ -249,10 +257,11 @@ static String getAndroidMetadataName(String clientName) { return MediaMetadata.KEY_LOCATION_LONGITUDE; case "locationName": return MediaMetadata.KEY_LOCATION_NAME; + case "originalAirDate": + return MediaMetadata.KEY_BROADCAST_DATE; case "queueItemId": return MediaMetadata.KEY_QUEUE_ITEM_ID; case "releaseDate": - case "originalAirDate": return MediaMetadata.KEY_RELEASE_DATE; case "season": return MediaMetadata.KEY_SEASON_NUMBER; @@ -292,7 +301,7 @@ static String getClientMetadataName(String androidName) { case MediaMetadata.KEY_BOOK_TITLE: return "bookTitle"; case MediaMetadata.KEY_BROADCAST_DATE: - return "broadcastDate"; + return "originalAirDate"; case MediaMetadata.KEY_CHAPTER_NUMBER: return "chapterNumber"; case MediaMetadata.KEY_CHAPTER_TITLE: @@ -350,7 +359,6 @@ static String getMetadataType(String androidName) { case MediaMetadata.KEY_ALBUM_TITLE: case MediaMetadata.KEY_ARTIST: case MediaMetadata.KEY_BOOK_TITLE: - case MediaMetadata.KEY_CHAPTER_NUMBER: case MediaMetadata.KEY_CHAPTER_TITLE: case MediaMetadata.KEY_COMPOSER: case MediaMetadata.KEY_LOCATION_NAME: @@ -359,6 +367,7 @@ static String getMetadataType(String androidName) { case MediaMetadata.KEY_SUBTITLE: case MediaMetadata.KEY_TITLE: return "string"; // 1 in MediaMetadata + case MediaMetadata.KEY_CHAPTER_NUMBER: case MediaMetadata.KEY_DISC_NUMBER: case MediaMetadata.KEY_EPISODE_NUMBER: case MediaMetadata.KEY_HEIGHT: @@ -498,7 +507,7 @@ static JSONObject createMediaObject(CastSession session, JSONArray items) { MediaStatus mediaStatus = session.getRemoteMediaClient().getMediaStatus(); // TODO: Missing attributes are commented out. - // These are returned by the chromecast desktop SDK, we should probbaly return them too + // These are returned by the chromecast desktop SDK, we should probably return them too //out.put("breakStatus",); out.put("currentItemId", mediaStatus.getCurrentItemId()); out.put("currentTime", mediaStatus.getStreamPosition() / 1000.0); @@ -513,7 +522,7 @@ static JSONObject createMediaObject(CastSession session, JSONArray items) { //out.put("liveSeekableRange",); out.put("loadingItemId", mediaStatus.getLoadingItemId()); out.put("media", createMediaInfoObject(session.getRemoteMediaClient().getMediaInfo())); - out.put("mediaSessionId", 1); + out.put("mediaSessionId", mediaSessionId); out.put("playbackRate", mediaStatus.getPlaybackRate()); out.put("playerState", ChromecastUtilities.getMediaPlayerState(mediaStatus.getPlayerState())); out.put("preloadedItemId", mediaStatus.getPreloadedItemId()); @@ -639,7 +648,12 @@ private static JSONObject createMediaInfoObject(MediaInfo mediaInfo) { out.put("contentId", mediaInfo.getContentId()); out.put("contentType", mediaInfo.getContentType()); out.put("customData", mediaInfo.getCustomData()); - out.put("duration", mediaInfo.getStreamDuration() / 1000.0); + long duration = mediaInfo.getStreamDuration(); + if (duration == -1) { + out.put("duration", null); + } else { + out.put("duration", duration / 1000.0); + } //out.put("mediaCategory",); out.put("metadata", createMetadataObject(mediaInfo.getMetadata())); out.put("streamType", ChromecastUtilities.getMediaInfoStreamType(mediaInfo)); @@ -654,10 +668,14 @@ private static JSONObject createMediaInfoObject(MediaInfo mediaInfo) { } static JSONObject createMetadataObject(MediaMetadata metadata) { - JSONObject out = new JSONObject(); if (metadata == null) { - return out; + return null; + } + Set keys = metadata.keySet(); + if (keys.size() == 0) { + return null; } + JSONObject out = new JSONObject(); try { try { // Must be in own try catch @@ -667,7 +685,6 @@ static JSONObject createMetadataObject(MediaMetadata metadata) { out.put("metadataType", metadata.getMediaType()); out.put("type", metadata.getMediaType()); - Set keys = metadata.keySet(); String outKey; // First translate and add the Android specific keys for (String key : keys) { @@ -772,7 +789,7 @@ static JSONObject createError(String code, String message) { return out; } -/* ------------------- Create NON-JSON (non-output) Objects ---------------------------------- */ + /* ------------------- Create NON-JSON (non-output) Objects ---------------------------------- */ /** * Creates a MediaQueueItem from a JSONObject representation of a MediaQueueItem. diff --git a/src/ios/MLPCastUtilities.m b/src/ios/MLPCastUtilities.m index 8df3abc..831423d 100644 --- a/src/ios/MLPCastUtilities.m +++ b/src/ios/MLPCastUtilities.m @@ -118,6 +118,9 @@ + (GCKMediaTextTrackStyle *)buildTextTrackStyle:(NSDictionary *)data { } +(GCKMediaMetadata*)buildMediaMetadata:(NSDictionary*)data { + if ([data isEqual:[NSNull null]] || data == nil) { + return nil; + } GCKMediaMetadata* mediaMetaData = [[GCKMediaMetadata alloc] initWithMetadataType:GCKMediaMetadataTypeGeneric]; if (data[@"metadataType"]) { @@ -195,7 +198,7 @@ +(NSString*)getMetadataType:(NSString*)iOSName { return @"date"; } if ([iOSName isEqualToString:kGCKMetadataKeyChapterNumber]) { - return @"string"; + return @"int"; } if ([iOSName isEqualToString:kGCKMetadataKeyChapterTitle]) { return @"string"; @@ -278,9 +281,6 @@ +(NSString*)getiOSMetadataName:(NSString*)clientName { if ([clientName isEqualToString:@"bookTitle"]) { return kGCKMetadataKeyBookTitle; } - if ([clientName isEqualToString:@"broadcastDate"]) { - return kGCKMetadataKeyBroadcastDate; - } if ([clientName isEqualToString:@"chapterNumber"]) { return kGCKMetadataKeyChapterNumber; } @@ -321,7 +321,7 @@ +(NSString*)getiOSMetadataName:(NSString*)clientName { return kGCKMetadataKeyReleaseDate; } if ([clientName isEqualToString:@"originalAirDate"]) { - return kGCKMetadataKeyReleaseDate; + return kGCKMetadataKeyBroadcastDate; } if ([clientName isEqualToString:@"season"]) { return kGCKMetadataKeySeasonNumber; @@ -373,7 +373,7 @@ +(NSString*)getClientMetadataName:(NSString*)iOSName { return @"bookTitle"; } if ([iOSName isEqualToString:kGCKMetadataKeyBroadcastDate]) { - return @"broadcastDate"; + return @"originalAirDate"; } if ([iOSName isEqualToString:kGCKMetadataKeyChapterNumber]) { return @"chapterNumber"; @@ -588,7 +588,7 @@ + (NSDictionary *)createMediaInfoObject:(GCKMediaInformation *)mediaInfo { returnDict[@"contentId"] = mediaInfo.contentID? mediaInfo.contentID : mediaInfo.contentURL.absoluteString; returnDict[@"contentType"] = mediaInfo.contentType; returnDict[@"customData"] = mediaInfo.customData == nil ? @{} : mediaInfo.customData; - returnDict[@"duration"] = @(mediaInfo.streamDuration); + returnDict[@"duration"] = mediaInfo.streamDuration == INFINITY ? nil : @(mediaInfo.streamDuration); returnDict[@"metadata" ] = [MLPCastUtilities createMetadataObject:mediaInfo.metadata]; returnDict[@"streamType"] = [MLPCastUtilities getStreamType:mediaInfo.streamType]; returnDict[@"tracks"] = [MLPCastUtilities getMediaTracks:mediaInfo.mediaTracks]; @@ -597,16 +597,18 @@ + (NSDictionary *)createMediaInfoObject:(GCKMediaInformation *)mediaInfo { } + (NSDictionary*)createMetadataObject:(GCKMediaMetadata*)metadata { - - NSMutableDictionary* outputDict = [NSMutableDictionary new]; if (!metadata) { - return [NSDictionary dictionaryWithDictionary:outputDict]; + return nil; + } + NSArray* keys = metadata.allKeys; + if ([keys count] == 0) { + return nil; } + NSMutableDictionary* outputDict = [NSMutableDictionary new]; outputDict[@"images"] = [MLPCastUtilities createImagesArray:metadata.images]; outputDict[@"metadataType"] = @(metadata.metadataType); outputDict[@"type"] = @(metadata.metadataType); - NSArray* keys = metadata.allKeys; for (NSString* key in keys) { NSString* outKey = [MLPCastUtilities getClientMetadataName:key]; if ([outKey isEqualToString:key] || [outKey isEqualToString:@"type"]) { diff --git a/src/ios/MLPChromecastSession.m b/src/ios/MLPChromecastSession.m index 1cc71f1..8aa0799 100644 --- a/src/ios/MLPChromecastSession.m +++ b/src/ios/MLPChromecastSession.m @@ -214,21 +214,24 @@ - (void)createMessageChannelWithCommand:(CDVInvokedUrlCommand*)command namespace [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } -- (void)sendMessageWithCommand:(CDVInvokedUrlCommand*)command namespace:(NSString*)namespace message:(NSString*)message { - GCKGenericChannel* channel = self.genericChannels[namespace]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[NSString stringWithFormat:@"Namespace %@ not founded",namespace]]; +- (void)sendMessageWithCommand:(CDVInvokedUrlCommand*)command namespace:(NSString*)namespace message:(NSString*)message{ + + GCKGenericChannel* newChannel = [[GCKGenericChannel alloc] initWithNamespace:namespace]; + newChannel.delegate = self; + self.genericChannels[namespace] = newChannel; + [currentSession addChannel:newChannel]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[NSString stringWithFormat:@"Namespace %@ not found",namespace]]; - if (channel != nil) { + if(newChannel != nil) { GCKError* error = nil; - [channel sendTextMessage:message error:&error]; + [newChannel sendTextMessage:message error:&error]; if (error != nil) { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error.description]; } else { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; } } - - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } - (void)mediaSeekWithCommand:(CDVInvokedUrlCommand*)command position:(NSTimeInterval)position resumeState:(GCKMediaResumeState)resumeState { diff --git a/tests/www/chrome/cordova_stubs.js b/tests/www/chrome/cordova_stubs.js index 7f09260..1435cb3 100644 --- a/tests/www/chrome/cordova_stubs.js +++ b/tests/www/chrome/cordova_stubs.js @@ -1,5 +1,5 @@ /** - * These stub plugin specific bahaviour so we can run the auto tests on chrome + * These stub plugin specific behaviour so we can run the auto tests on chrome * desktop browser. */ (function () { @@ -110,24 +110,4 @@ successCallback(['SETUP']); }; -/* ------------------------- Start Tests ---------------------------------- */ - - // This actually starts the tests - window['__onGCastApiAvailable'] = function (isAvailable, err) { - // If error, it is probably because we are not on chrome, so just disregard - if (isAvailable) { - var runner; - if (window['cordova-plugin-chromecast-tests'].runMocha) { - runner = window['cordova-plugin-chromecast-tests'].runMocha(); - } else { - runner = mocha.run(); - } - // This makes it so that tests actually fail in the case of - // uncaught exceptions inside promise catch blocks - window.addEventListener('unhandledrejection', function (event) { - runner.fail(runner.test, event.reason); - }); - } - }; - }()); diff --git a/tests/www/chrome/tests_auto_chrome.html b/tests/www/chrome/tests_auto_chrome.html deleted file mode 100644 index 6e64b09..0000000 --- a/tests/www/chrome/tests_auto_chrome.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Cordova tests - - - - - - - - - - - - - -

Auto Tests

- - -
-

Action required:

-

Starting Tests...

- -
-
- - - - - diff --git a/tests/www/chrome/tests_chrome.html b/tests/www/chrome/tests_chrome.html deleted file mode 100644 index ce3d172..0000000 --- a/tests/www/chrome/tests_chrome.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - Cordova tests - - - - - - - - - - - -

cordova-plugin-chromecast Tests

-
-

Auto Tests should be run (and passing) before attempting the manual tests.

- - - -


- Manual Tests (Primary) Part 1 is the entry point for manual tests.
- You will require 2 devices or 1 device and a desktop chrome browser.
- (See readme for instructions on how to run tests from the desktop chrome browser.)

- Click Manual Tests (Primary) Part 1 and follow the directions carefully. -

- - - - -
- - diff --git a/tests/www/chrome/tests_manual_primary_1_chrome.html b/tests/www/chrome/tests_manual_primary_1_chrome.html deleted file mode 100644 index eeeea42..0000000 --- a/tests/www/chrome/tests_manual_primary_1_chrome.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - Cordova tests - - - - - - - - - - - - - -

Manual Tests (Primary Device) Part 1

- - - -
-

Action required:

-

Starting Tests...

- -
- - - -
- - - - - - diff --git a/tests/www/chrome/tests_manual_primary_2_chrome.html b/tests/www/chrome/tests_manual_primary_2_chrome.html deleted file mode 100644 index 32dced1..0000000 --- a/tests/www/chrome/tests_manual_primary_2_chrome.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - Cordova tests - - - - - - - - - - - - - -

Manual Tests (Primary Device) Part 2

- - - -
-

Action required:

-

Starting Tests...

- -
- - - -
- - - - - - diff --git a/tests/www/chrome/tests_manual_secondary_chrome.html b/tests/www/chrome/tests_manual_secondary_chrome.html deleted file mode 100644 index 63dc6ed..0000000 --- a/tests/www/chrome/tests_manual_secondary_chrome.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - Cordova tests - - - - - - - - - - - - - -

Manual Tests (Secondary Device)

- - -
-

Action required:

-

Preparing Secondary App...

- -
- - - -
- - - - - - diff --git a/tests/www/html/tests.html b/tests/www/html/tests.html index bfad128..e7a01b8 100644 --- a/tests/www/html/tests.html +++ b/tests/www/html/tests.html @@ -7,41 +7,41 @@ - +

cordova-plugin-chromecast Tests

-
-

Auto Tests should be run (and passing) before attempting the manual tests.

+
+

Auto Tests should be run (and passing) before attempting other tests.

- + + + -


- Manual Tests (Primary) Part 1 is the entry point for manual tests.
- You will require 2 devices or 1 device and a desktop chrome browser.
- (See readme for instructions on how to run tests from the desktop chrome browser.)

- Click Manual Tests (Primary) Part 1 and follow the directions carefully. -

- - -
diff --git a/tests/www/html/tests_auto.html b/tests/www/html/tests_auto.html index 3da9452..d9dbe6e 100644 --- a/tests/www/html/tests_auto.html +++ b/tests/www/html/tests_auto.html @@ -7,13 +7,13 @@ - + - - - - - + + + + +

Auto Tests

@@ -30,12 +30,14 @@

Auto Tests

+
+

Action required:

+

None.

+ +
+
- + diff --git a/tests/www/html/tests_manual_primary_1.html b/tests/www/html/tests_interaction_primary.html similarity index 53% rename from tests/www/html/tests_manual_primary_1.html rename to tests/www/html/tests_interaction_primary.html index 5edf6a7..293016a 100644 --- a/tests/www/html/tests_manual_primary_1.html +++ b/tests/www/html/tests_interaction_primary.html @@ -7,16 +7,16 @@ - + - - - - - + + + + + -

Manual Tests (Primary Device) Part 1

+

Interaction Tests - Primary Device

- - - + + diff --git a/tests/www/html/tests_manual_secondary.html b/tests/www/html/tests_interaction_secondary.html similarity index 54% rename from tests/www/html/tests_manual_secondary.html rename to tests/www/html/tests_interaction_secondary.html index 3708dd0..30f49fb 100644 --- a/tests/www/html/tests_manual_secondary.html +++ b/tests/www/html/tests_interaction_secondary.html @@ -7,23 +7,23 @@ - + - - - - - + + + + + -

Manual Tests (Secondary Device)

+

Interaction Tests - Secondary Device

- - - + + diff --git a/tests/www/html/tests_manual_primary_2.html b/tests/www/html/tests_manual.html similarity index 53% rename from tests/www/html/tests_manual_primary_2.html rename to tests/www/html/tests_manual.html index 5f694c4..f408fc1 100644 --- a/tests/www/html/tests_manual_primary_2.html +++ b/tests/www/html/tests_manual.html @@ -7,16 +7,16 @@ - + - - - - - + + + + + -

Manual Tests (Primary Device) Part 2

+

Manual Tests

- - - + + diff --git a/tests/www/js/tests_auto.js b/tests/www/js/tests_auto.js index 74ab8a0..2ade66e 100644 --- a/tests/www/js/tests_auto.js +++ b/tests/www/js/tests_auto.js @@ -28,12 +28,9 @@ var assert = window.chai.assert; var utils = window['cordova-plugin-chromecast-tests'].utils; + var mediaUtils = window['cordova-plugin-chromecast-tests'].mediaUtils; describe('cordova-plugin-chromecast', function () { - var imageUrl = 'https://ia800705.us.archive.org/1/items/GoodHousekeeping193810/Good%20Housekeeping%201938-10.jpg'; - var videoUrl = 'https://ia801302.us.archive.org/1/items/TheWater_201510/TheWater.mp4'; - var audioUrl = 'https://ia800306.us.archive.org/26/items/1939RadioNews/1939-10-24-CBS-Elmer-Davis-Reports-City-Of-Flint-Still-Missing.mp3'; - // callOrder constants that are re-used frequently var success = 'success'; var update = 'update'; @@ -73,7 +70,6 @@ assert.exists(chrome.cast.media); assert.exists(chrome.cast.initialize); assert.exists(chrome.cast.requestSession); - assert.exists(chrome.cast.setCustomReceivers); assert.exists(chrome.cast.Session); assert.exists(chrome.cast.media.PlayerState); assert.exists(chrome.cast.media.ResumeState); @@ -338,7 +334,14 @@ var called = utils.callOrder([ { id: success, repeats: false }, { id: update, repeats: true } - ], done); + ], function () { + // TODO chrome desktop bug 2020-10-25 - recheck later + // Need to give desktop chrome cast some time to fully disconnect, otherwise + // the next test fails because it receives a session on initialize + setTimeout(function () { + done(); + }, 1000); + }); session.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); if (session.status === chrome.cast.SessionStatus.DISCONNECTED) { @@ -355,7 +358,7 @@ it('initialize should not receive a session after session.leave', function (done) { var apiConfig = new chrome.cast.ApiConfig(new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), function sessionListener (session) { assert.fail('should not receive a session (we did sessionLeave so we shouldnt be able to auto rejoin rejoin)'); - }); + }, function receiverListener () {}); chrome.cast.initialize(apiConfig, function () { done(); }, function (err) { @@ -504,7 +507,7 @@ it('initialize should not receive a session after session.stop', function (done) { var apiConfig = new chrome.cast.ApiConfig(new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), function sessionListener (session) { assert.fail('should not receive a session (we did sessionStop so we shouldnt be able to auto rejoin rejoin)'); - }); + }, function receiverListener () {}); chrome.cast.initialize(apiConfig, function () { done(); }, function (err) { @@ -536,7 +539,18 @@ var mediaListener = function (media) { assert.fail('session.addMediaListener should only be called when an external sender loads media'); }; - before(function (done) { + var mediaInfo; + before('Create mediaInfo with custom metadata fields', function () { + mediaInfo = mediaUtils.getMediaInfoItem('VIDEO', chrome.cast.media.MetadataType.GENERIC); + mediaInfo.metadata.someTrueBoolean = true; + mediaInfo.metadata.someFalseBoolean = false; + mediaInfo.metadata.someSmallNumber = 15; + mediaInfo.metadata.someLargeNumber = 1234567890123456; + mediaInfo.metadata.someSmallDecimal = 15.15; + mediaInfo.metadata.someLargeDecimal = 1234567.123456789; + mediaInfo.metadata.someString = 'SomeString'; + }); + before('Get session', function (done) { // need to have a valid session to run these tests session = null; var scanState = 'running'; @@ -586,26 +600,11 @@ }); describe('Media (non-queues)', function () { it('session.loadMedia should be able to load a remote video and handle GenericMediaMetadata', function (done) { - var mediaInfo = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.subtitle = 'DaSubtitle'; - mediaInfo.metadata.releaseDate = new Date().valueOf(); - mediaInfo.metadata.someTrueBoolean = true; - mediaInfo.metadata.someFalseBoolean = false; - mediaInfo.metadata.someSmallNumber = 15; - mediaInfo.metadata.someLargeNumber = 1234567890123456; - mediaInfo.metadata.someSmallDecimal = 15.15; - mediaInfo.metadata.someLargeDecimal = 1234567.123456789; - mediaInfo.metadata.someString = 'SomeString'; - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { media = m; utils.testMediaProperties(media); assert.isUndefined(media.queueData); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.releaseDate, mediaInfo.metadata.releaseDate); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); // TODO figure out how to maintain the data types for custom params on the native side // so that we don't have to do turn each actual and expected into a string assert.equal(media.media.metadata.someTrueBoolean + '', mediaInfo.metadata.someTrueBoolean + ''); @@ -615,13 +614,11 @@ assert.equal(media.media.metadata.someSmallDecimal + '', mediaInfo.metadata.someSmallDecimal + ''); assert.equal(media.media.metadata.someLargeDecimal + '', mediaInfo.metadata.someLargeDecimal + ''); assert.equal(media.media.metadata.someString, mediaInfo.metadata.someString); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.GENERIC); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.GENERIC); assert.notExists(media.idleReason); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -652,6 +649,7 @@ media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.instanceOf(media.volume, chrome.cast.Volume); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.volume.level === vol) { media.removeUpdateListener(listener); called(update); @@ -661,6 +659,7 @@ media.setVolume(request, function () { assert.instanceOf(media.volume, chrome.cast.Volume); assert.equal(media.volume.level, vol); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -678,6 +677,7 @@ media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.instanceOf(media.volume, chrome.cast.Volume); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.volume.muted === muted) { media.removeUpdateListener(listener); called(update); @@ -687,6 +687,7 @@ media.setVolume(request, function () { assert.instanceOf(media.volume, chrome.cast.Volume); assert.equal(media.volume.muted, muted); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -711,6 +712,7 @@ media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.instanceOf(media.volume, chrome.cast.Volume); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.volume.level === vol && media.volume.muted === request.volume.muted) { media.removeUpdateListener(listener); @@ -722,6 +724,7 @@ assert.instanceOf(media.volume, chrome.cast.Volume); assert.equal(media.volume.level, vol); assert.equal(media.volume.muted, muted); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -735,6 +738,7 @@ media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.notEqual(media.playerState, chrome.cast.media.PlayerState.IDLE); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.playerState === chrome.cast.media.PlayerState.PAUSED) { media.removeUpdateListener(listener); called(update); @@ -742,6 +746,7 @@ }); media.pause(null, function () { assert.equal(media.playerState, chrome.cast.media.PlayerState.PAUSED); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -755,6 +760,7 @@ media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.notEqual(media.playerState, chrome.cast.media.PlayerState.IDLE); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.playerState === chrome.cast.media.PlayerState.PLAYING) { media.removeUpdateListener(listener); called(update); @@ -764,6 +770,7 @@ assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -778,6 +785,7 @@ request.currentTime = media.media.duration / 2; media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.getEstimatedTime() > request.currentTime - 1 && media.getEstimatedTime() < request.currentTime + 1) { media.removeUpdateListener(listener); @@ -786,6 +794,7 @@ }); media.seek(request, function () { assert.closeTo(media.getEstimatedTime(), request.currentTime, 1); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -799,6 +808,7 @@ var request = new chrome.cast.media.SeekRequest(); request.currentTime = media.media.duration; media.addUpdateListener(function listener (isAlive) { + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.playerState === chrome.cast.media.PlayerState.IDLE) { media.removeUpdateListener(listener); assert.equal(media.idleReason, chrome.cast.media.IdleReason.FINISHED); @@ -807,6 +817,7 @@ } }); media.seek(request, function () { + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -870,29 +881,19 @@ done(); }); }); - it('session.loadMedia should be able to load videos twice in a row and handle MovieMediaMetadata and TvShowMediaMetadata correctly, and first media should be invalidated', function (done) { + it('session.loadMedia should be able to load media twice in a row and handle MovieMediaMetadata and AudiobookChapterMediaMetadata correctly, and first media should be invalidated', function (done) { var firstMedia; - var mediaInfo = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - mediaInfo.metadata = new chrome.cast.media.MovieMediaMetadata(); - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.subtitle = 'DaSubtitle'; - mediaInfo.metadata.studio = 'DaStudio'; - mediaInfo.metadata.myMadeUpMetadata = 'DaMadeUpMetadata'; - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; + var mediaInfo = mediaUtils.getMediaInfoItem('VIDEO', chrome.cast.media.MetadataType.MOVIE); + session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { media = m; firstMedia = m; utils.testMediaProperties(media); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.studio, mediaInfo.metadata.studio); - assert.equal(media.media.metadata.myMadeUpMetadata, mediaInfo.metadata.myMadeUpMetadata); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.MOVIE); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.MOVIE); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -906,15 +907,7 @@ }); function loadSecond () { - var mediaInfo = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - mediaInfo.metadata = new chrome.cast.media.TvShowMediaMetadata(); - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.subtitle = 'DaSubtitle'; - mediaInfo.metadata.originalAirDate = new Date().valueOf(); - mediaInfo.metadata.episode = 15; - mediaInfo.metadata.season = 2; - mediaInfo.metadata.seriesTitle = 'DaSeries'; - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; + var mediaInfo = mediaUtils.getMediaInfoItem('AUDIO', chrome.cast.media.MetadataType.AUDIOBOOK_CHAPTER); session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { media = m; // Test old media is invalid (should not equal new media and should give error on pause) @@ -929,18 +922,11 @@ // Now verify the new media utils.testMediaProperties(media); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.originalAirDate, mediaInfo.metadata.originalAirDate); - assert.equal(media.media.metadata.episode, mediaInfo.metadata.episode); - assert.equal(media.media.metadata.season, mediaInfo.metadata.season); - assert.equal(media.media.metadata.seriesTitle, mediaInfo.metadata.seriesTitle); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.TV_SHOW); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.TV_SHOW); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -955,35 +941,17 @@ }); } }); - it('session.loadMedia should be able to load remote audio and return the MusicTrackMediaMetadata', function (done) { - var mediaInfo = new chrome.cast.media.MediaInfo(audioUrl, 'audio/mpeg'); - mediaInfo.metadata = new chrome.cast.media.MusicTrackMediaMetadata(); - mediaInfo.metadata.albumArtist = 'DaAlmbumArtist'; - mediaInfo.metadata.albumName = 'DaAlbum'; - mediaInfo.metadata.artist = 'DaArtist'; - mediaInfo.metadata.composer = 'DaComposer'; - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.songName = 'DaSongName'; - mediaInfo.metadata.releaseDate = new Date().valueOf(); - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; - mediaInfo.metadata.myMadeUpMetadata = 15; + it('session.loadMedia should be able to load remote video with no metadata', function (done) { + var mediaInfo = mediaUtils.getMediaInfoItem('VIDEO'); session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { media = m; utils.testMediaProperties(media); - assert.equal(media.media.metadata.albumArtist, mediaInfo.metadata.albumArtist); - assert.equal(media.media.metadata.albumName, mediaInfo.metadata.albumName); - assert.equal(media.media.metadata.artist, mediaInfo.metadata.artist); - assert.equal(media.media.metadata.composer, mediaInfo.metadata.composer); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.songName, mediaInfo.metadata.songName); - assert.equal(media.media.metadata.releaseDate, mediaInfo.metadata.releaseDate); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.myMadeUpMetadata, mediaInfo.metadata.myMadeUpMetadata); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.MUSIC_TRACK); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.MUSIC_TRACK); + assert.oneOf(media.media.metadata, [null, undefined]); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -996,37 +964,40 @@ assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); }); }); + it('session.loadMedia should be able to load live stream audio media', function (done) { + this.timeout(90000); + var mediaInfo = mediaUtils.getMediaInfoItem('LIVE_AUDIO'); + session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { + media = m; + utils.testMediaProperties(media, true); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); + media.addUpdateListener(function listener (isAlive) { + assert.isTrue(isAlive); + utils.testMediaProperties(media, true); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); + assert.oneOf(media.playerState, [ + chrome.cast.media.PlayerState.PLAYING, + chrome.cast.media.PlayerState.BUFFERING]); + if (media.playerState === chrome.cast.media.PlayerState.PLAYING) { + console.log(media); + media.removeUpdateListener(listener); + done(); + } + }); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + }); it('session.loadMedia should be able to load remote image and return the PhotoMediaMetadata', function (done) { - var mediaInfo = new chrome.cast.media.MediaInfo(imageUrl, 'image/jpeg'); - mediaInfo.metadata = new chrome.cast.media.PhotoMediaMetadata(); - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.artist = 'DaArtist'; - mediaInfo.metadata.location = 'DaLocation'; - mediaInfo.metadata.latitude = 102.13; - mediaInfo.metadata.longitude = 101.12; - mediaInfo.metadata.height = 100; - mediaInfo.metadata.width = 100; - mediaInfo.metadata.myMadeUpMetadata = 15; - mediaInfo.metadata.creationDateTime = new Date().valueOf(); - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; + var mediaInfo = mediaUtils.getMediaInfoItem('IMAGE', chrome.cast.media.MetadataType.PHOTO); session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { media = m; utils.testMediaProperties(media); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.artist, mediaInfo.metadata.artist); - assert.equal(media.media.metadata.location, mediaInfo.metadata.location); - assert.equal(media.media.metadata.latitude, mediaInfo.metadata.latitude); - assert.equal(media.media.metadata.longitude, mediaInfo.metadata.longitude); - assert.equal(media.media.metadata.height, mediaInfo.metadata.height); - assert.equal(media.media.metadata.width, mediaInfo.metadata.width); - assert.equal(media.media.metadata.myMadeUpMetadata, mediaInfo.metadata.myMadeUpMetadata); - assert.equal(media.media.metadata.creationDateTime, mediaInfo.metadata.creationDateTime); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.PHOTO); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.PHOTO); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.playerState === chrome.cast.media.PlayerState.PAUSED) { media.removeUpdateListener(listener); done(); @@ -1036,7 +1007,7 @@ assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); }); }); - it('media.stop should end video playback', function (done) { + it('media.stop should end media playback', function (done) { var called = utils.waitForAllCalls([ { id: success, repeats: false }, { id: update, repeats: true } @@ -1064,32 +1035,7 @@ var startTime = 40; var jumpItemId; var request; - var assertVideoItem = function (media) { - assert.equal(media.contentId, videoUrl); - assert.equal(media.metadata.title, videoItem.metadata.title); - assert.equal(media.metadata.subtitle, videoItem.metadata.subtitle); - assert.equal(media.metadata.originalAirDate, videoItem.metadata.originalAirDate); - assert.equal(media.metadata.episode, videoItem.metadata.episode); - assert.equal(media.metadata.season, videoItem.metadata.season); - assert.equal(media.metadata.seriesTitle, videoItem.metadata.seriesTitle); - assert.equal(media.metadata.images[0].url, videoItem.metadata.images[0].url); - assert.equal(media.metadata.metadataType, chrome.cast.media.MetadataType.TV_SHOW); - assert.equal(media.metadata.type, chrome.cast.media.MetadataType.TV_SHOW); - }; - var assertAudioItem = function (media) { - assert.equal(media.contentId, audioUrl); - assert.equal(media.metadata.albumArtist, audioItem.metadata.albumArtist); - assert.equal(media.metadata.albumName, audioItem.metadata.albumName); - assert.equal(media.metadata.artist, audioItem.metadata.artist); - assert.equal(media.metadata.composer, audioItem.metadata.composer); - assert.equal(media.metadata.title, audioItem.metadata.title); - assert.equal(media.metadata.songName, audioItem.metadata.songName); - assert.equal(media.metadata.releaseDate, audioItem.metadata.releaseDate); - assert.equal(media.metadata.images[0].url, audioItem.metadata.images[0].url); - assert.equal(media.metadata.myMadeUpMetadata, audioItem.metadata.myMadeUpMetadata); - assert.equal(media.metadata.metadataType, chrome.cast.media.MetadataType.MUSIC_TRACK); - assert.equal(media.metadata.type, chrome.cast.media.MetadataType.MUSIC_TRACK); - }; + var assertQueueProperties = function (media) { utils.testMediaProperties(media); assert.equal(media.repeatMode, request.repeatMode); @@ -1099,28 +1045,8 @@ utils.testQueueItems(media.items); }; before(function () { - videoItem = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - videoItem.metadata = new chrome.cast.media.TvShowMediaMetadata(); - videoItem.metadata.title = 'DaTitle'; - videoItem.metadata.subtitle = 'DaSubtitle'; - videoItem.metadata.originalAirDate = new Date().valueOf(); - videoItem.metadata.episode = 15; - videoItem.metadata.season = 2; - videoItem.metadata.seriesTitle = 'DaSeries'; - videoItem.metadata.images = [new chrome.cast.Image(imageUrl)]; - - audioItem = new chrome.cast.media.MediaInfo(audioUrl, 'audio/mpeg'); - audioItem.metadata = new chrome.cast.media.MusicTrackMediaMetadata(); - audioItem.metadata.albumArtist = 'DaAlmbumArtist'; - audioItem.metadata.albumName = 'DaAlbum'; - audioItem.metadata.artist = 'DaArtist'; - audioItem.metadata.composer = 'DaComposer'; - audioItem.metadata.title = 'DaTitle'; - audioItem.metadata.songName = 'DaSongName'; - audioItem.metadata.myMadeUpMetadata = '15'; - audioItem.metadata.releaseDate = new Date().valueOf(); - audioItem.metadata.images = [new chrome.cast.Image(imageUrl)]; - + videoItem = mediaUtils.getMediaInfoItem('VIDEO', chrome.cast.media.MetadataType.TV_SHOW); + audioItem = mediaUtils.getMediaInfoItem('AUDIO', chrome.cast.media.MetadataType.MUSIC_TRACK); var item; var queue = []; @@ -1154,7 +1080,7 @@ session.queueLoad(request, function (m) { media = m; assertQueueProperties(media); - assertAudioItem(media.media); + mediaUtils.assertMediaInfoItemEquals(media.media, audioItem); assert.closeTo(media.getEstimatedTime(), startTime * 2, 5); // Items should contain the last 2 items in the queue @@ -1162,12 +1088,12 @@ var i = utils.getCurrentItemIndex(media); - assertAudioItem(media.items[i - 1].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i - 1].media, audioItem); assert.equal(media.items[i - 1].orderId, request.startIndex - 1); assert.isTrue(media.items[i - 1].autoplay); assert.equal(media.items[i - 1].startTime, startTime * 2); - assertAudioItem(media.items[i].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i].media, audioItem); assert.equal(media.items[i].orderId, request.startIndex); assert.isTrue(media.items[i].autoplay); assert.equal(media.items[i].startTime, startTime * 2); @@ -1214,19 +1140,19 @@ media.removeUpdateListener(listener); assertQueueProperties(media); - assertVideoItem(media.media); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); assert.closeTo(media.getEstimatedTime(), startTime, 5); // Items should contain the first 2 items in the queue assert.equal(media.items.length, 2); var i = utils.getCurrentItemIndex(media); - assertVideoItem(media.items[i].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i].media, videoItem); assert.equal(media.items[i].orderId, 0); assert.isTrue(media.items[i].autoplay); assert.equal(media.items[i].startTime, startTime); - assertVideoItem(media.items[i + 1].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i + 1].media, videoItem); assert.equal(media.items[i + 1].orderId, 1); assert.isTrue(media.items[i + 1].autoplay); assert.equal(media.items[i + 1].startTime, startTime); @@ -1292,24 +1218,24 @@ media.removeUpdateListener(listener); assertQueueProperties(media); - assertVideoItem(media.media); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); assert.closeTo(media.getEstimatedTime(), startTime, 5); // Items should contain the first 3 items in the queue (1 before and 1 after current item) assert.equal(media.items.length, 3); var i = utils.getCurrentItemIndex(media); - assertVideoItem(media.items[i - 1].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i - 1].media, videoItem); assert.equal(media.items[i - 1].orderId, 0); assert.isTrue(media.items[i - 1].autoplay); assert.equal(media.items[i - 1].startTime, startTime); - assertVideoItem(media.items[i].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i].media, videoItem); assert.equal(media.items[i].orderId, 1); assert.isTrue(media.items[i].autoplay); assert.equal(media.items[i].startTime, startTime); - assertAudioItem(media.items[i + 1].media); + mediaUtils.assertMediaInfoItemEquals(media.items[i + 1].media, audioItem); assert.equal(media.items[i + 1].orderId, 2); assert.isTrue(media.items[i + 1].autoplay); assert.equal(media.items[i + 1].startTime, startTime * 2); diff --git a/tests/www/js/tests_interaction_primary.js b/tests/www/js/tests_interaction_primary.js new file mode 100644 index 0000000..a859f25 --- /dev/null +++ b/tests/www/js/tests_interaction_primary.js @@ -0,0 +1,393 @@ +/** + * The order of these tests and this.bail(true) is very important. + * + * Rather than nesting deep with describes and before's we just ensure the + * tests occur in the correct order. + * The major advantage to this is not having to repeat test code frequently + * making the suite slow. + * + */ + +(function () { + 'use strict'; + /* eslint-env mocha */ + /* global chrome */ + + var assert = window.chai.assert; + var utils = window['cordova-plugin-chromecast-tests'].utils; + var mediaUtils = window['cordova-plugin-chromecast-tests'].mediaUtils; + + mocha.setup({ + bail: true, + ui: 'bdd', + useColors: true, + reporter: window['cordova-plugin-chromecast-tests'].customHtmlReporter, + slow: 10000, + timeout: 180000 + }); + + describe('Interaction Tests - Primary Device', function () { + // callOrder constants that are re-used frequently + var success = 'success'; + var stopped = 'stopped'; + var update = 'update'; + + var session; + var media; + var videoItem; + var audioItem; + + var cookieName = 'primary-p2_restart-reload'; + var runningNum = parseInt(utils.getValue(cookieName) || '-1'); + + before('setup constants', function () { + // This must be identical to the before('setup constants'.. in tests_interaction_secondary.js + videoItem = mediaUtils.getMediaInfoItem('VIDEO', chrome.cast.media.MetadataType.TV_SHOW, new Date(2020, 10, 31)); + audioItem = mediaUtils.getMediaInfoItem('AUDIO', chrome.cast.media.MetadataType.MUSIC_TRACK, new Date(2020, 10, 31)); + // TODO desktop chrome does not send all metadata attributes for some reason, + // So delete the metadata so that assertMediaInfoEquals does not compare it + videoItem.metadata = null; + audioItem.metadata = null; + }); + before('Api should be available and initialize successfully', function (done) { + this.timeout(15000); + session = null; + var interval = setInterval(function () { + if (chrome && chrome.cast && chrome.cast.isAvailable) { + clearInterval(interval); + done(); + } + }, 100); + }); + describe('External Sender Sends Commands', function () { + before('already passed all tests in this section', function () { + // Have we already passed the tests in this describe and should skip? + if (runningNum > -1) { + this.skip(); + } + }); + before('ensure initialized', function (done) { + this.timeout(15000); + utils.setAction('Initializing...'); + + var finished = false; // Need this so we stop testing after being finished + var available = 'available'; + var called = utils.callOrder([ + { id: success, repeats: false }, + { id: available, repeats: true } + ], function () { + finished = true; + if (session) { + assert.equal(session.status, chrome.cast.SessionStatus.STOPPED); + } + done(); + }); + var apiConfig = new chrome.cast.ApiConfig( + new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), + function (sess) { + session = sess; + assert.fail('Should not receive session on initialize. We should only call this initialize when there is no existing session.'); + }, function receiverListener (availability) { + if (!finished && availability === available) { + called(availability); + } + }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); + chrome.cast.initialize(apiConfig, function () { + called(success); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + }); + it('Join external session', function (done) { + if (utils.isDesktop()) { + // This is a hack because desktop chrome is incapable of + // joining a session. So we have to create the session + // from chrome first and then join from the app. + return utils.startSession(function (sess) { + session = sess; + showInstructions(done); + }); + } + // Else + showInstructions(function () { + utils.startSession(function (sess) { + session = sess; + utils.testSessionProperties(session); + done(); + }); + }); + function showInstructions (callback) { + utils.setAction('Ensure you have only 1 physical chromecast device on your network (castGroups are fine).
' + + '
1. On a secondary device (or desktop chrome browser),' + + ' navigate to Interaction Tests - Secondary Device
' + + '2. Follow instructions on secondary device.', + 'Listen for External Load Media', + function () { + callback(); + }); + } + }); + it('External loadMedia should trigger mediaListener', function (done) { + utils.setAction('On secondary click "Load Media"'); + var finished = false; + session.addMediaListener(function listener (m) { + if (finished) { + return; + } + utils.setAction('Tests running...'); + media = m; + utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); + finished = true; + done(); + }); + }); + it('External media stop should trigger media updateListener', function (done) { + utils.setAction('On secondary click "Stop Media"'); + media.addUpdateListener(function listener (isAlive) { + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); + if (media.playerState === chrome.cast.media.PlayerState.IDLE) { + media.removeUpdateListener(listener); + assert.equal(media.idleReason, chrome.cast.media.IdleReason.CANCELLED); + assert.isFalse(isAlive); + done(); + } + }); + }); + it('External queueLoad should trigger mediaListener', function (done) { + utils.setAction('On secondary click "Load Queue"'); + var finished = false; + session.addMediaListener(function listener (m) { + if (finished) { + return; + } + finished = true; + media = m; + mediaUtils.assertMediaInfoItemEquals(media.media, audioItem); + var interval = setInterval(function () { + if (media.currentItemId > -1) { + clearInterval(interval); + finished = true; + utils.testMediaProperties(media); + var items = media.items; + var startTime = 40; + assert.isTrue(items[0].autoplay); + assert.equal(items[0].startTime, startTime); + mediaUtils.assertMediaInfoItemEquals(items[0].media, videoItem); + assert.isTrue(items[1].autoplay); + assert.equal(items[1].startTime, startTime * 2); + mediaUtils.assertMediaInfoItemEquals(items[1].media, audioItem); + done(); + } + }, 400); + }); + }); + it('Jump to different queue item should trigger media.addUpdateListener and not session.addMediaListener', function (done) { + utils.setAction('On secondary click "Queue Jump"'); + var called = utils.callOrder([ + { id: stopped, repeats: true }, + { id: update, repeats: true } + ], done); + var currentItemId = media.currentItemId; + var mediaListener = function (media) { + assert.fail('session.addMediaListener should only be called when an external sender loads media. ' + + '(We are the one loading. We are not external to ourself.'); + }; + session.addMediaListener(mediaListener); + media.addUpdateListener(function listener (isAlive) { + assert.isTrue(isAlive); + if (media.playerState === chrome.cast.media.PlayerState.IDLE) { + assert.oneOf(media.idleReason, + [chrome.cast.media.IdleReason.INTERRUPTED, chrome.cast.media.IdleReason.FINISHED]); + called(stopped); + } + if (media.currentItemId !== currentItemId) { + session.removeMediaListener(mediaListener); + media.removeUpdateListener(listener); + utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); + called(update); + } + }); + }); + it('session.leave should leave the session', function (done) { + utils.setAction('Follow instructions on secondary.', 'Leave Session', function () { + // Set up the expected calls + var called = utils.callOrder([ + { id: success, repeats: false }, + { id: update, repeats: true } + ], function () { + utils.setAction('1. On secondary, click "Check Session"
' + + '2. Then follow directions on secondary!', 'Page Reload', function () { + utils.storeValue(cookieName, 0); + window.location.href = window.location.href; + done(); + }); + }); + var finished = false; + session.addUpdateListener(function listener (isAlive) { + if (finished) { + return; + } + assert.isTrue(isAlive); + if (session.status === chrome.cast.SessionStatus.DISCONNECTED) { + finished = true; + called(update); + } + }); + session.leave(function () { + called(success); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + }); + }); + after('Ensure we have left the session', function (done) { + if (!session) { + return done(); + } + session.leave(function () { + done(); + }, function () { + done(); + }); + }); + }); + describe('App restart and reload/change page simulation', function () { + it('Should not receive a session on initialize after a page change', function (done) { + this.timeout(15000); + if (runningNum > 0) { + // Just pass the test because we need to skip ahead + return done(); + } + utils.setAction('Checking for session after page load, (should not find session)...'); + var finished = false; // Need this so we stop testing after being finished + var available = 'available'; + var called = utils.callOrder([ + { id: success, repeats: false }, + { id: available, repeats: true } + ], function () { + finished = true; + // Give it an extra moment to check for the session + setTimeout(function () { + utils.storeValue(cookieName, ++runningNum); + done(); + }, 1000); + }); + var apiConfig = new chrome.cast.ApiConfig( + new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), + function (sess) { + session = sess; + if (!utils.isDesktop()) { + assert.fail('should not receive a session (make sure there is no active cast session when starting the tests)'); + } + }, function receiverListener (availability) { + if (!finished && availability === available) { + called(availability); + } + }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); + chrome.cast.initialize(apiConfig, function () { + called(success); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + }); + it('Should not receive a session on initialize after app restart', function (done) { + var instructionNum = 1; + var testNum = 2; + assert.isAtLeast(runningNum, instructionNum, 'Should not be running this test yet'); + switch (runningNum) { + case instructionNum: + // Show instructions for app restart + utils.storeValue(cookieName, testNum); + if (utils.isDesktop()) { + // If desktop, just reload the page (because restart doesn't work) + window.location.reload(); + } + this.timeout(0); // no timeout + utils.setAction('Force kill and restart the app, and navigate back to Interaction Tests - Primary Device.' + + '
Note: Android 4.4 does not support this feature, so just refresh the page.'); + break; + case testNum: + this.timeout(15000); + // Test initialize since we just reloaded + utils.setAction('Checking for session after app restart, (should not find session)...'); + var finished = false; // Need this so we stop testing after being finished + var available = 'available'; + var called = utils.callOrder([ + { id: success, repeats: false }, + { id: available, repeats: true } + ], function () { + finished = true; + // Give it an extra moment to check for the session + setTimeout(function () { + utils.storeValue(cookieName, ++runningNum); + done(); + }, 1000); + }); + var apiConfig = new chrome.cast.ApiConfig( + new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), + function (sess) { + session = sess; + if (!utils.isDesktop()) { + assert.fail('should not receive a session (make sure there is no active cast session when starting the tests)'); + } + }, function receiverListener (availability) { + if (!finished && availability === available) { + called(availability); + } + }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); + chrome.cast.initialize(apiConfig, function () { + called(success); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + break; + default: + // We must be looking to run a test further down the line + return done(); + } + }); + after(function () { + // Reset runningNum as we are done with it + utils.clearStoredValues(); + }); + }); + describe('session interaction with secondary', function () { + it('Create session', function (done) { + utils.setAction('On secondary click "Leave Session".', + 'Enter Session' + + (utils.isDesktop() ? '
(Desktop: Stop & Start casting from the same cast pop up)' : ''), + function () { + utils.startSession(function (sess) { + session = sess; + utils.testSessionProperties(session); + utils.setAction('On secondary click "Join/Start Session".'); + done(); + }); + }); + }); + it('External session.stop should kill this session as well', function (done) { + session.addUpdateListener(function listener (isAlive) { + if (session.status === chrome.cast.SessionStatus.STOPPED) { + assert.isFalse(isAlive); + session.removeUpdateListener(listener); + done(); + } + }); + }); + }); + after('Ensure we have stopped the session', function (done) { + if (!session) { + return done(); + } + session.stop(function () { + done(); + }, function () { + done(); + }); + }); + + }); + +}()); diff --git a/tests/www/js/tests_manual_secondary.js b/tests/www/js/tests_interaction_secondary.js similarity index 66% rename from tests/www/js/tests_manual_secondary.js rename to tests/www/js/tests_interaction_secondary.js index c983fb8..d192282 100644 --- a/tests/www/js/tests_manual_secondary.js +++ b/tests/www/js/tests_interaction_secondary.js @@ -15,7 +15,7 @@ var assert = window.chai.assert; var utils = window['cordova-plugin-chromecast-tests'].utils; - var isDesktop = window['cordova-plugin-chromecast-tests'].isDesktop || false; + var mediaUtils = window['cordova-plugin-chromecast-tests'].mediaUtils; mocha.setup({ bail: true, @@ -27,10 +27,6 @@ }); describe('Manual Tests - Secondary Device', function () { - var imageUrl = 'https://ia800705.us.archive.org/1/items/GoodHousekeeping193810/Good%20Housekeeping%201938-10.jpg'; - var videoUrl = 'https://ia801302.us.archive.org/1/items/TheWater_201510/TheWater.mp4'; - var audioUrl = 'https://ia800306.us.archive.org/26/items/1939RadioNews/1939-10-24-CBS-Elmer-Davis-Reports-City-Of-Flint-Still-Missing.mp3'; - // callOrder constants that are re-used frequently var success = 'success'; var stopped = 'stopped'; @@ -47,36 +43,21 @@ var checkItems = function (items) { assert.isTrue(items[0].autoplay); assert.equal(items[0].startTime, startTime); - assert.equal(items[0].media.contentId, videoUrl); + mediaUtils.assertMediaInfoItemEquals(items[0].media, videoItem); assert.isTrue(items[1].autoplay); assert.equal(items[1].startTime, startTime * 2); - assert.equal(items[1].media.contentId, audioUrl); + mediaUtils.assertMediaInfoItemEquals(items[1].media, audioItem); }; before('setup constants', function () { - videoItem = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - videoItem.metadata = new chrome.cast.media.TvShowMediaMetadata(); - videoItem.metadata.title = 'DaTitle'; - videoItem.metadata.subtitle = 'DaSubtitle'; - videoItem.metadata.originalAirDate = new Date().valueOf(); - videoItem.metadata.episode = 15; - videoItem.metadata.season = 2; - videoItem.metadata.seriesTitle = 'DaSeries'; - videoItem.metadata.images = [new chrome.cast.Image(imageUrl)]; - - audioItem = new chrome.cast.media.MediaInfo(audioUrl, 'audio/mpeg'); - audioItem.metadata = new chrome.cast.media.MusicTrackMediaMetadata(); - audioItem.metadata.albumArtist = 'DaAlmbumArtist'; - audioItem.metadata.albumName = 'DaAlbum'; - audioItem.metadata.artist = 'DaArtist'; - audioItem.metadata.composer = 'DaComposer'; - audioItem.metadata.title = 'DaTitle'; - audioItem.metadata.songName = 'DaSongName'; - audioItem.metadata.myMadeUpMetadata = '15'; - audioItem.metadata.releaseDate = new Date().valueOf(); - audioItem.metadata.images = [new chrome.cast.Image(imageUrl)]; + // This must be identical to the before('setup constants'.. in tests_interaction_primary.js + videoItem = mediaUtils.getMediaInfoItem('VIDEO', chrome.cast.media.MetadataType.TV_SHOW, new Date(2020, 10, 31)); + audioItem = mediaUtils.getMediaInfoItem('AUDIO', chrome.cast.media.MetadataType.MUSIC_TRACK, new Date(2020, 10, 31)); + // TODO desktop chrome does not send all metadata attributes for some reason, + // So delete the metadata so that assertMediaInfoEquals does not compare it + videoItem.metadata = null; + audioItem.metadata = null; }); - before('Api should be available and initialize successfully', function (done) { session = null; var interval = setInterval(function () { @@ -124,43 +105,17 @@ done(); }); }); - it('session.loadMedia should be able to load a remote video and handle GenericMediaMetadata', function (done) { - utils.setAction('On primary click "Continue"', 'Load Media', function () { - var mediaInfo = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.subtitle = 'DaSubtitle'; - mediaInfo.metadata.releaseDate = new Date().valueOf(); - mediaInfo.metadata.someTrueBoolean = true; - mediaInfo.metadata.someFalseBoolean = false; - mediaInfo.metadata.someSmallNumber = 15; - mediaInfo.metadata.someLargeNumber = 1234567890123456; - mediaInfo.metadata.someSmallDecimal = 15.15; - mediaInfo.metadata.someLargeDecimal = 1234567.123456789; - mediaInfo.metadata.someString = 'SomeString'; - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; - session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (m) { + it('session.loadMedia should be able to load a remote video', function (done) { + utils.setAction('On primary click "Listen for External Load Media"', 'Load Media', function () { + session.loadMedia(new chrome.cast.media.LoadRequest(videoItem), function (m) { media = m; utils.testMediaProperties(media); assert.isUndefined(media.queueData); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.releaseDate, mediaInfo.metadata.releaseDate); - // TODO figure out how to maintain the data types for custom params on the native side - // so that we don't have to do turn each actual and expected into a string - assert.equal(media.media.metadata.someTrueBoolean + '', mediaInfo.metadata.someTrueBoolean + ''); - assert.equal(media.media.metadata.someFalseBoolean + '', mediaInfo.metadata.someFalseBoolean + ''); - assert.equal(media.media.metadata.someSmallNumber + '', mediaInfo.metadata.someSmallNumber + ''); - assert.equal(media.media.metadata.someLargeNumber + '', mediaInfo.metadata.someLargeNumber + ''); - assert.equal(media.media.metadata.someSmallDecimal + '', mediaInfo.metadata.someSmallDecimal + ''); - assert.equal(media.media.metadata.someLargeDecimal + '', mediaInfo.metadata.someLargeDecimal + ''); - assert.equal(media.media.metadata.someString, mediaInfo.metadata.someString); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.GENERIC); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.GENERIC); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -183,6 +138,7 @@ done(); }); media.addUpdateListener(function listener (isAlive) { + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); if (media.playerState === chrome.cast.media.PlayerState.IDLE) { media.removeUpdateListener(listener); assert.equal(media.idleReason, chrome.cast.media.IdleReason.CANCELLED); @@ -191,6 +147,7 @@ } }); media.stop(null, function () { + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); assert.equal(media.playerState, chrome.cast.media.PlayerState.IDLE); assert.equal(media.idleReason, chrome.cast.media.IdleReason.CANCELLED); called(success); @@ -228,23 +185,14 @@ assert.isFalse(media.queueData.shuffle); assert.equal(media.queueData.startIndex, request.startIndex); utils.testQueueItems(media.items); - assert.equal(media.media.contentId, audioUrl); + mediaUtils.assertMediaInfoItemEquals(media.media, audioItem); assert.equal(media.items.length, 2); checkItems(media.items); - assert.equal(media.items[i].media.metadata.albumArtist, audioItem.metadata.albumArtist); - assert.equal(media.items[i].media.metadata.albumName, audioItem.metadata.albumName); - assert.equal(media.items[i].media.metadata.artist, audioItem.metadata.artist); - assert.equal(media.items[i].media.metadata.composer, audioItem.metadata.composer); - assert.equal(media.items[i].media.metadata.title, audioItem.metadata.title); - assert.equal(media.items[i].media.metadata.songName, audioItem.metadata.songName); - assert.equal(media.items[i].media.metadata.releaseDate, audioItem.metadata.releaseDate); - assert.equal(media.items[i].media.metadata.images[0].url, audioItem.metadata.images[0].url); - assert.equal(media.items[i].media.metadata.myMadeUpMetadata, audioItem.metadata.myMadeUpMetadata); - assert.equal(media.items[i].media.metadata.metadataType, chrome.cast.media.MetadataType.MUSIC_TRACK); - assert.equal(media.items[i].media.metadata.type, chrome.cast.media.MetadataType.MUSIC_TRACK); + mediaUtils.assertMediaInfoItemEquals(media.items[i].media, audioItem); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, audioItem); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -281,25 +229,16 @@ assert.isTrue(isAlive); calledOrder(stopped); } - if (media.currentItemId !== media.items[i].itemId && media.media.contentId === videoUrl) { + if (media.currentItemId !== media.items[i].itemId) { i = utils.getCurrentItemIndex(media); media.removeUpdateListener(listener); utils.testMediaProperties(media); assert.equal(media.currentItemId, media.items[i].itemId); utils.testQueueItems(media.items); - assert.equal(media.media.contentId, videoUrl); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); assert.equal(media.items.length, 2); checkItems(media.items); - assert.equal(media.items[i].media.contentId, videoUrl); - assert.equal(media.items[i].media.metadata.title, videoItem.metadata.title); - assert.equal(media.items[i].media.metadata.subtitle, videoItem.metadata.subtitle); - assert.equal(media.items[i].media.metadata.originalAirDate, videoItem.metadata.originalAirDate); - assert.equal(media.items[i].media.metadata.episode, videoItem.metadata.episode); - assert.equal(media.items[i].media.metadata.season, videoItem.metadata.season); - assert.equal(media.items[i].media.metadata.seriesTitle, videoItem.metadata.seriesTitle); - assert.equal(media.items[i].media.metadata.images[0].url, videoItem.metadata.images[0].url); - assert.equal(media.items[i].media.metadata.metadataType, chrome.cast.media.MetadataType.TV_SHOW); - assert.equal(media.items[i].media.metadata.type, chrome.cast.media.MetadataType.TV_SHOW); + mediaUtils.assertMediaInfoItemEquals(media.media, videoItem); assert.closeTo(media.getEstimatedTime(), startTime, 5); calledOrder(newMedia); } @@ -321,9 +260,8 @@ }); it('Primary should not receive session on initialize', function (done) { this.timeout(240000); - utils.setAction('1. On primary, click "Back".' - + '
2. On primary, Select Manual Tests (Primary) Part 2.' - + '
3. Wait for instructions from primary.', 'Start Part 2', done); + utils.setAction('1. On primary, click "Page Reload".' + + '
2. Wait for instructions from primary.', 'Leave Session', done); }); it('Secondary session.leave should cause session to end (because all senders have left)', function (done) { var called = utils.waitForAllCalls([ @@ -344,17 +282,17 @@ }); }); it('Join session', function (done) { - if (isDesktop) { + if (utils.isDesktop()) { // This is a hack because desktop chrome is incapable of // joining a session. So we have to create the session // from chrome first and then join from the app. utils.startSession(function (sess) { session = sess; - utils.setAction('1. On primary click "Enter Session"
2. Wait for instructions from primary.', 'Continue', done); + utils.setAction('1. On primary click "Enter Session"
2. Wait for instructions from primary.', 'Join/Start Session', done); }); return; } - utils.setAction('On primary click "Enter Session"', 'Continue', function () { + utils.setAction('On primary click "Enter Session"', 'Join/Start Session', function () { utils.startSession(function (sess) { session = sess; done(); diff --git a/tests/www/js/tests_manual_primary_1.js b/tests/www/js/tests_manual.js similarity index 57% rename from tests/www/js/tests_manual_primary_1.js rename to tests/www/js/tests_manual.js index 0d6a1be..6941f76 100644 --- a/tests/www/js/tests_manual_primary_1.js +++ b/tests/www/js/tests_manual.js @@ -11,11 +11,11 @@ (function () { 'use strict'; /* eslint-env mocha */ - /* global chrome */ + /* global chrome cordova */ var assert = window.chai.assert; var utils = window['cordova-plugin-chromecast-tests'].utils; - var isDesktop = window['cordova-plugin-chromecast-tests'].isDesktop || false; + var mediaUtils = window['cordova-plugin-chromecast-tests'].mediaUtils; mocha.setup({ bail: true, @@ -26,11 +26,7 @@ timeout: 180000 }); - describe('Manual Tests - Primary Device - Part 1', function () { - var imageUrl = 'https://ia800705.us.archive.org/1/items/GoodHousekeeping193810/Good%20Housekeeping%201938-10.jpg'; - var videoUrl = 'https://ia801302.us.archive.org/1/items/TheWater_201510/TheWater.mp4'; - var audioUrl = 'https://ia800306.us.archive.org/26/items/1939RadioNews/1939-10-24-CBS-Elmer-Davis-Reports-City-Of-Flint-Still-Missing.mp3'; - + describe('Manual Tests', function () { // callOrder constants that are re-used frequently var success = 'success'; var stopped = 'stopped'; @@ -55,20 +51,20 @@ var cookieName = 'primary-p1_restart-reload'; var runningNum = parseInt(utils.getValue(cookieName) || '0'); var mediaInfo; - before(function () { - mediaInfo = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); - mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); - mediaInfo.metadata.title = 'DaTitle'; - mediaInfo.metadata.subtitle = 'DaSubtitle'; - mediaInfo.metadata.releaseDate = new Date(2019, 10, 24).valueOf(); - mediaInfo.metadata.someTrueBoolean = true; - mediaInfo.metadata.someFalseBoolean = false; - mediaInfo.metadata.someSmallNumber = 15; - mediaInfo.metadata.someLargeNumber = 1234567890123456; - mediaInfo.metadata.someSmallDecimal = 15.15; - mediaInfo.metadata.someLargeDecimal = 1234567.123456789; - mediaInfo.metadata.someString = 'SomeString'; - mediaInfo.metadata.images = [new chrome.cast.Image(imageUrl)]; + var photoItem; + var assertQueueProperties = function (media) { + utils.testMediaProperties(media); + assert.isObject(media.queueData); + utils.testQueueItems(media.items); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); + var i = utils.getCurrentItemIndex(media); + assert.equal(i, 0); + mediaUtils.assertMediaInfoItemEquals(media.items[0].media, mediaInfo); + mediaUtils.assertMediaInfoItemEquals(media.items[1].media, photoItem); + }; + before('Create MediaInfo', function () { + mediaInfo = mediaUtils.getMediaInfoItem('VIDEO', chrome.cast.media.MetadataType.GENERIC, new Date(2019, 10, 24)); + photoItem = mediaUtils.getMediaInfoItem('IMAGE', chrome.cast.media.MetadataType.PHOTO, new Date(2020, 10, 31)); }); it('Create session', function (done) { this.timeout(15000); @@ -130,24 +126,11 @@ media = m; utils.testMediaProperties(media); assert.isUndefined(media.queueData); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.releaseDate, mediaInfo.metadata.releaseDate); - // TODO figure out how to maintain the data types for custom params on the native side - // so that we don't have to do turn each actual and expected into a string - assert.equal(media.media.metadata.someTrueBoolean + '', mediaInfo.metadata.someTrueBoolean + ''); - assert.equal(media.media.metadata.someFalseBoolean + '', mediaInfo.metadata.someFalseBoolean + ''); - assert.equal(media.media.metadata.someSmallNumber + '', mediaInfo.metadata.someSmallNumber + ''); - assert.equal(media.media.metadata.someLargeNumber + '', mediaInfo.metadata.someLargeNumber + ''); - assert.equal(media.media.metadata.someSmallDecimal + '', mediaInfo.metadata.someSmallDecimal + ''); - assert.equal(media.media.metadata.someLargeDecimal + '', mediaInfo.metadata.someLargeDecimal + ''); - assert.equal(media.media.metadata.someString, mediaInfo.metadata.someString); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.GENERIC); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.GENERIC); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); utils.testMediaProperties(media); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -198,21 +181,7 @@ assert.isAbove(sess.media.length, 0); media = sess.media[0]; assert.isUndefined(media.queueData); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.releaseDate, mediaInfo.metadata.releaseDate); - // TODO figure out how to maintain the data types for custom params on the native side - // so that we don't have to do turn each actual and expected into a string - assert.equal(media.media.metadata.someTrueBoolean + '', mediaInfo.metadata.someTrueBoolean + ''); - assert.equal(media.media.metadata.someFalseBoolean + '', mediaInfo.metadata.someFalseBoolean + ''); - assert.equal(media.media.metadata.someSmallNumber + '', mediaInfo.metadata.someSmallNumber + ''); - assert.equal(media.media.metadata.someLargeNumber + '', mediaInfo.metadata.someLargeNumber + ''); - assert.equal(media.media.metadata.someSmallDecimal + '', mediaInfo.metadata.someSmallDecimal + ''); - assert.equal(media.media.metadata.someLargeDecimal + '', mediaInfo.metadata.someLargeDecimal + ''); - assert.equal(media.media.metadata.someString, mediaInfo.metadata.someString); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.GENERIC); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.GENERIC); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); @@ -234,7 +203,7 @@ return done(); } }); - it('media.pause should pause playback', function (done) { + it('session.queueLoad after page reload should get new media items', function (done) { this.timeout(15000); var testNum = 2; assert.isAtLeast(runningNum, testNum, 'Should not be running this test yet'); @@ -244,27 +213,65 @@ } // Else, run the test - var called = utils.waitForAllCalls([ - { id: success, repeats: false }, - { id: update, repeats: true } - ], function () { - utils.storeValue(cookieName, ++runningNum); - done(); + // Add items to the queue + var queue = []; + queue.push(new chrome.cast.media.QueueItem(mediaInfo)); + queue.push(new chrome.cast.media.QueueItem(photoItem)); + + // Create request to repeat all and start at last item + var request = new chrome.cast.media.QueueLoadRequest(queue); + session.queueLoad(request, function (m) { + media = m; + console.log(media); + assertQueueProperties(media); + media.addUpdateListener(function listener (isAlive) { + assert.isTrue(isAlive); + assertQueueProperties(media); + assert.oneOf(media.playerState, [ + chrome.cast.media.PlayerState.PLAYING, + chrome.cast.media.PlayerState.BUFFERING]); + if (media.playerState === chrome.cast.media.PlayerState.PLAYING) { + media.removeUpdateListener(listener); + done(); + } + }); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); }); + }); + it('Pause media from notifications', function (done) { + this.timeout(15000); + var testNum = 2; + assert.isAtLeast(runningNum, testNum, 'Should not be running this test yet'); + if (runningNum > testNum) { + // We must be looking to run a test further down the line + return done(); + } + // Else, run the test media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.notEqual(media.playerState, chrome.cast.media.PlayerState.IDLE); + assertQueueProperties(media); if (media.playerState === chrome.cast.media.PlayerState.PAUSED) { media.removeUpdateListener(listener); - called(update); + utils.storeValue(cookieName, ++runningNum); + done(); } }); - media.pause(null, function () { - assert.equal(media.playerState, chrome.cast.media.PlayerState.PAUSED); - called(success); - }, function (err) { - assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); - }); + if (!utils.isDesktop() && cordova.platformId === 'android') { + utils.setAction('1. Drag down the Android notifications from the top status bar
2. Click the pause button', + 'There is no chromecast notification drop-down', mediaPause); + } else { + mediaPause(); + } + function mediaPause () { + media.pause(null, function () { + assert.equal(media.playerState, chrome.cast.media.PlayerState.PAUSED); + assertQueueProperties(media); + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + } }); it('Restart app with active session, should receive session on initialize', function (done) { var instructionNum = 3; @@ -274,12 +281,12 @@ case instructionNum: // Show instructions for app restart utils.storeValue(cookieName, testNum); - if (isDesktop) { + if (utils.isDesktop()) { // If desktop, just reload the page (because restart doesn't work) window.location.reload(); } this.timeout(0); // no timeout - utils.setAction('Force kill and restart the app, and navigate back to Manual Tests (Primary) Part 1.' + utils.setAction('Force kill and restart the app, and navigate back to Manual Tests.' + '
Note: Android 4.4 does not support this feature, so just refresh the page.'); break; case testNum: @@ -304,25 +311,9 @@ function (sess) { session = sess; utils.testSessionProperties(sess); - // // Ensure the media is maintained - assert.isAbove(sess.media.length, 0); + // Ensure the media is maintained media = sess.media[0]; - assert.isUndefined(media.queueData); - assert.equal(media.media.metadata.title, mediaInfo.metadata.title); - assert.equal(media.media.metadata.subtitle, mediaInfo.metadata.subtitle); - assert.equal(media.media.metadata.releaseDate, mediaInfo.metadata.releaseDate); - // TODO figure out how to maintain the data types for custom params on the native side - // so that we don't have to do turn each actual and expected into a string - assert.equal(media.media.metadata.someTrueBoolean + '', mediaInfo.metadata.someTrueBoolean + ''); - assert.equal(media.media.metadata.someFalseBoolean + '', mediaInfo.metadata.someFalseBoolean + ''); - assert.equal(media.media.metadata.someSmallNumber + '', mediaInfo.metadata.someSmallNumber + ''); - assert.equal(media.media.metadata.someLargeNumber + '', mediaInfo.metadata.someLargeNumber + ''); - assert.equal(media.media.metadata.someSmallDecimal + '', mediaInfo.metadata.someSmallDecimal + ''); - assert.equal(media.media.metadata.someLargeDecimal + '', mediaInfo.metadata.someLargeDecimal + ''); - assert.equal(media.media.metadata.someString, mediaInfo.metadata.someString); - assert.equal(media.media.metadata.images[0].url, mediaInfo.metadata.images[0].url); - assert.equal(media.media.metadata.metadataType, chrome.cast.media.MetadataType.GENERIC); - assert.equal(media.media.metadata.type, chrome.cast.media.MetadataType.GENERIC); + assertQueueProperties(media); assert.equal(media.playerState, chrome.cast.media.PlayerState.PAUSED); called(session_listener); }, function receiverListener (availability) { @@ -362,6 +353,7 @@ media.addUpdateListener(function listener (isAlive) { assert.isTrue(isAlive); assert.notEqual(media.playerState, chrome.cast.media.PlayerState.IDLE); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); if (media.playerState === chrome.cast.media.PlayerState.PLAYING) { media.removeUpdateListener(listener); called(update); @@ -371,6 +363,7 @@ assert.oneOf(media.playerState, [ chrome.cast.media.PlayerState.PLAYING, chrome.cast.media.PlayerState.BUFFERING]); + mediaUtils.assertMediaInfoItemEquals(media.media, mediaInfo); called(success); }, function (err) { assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); @@ -410,6 +403,10 @@ function (sess) { session = sess; utils.testSessionProperties(sess); + // Ensure the media is maintained + media = session.media[0]; + assertQueueProperties(media); + assert.equal(media.playerState, chrome.cast.media.PlayerState.PLAYING); called(session_listener); }, function receiverListener (availability) { if (!finished) { @@ -428,6 +425,28 @@ return done(); } }); + it('Stop session from notifications (android)', function (done) { + session.addUpdateListener(function listener (isAlive) { + if (session.status === chrome.cast.SessionStatus.STOPPED) { + assert.isFalse(isAlive); + session.removeUpdateListener(listener); + session = null; + done(); + } + }); + if (!utils.isDesktop() && cordova.platformId === 'android') { + utils.setAction('1. Drag down the Android notifications from the top status bar
2. Click the "X"', + 'There is no chromecast notification drop-down', sessionStop); + } else { + sessionStop(); + } + function sessionStop () { + session.stop(function () { + }, function (err) { + assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); + }); + } + }); after('Ensure session is stopped', function (done) { // Reset tests utils.storeValue(cookieName, 0); @@ -448,11 +467,9 @@ utils.setAction('Initializing...'); var finished = false; // Need this so we stop testing after being finished - var unavailable = 'unavailable'; var available = 'available'; var called = utils.callOrder([ { id: success, repeats: false }, - { id: unavailable, repeats: true }, { id: available, repeats: true } ], function () { finished = true; @@ -464,7 +481,7 @@ session = sess; assert.fail('Should not receive session on initialize. We should only call this initialize when there is no existing session.'); }, function receiverListener (availability) { - if (!finished) { + if (!finished && availability === available) { called(availability); } }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); @@ -521,7 +538,7 @@ } }); utils.setAction('1. Click "Open Dialog".
2. Select "Stop Casting" in the stop casting dialog.' - + (isDesktop ? '
3. Click outside of the stop casting dialog to dismiss it.' : ''), + + (utils.isDesktop() ? '
3. Click outside of the stop casting dialog to dismiss it.' : ''), 'Open Dialog', function () { chrome.cast.requestSession(function (session) { @@ -546,206 +563,6 @@ }); }); - describe('External Sender Sends Commands', function () { - before('ensure initialized', function (done) { - this.timeout(15000); - utils.setAction('Initializing...'); - - var finished = false; // Need this so we stop testing after being finished - var unavailable = 'unavailable'; - var available = 'available'; - var called = utils.callOrder([ - { id: success, repeats: false }, - { id: unavailable, repeats: true }, - { id: available, repeats: true } - ], function () { - finished = true; - if (session) { - assert.equal(session.status, chrome.cast.SessionStatus.STOPPED); - } - done(); - }); - var apiConfig = new chrome.cast.ApiConfig( - new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), - function (sess) { - session = sess; - assert.fail('Should not receive session on initialize. We should only call this initialize when there is no existing session.'); - }, function receiverListener (availability) { - if (!finished) { - called(availability); - } - }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); - chrome.cast.initialize(apiConfig, function () { - called(success); - }, function (err) { - assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); - }); - }); - it('Join external session', function (done) { - if (isDesktop) { - // This is a hack because desktop chrome is incapable of - // joining a session. So we have to create the session - // from chrome first and then join from the app. - return utils.startSession(function (sess) { - session = sess; - showInstructions(done); - }); - } - // Else - showInstructions(function () { - utils.startSession(function (sess) { - session = sess; - utils.testSessionProperties(session); - done(); - }); - }); - function showInstructions (callback) { - utils.setAction('Ensure you have only 1 physical chromecast device on your network (castGroups are fine).
' - + '
1. On a secondary device (or desktop chrome browser),' - + ' navigate to Manual Tests (Secondary)
' - + '2. Follow instructions on secondary app.', - 'Continue', - function () { - callback(); - }); - } - }); - it('External loadMedia should trigger mediaListener', function (done) { - utils.setAction('On secondary click "Load Media"'); - var finished = false; - session.addMediaListener(function listener (m) { - if (finished) { - return; - } - utils.setAction('Tests running...'); - media = m; - var interval = setInterval(function () { - if (media.media.tracks != null && media.media.tracks !== undefined) { - clearInterval(interval); - utils.testMediaProperties(media); - finished = true; - done(); - } - }, 400); - }); - }); - it('External media stop should trigger media updateListener', function (done) { - utils.setAction('On secondary click "Stop Media"'); - media.addUpdateListener(function listener (isAlive) { - if (media.playerState === chrome.cast.media.PlayerState.IDLE) { - media.removeUpdateListener(listener); - assert.equal(media.idleReason, chrome.cast.media.IdleReason.CANCELLED); - assert.isFalse(isAlive); - done(); - } - }); - }); - it('External queueLoad should trigger mediaListener', function (done) { - utils.setAction('On secondary click "Load Queue"'); - var finished = false; - session.addMediaListener(function listener (m) { - if (finished) { - return; - } - finished = true; - media = m; - var interval = setInterval(function () { - if (media.currentItemId > -1 && media.media.tracks) { - clearInterval(interval); - finished = true; - utils.testMediaProperties(media); - var items = media.items; - var startTime = 40; - assert.isTrue(items[0].autoplay); - assert.equal(items[0].startTime, startTime); - assert.equal(items[0].media.contentId, videoUrl); - assert.isTrue(items[1].autoplay); - assert.equal(items[1].startTime, startTime * 2); - assert.equal(items[1].media.contentId, audioUrl); - done(); - } - }, 400); - }); - }); - it('Jump to different queue item should trigger media.addUpdateListener and not session.addMediaListener', function (done) { - utils.setAction('On secondary click "Queue Jump"'); - var called = utils.callOrder([ - { id: stopped, repeats: true }, - { id: update, repeats: true } - ], done); - var currentItemId = media.currentItemId; - var mediaListener = function (media) { - assert.fail('session.addMediaListener should only be called when an external sender loads media. ' - + '(We are the one loading. We are not external to ourself.'); - }; - session.addMediaListener(mediaListener); - media.addUpdateListener(function listener (isAlive) { - assert.isTrue(isAlive); - if (media.playerState === chrome.cast.media.PlayerState.IDLE) { - assert.oneOf(media.idleReason, - [chrome.cast.media.IdleReason.INTERRUPTED, chrome.cast.media.IdleReason.FINISHED]); - called(stopped); - } - if (media.currentItemId !== currentItemId) { - session.removeMediaListener(mediaListener); - media.removeUpdateListener(listener); - utils.testMediaProperties(media); - called(update); - } - }); - }); - it('session.leave should leave the session', function (done) { - utils.setAction('Follow instructions on secondary.', 'Leave Session', function () { - // Set up the expected calls - var called = utils.callOrder([ - { id: success, repeats: false }, - { id: update, repeats: true } - ], function () { - done(); - }); - var finished = false; - session.addUpdateListener(function listener (isAlive) { - if (finished) { - return; - } - assert.isTrue(isAlive); - if (session.status === chrome.cast.SessionStatus.DISCONNECTED) { - finished = true; - called(update); - } - }); - session.leave(function () { - called(success); - }, function (err) { - assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); - }); - }); - }); - after('Ensure we have left the session', function (done) { - if (!session) { - return done(); - } - session.leave(function () { - done(); - }, function () { - done(); - }); - }); - }); - }); - window['cordova-plugin-chromecast-tests'] = window['cordova-plugin-chromecast-tests'] || {}; - window['cordova-plugin-chromecast-tests'].runMocha = function () { - var runner = mocha.run(); - runner.on('suite end', function (suite) { - var passed = this.stats.passes === runner.total; - if (passed) { - utils.setAction('1. On secondary, click "Check Session"
Then follow directions on secondary!'); - document.getElementById('action').style.backgroundColor = '#ceffc4'; - } - }); - return runner; - }; - }()); diff --git a/tests/www/js/tests_manual_primary_2.js b/tests/www/js/tests_manual_primary_2.js deleted file mode 100644 index ea8e123..0000000 --- a/tests/www/js/tests_manual_primary_2.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * The order of these tests and this.bail(true) is very important. - * - * Rather than nesting deep with describes and before's we just ensure the - * tests occur in the correct order. - * The major advantage to this is not having to repeat test code frequently - * making the suite slow. - * - */ - -(function () { - 'use strict'; - /* eslint-env mocha */ - /* global chrome */ - - var assert = window.chai.assert; - var utils = window['cordova-plugin-chromecast-tests'].utils; - var isDesktop = window['cordova-plugin-chromecast-tests'].isDesktop || false; - - mocha.setup({ - bail: true, - ui: 'bdd', - useColors: true, - reporter: window['cordova-plugin-chromecast-tests'].customHtmlReporter, - slow: 10000, - timeout: 180000 - }); - - describe('Manual Tests - Primary Device - Part 2', function () { - // callOrder constants that are re-used frequently - var success = 'success'; - var session; - - before('Api should be available and initialize successfully', function (done) { - this.timeout(15000); - session = null; - var interval = setInterval(function () { - if (chrome && chrome.cast && chrome.cast.isAvailable) { - clearInterval(interval); - done(); - } - }, 100); - }); - describe('App restart and reload/change page simulation', function () { - var cookieName = 'primary-p2_restart-reload'; - var runningNum = parseInt(utils.getValue(cookieName) || '0'); - it('Should not receive a session on initialize after a page change', function (done) { - this.timeout(15000); - if (runningNum > 0) { - // Just pass the test because we need to skip ahead - return done(); - } - utils.setAction('Checking for session after page load, (should not find session)...'); - var finished = false; // Need this so we stop testing after being finished - var unavailable = 'unavailable'; - var available = 'available'; - var called = utils.callOrder([ - { id: success, repeats: false }, - { id: unavailable, repeats: true }, - { id: available, repeats: true } - ], function () { - finished = true; - // Give it an extra moment to check for the session - setTimeout(function () { - utils.storeValue(cookieName, ++runningNum); - done(); - }, 1000); - }); - var apiConfig = new chrome.cast.ApiConfig( - new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), - function (sess) { - session = sess; - if (!isDesktop) { - assert.fail('should not receive a session (make sure there is no active cast session when starting the tests)'); - } - }, function receiverListener (availability) { - if (!finished) { - called(availability); - } - }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); - chrome.cast.initialize(apiConfig, function () { - called(success); - }, function (err) { - assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); - }); - }); - it('Should not receive a session on initialize after app restart', function (done) { - var instructionNum = 1; - var testNum = 2; - assert.isAtLeast(runningNum, instructionNum, 'Should not be running this test yet'); - switch (runningNum) { - case instructionNum: - // Show instructions for app restart - utils.storeValue(cookieName, testNum); - if (isDesktop) { - // If desktop, just reload the page (because restart doesn't work) - window.location.reload(); - } - this.timeout(0); // no timeout - utils.setAction('Force kill and restart the app, and navigate back to Manual Tests (Primary) Part 2.' - + '
Note: Android 4.4 does not support this feature, so just refresh the page.'); - break; - case testNum: - this.timeout(15000); - // Test initialize since we just reloaded - utils.setAction('Checking for session after app restart, (should not find session)...'); - var finished = false; // Need this so we stop testing after being finished - var unavailable = 'unavailable'; - var available = 'available'; - var called = utils.callOrder([ - { id: success, repeats: false }, - { id: unavailable, repeats: true }, - { id: available, repeats: true } - ], function () { - finished = true; - // Give it an extra moment to check for the session - setTimeout(function () { - utils.storeValue(cookieName, ++runningNum); - done(); - }, 1000); - }); - var apiConfig = new chrome.cast.ApiConfig( - new chrome.cast.SessionRequest(chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID), - function (sess) { - session = sess; - if (!isDesktop) { - assert.fail('should not receive a session (make sure there is no active cast session when starting the tests)'); - } - }, function receiverListener (availability) { - if (!finished) { - called(availability); - } - }, chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED); - chrome.cast.initialize(apiConfig, function () { - called(success); - }, function (err) { - assert.fail('Unexpected Error: ' + err.code + ': ' + err.description); - }); - break; - default: - // We must be looking to run a test further down the line - return done(); - } - }); - after(function () { - // Reset tests - utils.storeValue(cookieName, 0); - }); - }); - describe('session interaction with secondary', function () { - it('Create session', function (done) { - utils.setAction('On secondary click "Start Part 2".', 'Enter Session', function () { - utils.startSession(function (sess) { - session = sess; - utils.testSessionProperties(session); - utils.setAction('On secondary click "Continue".'); - done(); - }); - }); - }); - it('External session.stop should kill this session as well', function (done) { - session.addUpdateListener(function listener (isAlive) { - if (session.status === chrome.cast.SessionStatus.STOPPED) { - assert.isFalse(isAlive); - session.removeUpdateListener(listener); - done(); - } - }); - }); - }); - after('Ensure we have stopped the session', function (done) { - if (!session) { - return done(); - } - session.stop(function () { - done(); - }, function () { - done(); - }); - }); - - }); - - window['cordova-plugin-chromecast-tests'] = window['cordova-plugin-chromecast-tests'] || {}; - window['cordova-plugin-chromecast-tests'].runMocha = function () { - var runner = mocha.run(); - runner.on('suite end', function (suite) { - var passed = this.stats.passes === runner.total; - if (passed) { - utils.setAction('All manual tests passed! [Assuming you did Part 1 as well :) ]'); - document.getElementById('action').style.backgroundColor = '#ceffc4'; - } - }); - return runner; - }; - -}()); diff --git a/tests/www/js/custom_mocha_html_reporter.js b/tests/www/lib/custom_mocha_html_reporter.js similarity index 70% rename from tests/www/js/custom_mocha_html_reporter.js rename to tests/www/lib/custom_mocha_html_reporter.js index 110879b..015e418 100644 --- a/tests/www/js/custom_mocha_html_reporter.js +++ b/tests/www/lib/custom_mocha_html_reporter.js @@ -6,6 +6,9 @@ 'use strict'; /* eslint-env mocha */ + var Mocha = window.Mocha; + var utils = window['cordova-plugin-chromecast-tests'].utils; + // Save htmlReporter reference var htmlReporter = mocha._reporter; @@ -13,7 +16,7 @@ // with linking to source for quick debugging in dev tools var myReporter = function (runner, options) { // Add the error listener - runner.on('fail', function (test, err) { + runner.on(Mocha.Runner.constants.EVENT_TEST_FAIL, function (test, err) { // Need to add the full code location path // so that the debugger can link to it @@ -42,6 +45,20 @@ // Log the error console.error(lines.join('\n')); }); + + // Custom suite end + runner.on(Mocha.Runner.constants.EVENT_SUITE_END, function (test, err) { + // Include pending as a passed test because those are skipped tests (usually) + var passed = (this.stats.passes + this.stats.pending) === this.total; + if (passed) { + utils.setAction('All tests passed!'); + document.getElementById('action').style.backgroundColor = '#ceffc4'; + } else if (this.stats.failures) { + utils.setAction('Test failed. :('); + document.getElementById('action').style.backgroundColor = '#e28282'; + } + }); + // And return the default HTML reporter htmlReporter.call(this, runner, options); }; diff --git a/tests/www/lib/mediaGenerateAndAssert.js b/tests/www/lib/mediaGenerateAndAssert.js new file mode 100644 index 0000000..77f40ac --- /dev/null +++ b/tests/www/lib/mediaGenerateAndAssert.js @@ -0,0 +1,184 @@ +/** + * Utility functions for creating media requests and checking returned media + * states. + */ + +(function () { + 'use strict'; + /* eslint-env mocha */ + /* global chrome */ + var assert = window.chai.assert; + + var audioUrl = 'https://ia800306.us.archive.org/26/items/1939RadioNews/1939-10-24-CBS-Elmer-Davis-Reports-City-Of-Flint-Still-Missing.mp3'; + var imageUrl = 'https://ia800705.us.archive.org/1/items/GoodHousekeeping193810/Good%20Housekeeping%201938-10.jpg'; + var liveAudioUrl = 'http://relay.publicdomainproject.org/classical.mp3'; + var videoUrl = 'https://ia801302.us.archive.org/1/items/TheWater_201510/TheWater.mp4'; + + var mediaUtils = { + CONTENT_TYPE: { + 'VIDEO': function () { + return new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4'); + }, + 'AUDIO': function () { + return new chrome.cast.media.MediaInfo(audioUrl, 'audio/mpeg'); + }, + 'IMAGE': function () { + return new chrome.cast.media.MediaInfo(imageUrl, 'image/jpeg'); + }, + 'LIVE_AUDIO': function () { + return new chrome.cast.media.MediaInfo(liveAudioUrl, 'audio/mpeg'); + } + } + }; + + /** + * Returns a new media item for use in requests. + * + * @param {*} contentType - Must be a string matching one of + * mediaUtils.CONTENT_TYPE or a chrome.cast.media.MediaInfo object. + * @param {chrome.cast.media.*Metadata} metadataType - (optional) Must be a + * chrome.cast.media.*Metadata object, or null. + * @param {Date} metadataDate - (optional) Used for any metadata fields where + * you would prefer the date to be constant. + */ + mediaUtils.getMediaInfoItem = function (contentType, metadataType, metadataDate) { + // Get the content + if (mediaUtils.CONTENT_TYPE[contentType]) { + contentType = mediaUtils.CONTENT_TYPE[contentType](); + } else { + assert.instanceOf(contentType, chrome.cast.cordova.MediaInfo); + } + // Get the metadata + if (metadataType !== undefined && metadataType !== null) { + contentType.metadata = generateMetadata(metadataType, metadataDate); + } + return contentType; + }; + + /** + * Asserts that the 2 MediaInfo items are equivalent. + * @param {chrome.cast.media.MediaInfo} actual + * @param {chrome.cast.media.MediaInfo} expected + */ + mediaUtils.assertMediaInfoItemEquals = function (actual, expected) { + // Test MediaInfo direct properties + assert.equal(actual.contentId, expected.contentId); + if (!actual.metadata && !expected.metadata) { + return; // No metadata to check + } + // Test common *Metadata properties + assert.equal(actual.metadata.images[0].url, expected.metadata.images[0].url); + assert.equal(actual.metadata.metadataType, expected.metadata.metadataType); + assert.equal(actual.metadata.queueItemId, expected.metadata.queueItemId); + assert.equal(actual.metadata.sectionDuration, expected.metadata.sectionDuration); + assert.equal(actual.metadata.sectionStartAbsoluteTime, expected.metadata.sectionStartAbsoluteTime); + assert.equal(actual.metadata.sectionStartTimeInContainer, expected.metadata.sectionStartTimeInContainer); + assert.equal(actual.metadata.sectionStartTimeInMedia, expected.metadata.sectionStartTimeInMedia); + assert.equal(actual.metadata.title, expected.metadata.title); + assert.equal(actual.metadata.type, expected.metadata.type); + assert.equal(actual.metadata.xMyMadeUpMetadata, expected.metadata.xMyMadeUpMetadata); + // Test unique Metadata properties + switch (actual.metadata.type) { + case chrome.cast.media.MetadataType.AUDIOBOOK_CHAPTER: + assert.equal(actual.metadata.bookTitle, expected.metadata.bookTitle); + assert.equal(actual.metadata.chapterNumber, expected.metadata.chapterNumber); + assert.equal(actual.metadata.chapterTitle, expected.metadata.chapterTitle); + assert.equal(actual.metadata.subtitle, expected.metadata.subtitle); + break; + case chrome.cast.media.MetadataType.GENERIC: + assert.equal(actual.metadata.releaseDate, expected.metadata.releaseDate); + assert.equal(actual.metadata.releaseDate, expected.metadata.releaseDate); + break; + case chrome.cast.media.MetadataType.MOVIE: + assert.equal(actual.metadata.studio, expected.metadata.studio); + break; + case chrome.cast.media.MetadataType.MUSIC_TRACK: + assert.equal(actual.metadata.albumArtist, expected.metadata.albumArtist); + assert.equal(actual.metadata.albumName, expected.metadata.albumName); + assert.equal(actual.metadata.artist, expected.metadata.artist); + assert.equal(actual.metadata.composer, expected.metadata.composer); + assert.equal(actual.metadata.songName, expected.metadata.songName); + assert.equal(actual.metadata.releaseDate, expected.metadata.releaseDate); + break; + case chrome.cast.media.MetadataType.PHOTO: + assert.equal(actual.metadata.artist, expected.metadata.artist); + assert.equal(actual.metadata.height, expected.metadata.height); + assert.equal(actual.metadata.creationDateTime, expected.metadata.creationDateTime); + assert.equal(actual.metadata.latitude, expected.metadata.latitude); + assert.equal(actual.metadata.location, expected.metadata.location); + assert.equal(actual.metadata.longitude, expected.metadata.longitude); + assert.equal(actual.metadata.width, expected.metadata.width); + break; + case chrome.cast.media.MetadataType.TV_SHOW: + assert.equal(actual.metadata.episode, expected.metadata.episode); + assert.equal(actual.metadata.originalAirDate, expected.metadata.originalAirDate); + assert.equal(actual.metadata.season, expected.metadata.season); + assert.equal(actual.metadata.seriesTitle, expected.metadata.seriesTitle); + assert.equal(actual.metadata.subtitle, expected.metadata.subtitle); + break; + default: + assert.fail('Unknown metadata type: "' + actual.metadata.type + '"'); + } + }; + + function generateMetadata (metadataType, metadataDate) { + var metadata; + metadataDate = (metadataDate && metadataDate.valueOf()) || new Date().valueOf(); + switch (metadataType) { + case chrome.cast.media.MetadataType.AUDIOBOOK_CHAPTER: + metadata = new chrome.cast.media.AudiobookChapterMediaMetadata(); + metadata.bookTitle = 'AudiobookBookTitle'; + metadata.chapterNumber = 12; + metadata.chapterTitle = 'AudiobookChapterTitle'; + metadata.subtitle = 'AudiobookSubtitle'; + break; + case chrome.cast.media.MetadataType.GENERIC: + metadata = new chrome.cast.media.GenericMediaMetadata(); + metadata.releaseDate = metadataDate; + metadata.subtitle = 'GenericSubtitle'; + break; + case chrome.cast.media.MetadataType.MOVIE: + metadata = new chrome.cast.media.MovieMediaMetadata(); + metadata.studio = 'MovieStudio'; + metadata.subtitle = 'MovieSubtitle'; + break; + case chrome.cast.media.MetadataType.MUSIC_TRACK: + metadata = new chrome.cast.media.MusicTrackMediaMetadata(); + metadata.albumArtist = 'MusicAlbumArtist'; + metadata.albumName = 'MusicAlbum'; + metadata.artist = 'MusicArtist'; + metadata.composer = 'MusicComposer'; + metadata.releaseDate = metadataDate; + metadata.songName = 'MusicSongName'; + break; + case chrome.cast.media.MetadataType.PHOTO: + metadata = new chrome.cast.media.PhotoMediaMetadata(); + metadata.artist = 'PhotoArtist'; + metadata.height = 100; + metadata.creationDateTime = metadataDate; + metadata.latitude = 102.13; + metadata.location = 'PhotoLocation'; + metadata.longitude = 101.12; + metadata.width = 100; + break; + case chrome.cast.media.MetadataType.TV_SHOW: + metadata = new chrome.cast.media.TvShowMediaMetadata(); + metadata.episode = 15; + metadata.originalAirDate = metadataDate; + metadata.season = 2; + metadata.seriesTitle = 'TvSeries'; + metadata.subtitle = 'TvSubtitle'; + break; + default: + assert.fail('Unknown metadata type: "' + metadataType + '"'); + } + // Add common metadata + metadata.images = [new chrome.cast.Image(imageUrl)]; + metadata.title = 'Title-' + metadata.type; + metadata.xMyMadeUpMetadata = 'MyMadeUpMetadata-' + metadata.type; + return metadata; + } + + window['cordova-plugin-chromecast-tests'] = window['cordova-plugin-chromecast-tests'] || {}; + window['cordova-plugin-chromecast-tests'].mediaUtils = mediaUtils; +}()); diff --git a/tests/www/lib/test_starter.js b/tests/www/lib/test_starter.js new file mode 100644 index 0000000..80e0dc7 --- /dev/null +++ b/tests/www/lib/test_starter.js @@ -0,0 +1,63 @@ +(function () { + 'use strict'; + + // This starts the tests for Android/iOS + document.addEventListener('deviceready', function () { + runTests(); + }); + + // This starts the tests for desktop chrome + window['__onGCastApiAvailable'] = function (isAvailable, err) { + // If error, it is probably because we are not on desktop chrome + if (err || !isAvailable) { + // So try loading mobile + return loadMobile(); + } + // Else we are likely on chrome desktop + if (isAvailable) { + addScriptToPage('../chrome/cordova_stubs.js'); + runTests(); + } + }; + + // Assume we are on Desktop to start + window['cordova-plugin-chromecast-tests'].isDesktop = true; + + // Url should match below if we are testing on mobile + if (window.location.href.match(/plugins\/cordova-plugin-chromecast/)) { + loadMobile(); + } else { + // Assume we are on desktop and attempt to load the cast library + addScriptToPage( + 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=0', + function onerror () { + // If failed to load, we are probably on mobile + loadMobile(); + }); + } + + function loadMobile () { + // The assumption that we were on desktop chrome was wrong apparently + window['cordova-plugin-chromecast-tests'].isDesktop = false; + addScriptToPage('../../../../cordova.js'); + } + + function runTests () { + var runner = window.mocha.run(); + // This makes it so that tests actually fail in the case of + // uncaught exceptions inside promise catch blocks + window.addEventListener('unhandledrejection', function (event) { + runner.fail(runner.test || runner.currentRunnable, event.reason); + }); + } + + function addScriptToPage (src, errorCallback) { + var s = document.createElement('script'); + if (errorCallback) { + s.onerror = errorCallback; + } + s.setAttribute('src', src); + document.body.appendChild(s); + } + +}()); diff --git a/tests/www/js/utils.js b/tests/www/lib/utils.js similarity index 95% rename from tests/www/js/utils.js rename to tests/www/lib/utils.js index 6cafdc7..5ed8cd2 100644 --- a/tests/www/js/utils.js +++ b/tests/www/lib/utils.js @@ -31,6 +31,10 @@ localStorage.clear(); }; + utils.isDesktop = function () { + return window['cordova-plugin-chromecast-tests'].isDesktop || false; + }; + /** * Displays the action information. */ @@ -275,14 +279,14 @@ assert.isFunction(session.loadMedia); }; - utils.testMediaProperties = function (media) { + utils.testMediaProperties = function (media, isLiveStream) { assert.instanceOf(media, chrome.cast.media.Media); assert.isNumber(media.currentItemId); assert.isNumber(media.currentTime); if (media.idleReason) { assert.oneOf(media.idleReason, utils.getObjectValues(chrome.cast.media.IdleReason)); } - utils.testMediaInfoProperties(media.media); + utils.testMediaInfoProperties(media.media, isLiveStream); assert.isNumber(media.mediaSessionId); assert.isNumber(media.playbackRate); assert.oneOf(media.playerState, utils.getObjectValues(chrome.cast.media.PlayerState)); @@ -294,14 +298,19 @@ assert.isFunction(media.removeUpdateListener); }; - utils.testMediaInfoProperties = function (mediaInfo) { + utils.testMediaInfoProperties = function (mediaInfo, isLiveStream) { // queue items contain a subset of identical properties utils.testQueueItemMediaInfoProperties(mediaInfo); // properties that are exclusive (or mandatory) to media.media - assert.isNumber(mediaInfo.duration); - if (mediaInfo.contentType.toLowerCase().indexOf('video') > -1 - || mediaInfo.contentType.toLowerCase().indexOf('audio') > -1) { - assert.isAbove(mediaInfo.duration, 0); + if (isLiveStream) { + // Live stream has null duration + assert.isNull(mediaInfo.duration); + } else { + assert.isNumber(mediaInfo.duration); + if (mediaInfo.contentType.toLowerCase().indexOf('video') > -1 + || mediaInfo.contentType.toLowerCase().indexOf('audio') > -1) { + assert.isAbove(mediaInfo.duration, 0); + } } assert.isArray(mediaInfo.tracks); }; diff --git a/tests/www/lib/chai.js b/tests/www/vendor/chai.js similarity index 100% rename from tests/www/lib/chai.js rename to tests/www/vendor/chai.js diff --git a/tests/www/lib/mocha.css b/tests/www/vendor/mocha.css similarity index 100% rename from tests/www/lib/mocha.css rename to tests/www/vendor/mocha.css diff --git a/tests/www/lib/mocha.js b/tests/www/vendor/mocha.js similarity index 100% rename from tests/www/lib/mocha.js rename to tests/www/vendor/mocha.js diff --git a/tests/www/lib/readme.md b/tests/www/vendor/readme.md similarity index 100% rename from tests/www/lib/readme.md rename to tests/www/vendor/readme.md diff --git a/www/chrome.cast.js b/www/chrome.cast.js index ec07f16..0d6cc5c 100644 --- a/www/chrome.cast.js +++ b/www/chrome.cast.js @@ -362,6 +362,21 @@ chrome.cast = { this.customData = null; }, + /** + * An audiobook chapter description. + * @property {string} bookTitle Audiobook title. + * @property {number} chapterNumber Chapter number, used for display purposes. + * @property {string} chapterTitle Chapter title. + * @property {chrome.cast.Image[]} images Content images. + * @property {string} subtitle Content subtitle. + * @property {string} title Content title. + * @property {chrome.cast.media.MetadataType} type The type of metadata. + */ + AudiobookChapterMediaMetadata: function GenericMediaMetadata () { + this.metadataType = this.type = chrome.cast.media.MetadataType.AUDIOBOOK_CHAPTER; + this.bookTitle = this.chapterNumber = this.chapterTitle = this.images = this.subtitle = this.title = undefined; + }, + /** * A generic media description. * @property {chrome.cast.Image[]} images Content images. @@ -561,16 +576,6 @@ chrome.cast.requestSession = function (successCallback, errorCallback, opt_sessi }); }; -/** - * Sets custom receiver list - * @param {chrome.cast.Receiver[]} receivers The new list. Must not be null. - * @param {function} successCallback - * @param {function} errorCallback - */ -chrome.cast.setCustomReceivers = function (receivers, successCallback, errorCallback) { - // TODO: Implement -}; - /** * Describes the state of a currently running Cast application. Normally, these objects should not be created by the client. * @param {string} sessionId Uniquely identifies this instance of the receiver application.