+---
+
+### 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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.
-
- 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.
-