Skip to content

Commit

Permalink
SessionManager.startSession did not work reliably on android 4.4.2, s…
Browse files Browse the repository at this point in the history
…o switch to MediaRouter.selectRoute()

Add isNearbyDevice to routes output from startRouteScan (mostly so testing can avoid attempting to join these routes, since they require manual input.  But could be useful to the user as well.)
Remove unnecessary chrome.cast.js global vars.
Added polyfill for Promise to tests since Android 4.4.3- does not have Promise natively.
Contributing to Issue jellyfin-archive#36
Relevant to Issue #22
  • Loading branch information
Lindsay-Needs-Sleep committed Sep 15, 2019
1 parent eaf54c1 commit 125f37f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 38 deletions.
8 changes: 7 additions & 1 deletion src/android/Chromecast.java
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ void onRouteUpdate(List<RouteInfo> routes) {
callbackContext.sendPluginResult(pluginResult);
}
};
connection.scanForRoutes(clientScan);
connection.scanForRoutes(null, clientScan, null);

return true;
}
Expand Down Expand Up @@ -438,6 +438,12 @@ private JSONArray routesToJSON(List<RouteInfo> routes) {
JSONObject obj = new JSONObject();
obj.put("name", route.getName());
obj.put("id", route.getId());

CastDevice device = CastDevice.getFromBundle(route.getExtras());
if (device != null) {
obj.put("isNearbyDevice", !device.isOnLocalNetwork());
}

routesArray.put(obj);
} catch (JSONException e) {
}
Expand Down
84 changes: 72 additions & 12 deletions src/android/ChromecastConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
Expand Down Expand Up @@ -201,15 +200,51 @@ public void run() {
callback.onJoin(session);
return;
}
listenForConnection(callback);

Intent castIntent = new Intent();
castIntent.putExtra("CAST_INTENT_TO_CAST_ROUTE_ID_KEY", routeId);
// RouteName and toast are just for display
castIntent.putExtra("CAST_INTENT_TO_CAST_DEVICE_NAME_KEY", routeName);
castIntent.putExtra("CAST_INTENT_TO_CAST_NO_TOAST_KEY", false);
// We need this hack so that we can access the foundRoute value
// Without having to store it as a global variable.
// Just always access first element
final boolean[] foundRoute = {false};

listenForConnection(callback);

getSessionManager().startSession(castIntent);
// We need to start an active scan because getMediaRouter().getRoutes() may be out
// of date. Also, maintaining a list of known routes doesn't work. It is possible
// to have a route in your "known" routes list, but is not in
// 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
// 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.
ScanCallback scan = new ScanCallback() {
@Override
void onRouteUpdate(List<RouteInfo> routes) {
// Look for the matching route
for (RouteInfo route : routes) {
if (!foundRoute[0] && route.getId().equals(routeId)) {
// Found the route!
foundRoute[0] = true;
// So stop the scan
stopScan(this);
// And select it!
getMediaRouter().selectRoute(route);
}
}
}
};
scanForRoutes(5000L, scan, new Runnable() {
@Override
public void run() {
// If we were not able to find the route
if (!foundRoute[0]) {
stopScan(scan);
callback.onError("TIMEOUT Could not find active route with id: "
+ routeId + " after 5s.");
}
}
});
}
});
}
Expand Down Expand Up @@ -308,9 +343,13 @@ public void onSessionStartFailed(CastSession castSession, int errCode) {
/**
* Starts listening for receiver updates.
* Must call stopScan(callback) or the battery will drain with non-stop active scanning.
* @param timeout ms until the scan automatically stops,
* if 0 only calls callback.onRouteUpdate once with the currently known routes
* if null, will scan until stopScan is called
* @param callback the callback to receive route updates on
* @param onTimeout called when the timeout hits
*/
public void scanForRoutes(ScanCallback callback) {
public void scanForRoutes(Long timeout, ScanCallback callback, Runnable onTimeout) {
// Add the callback in active scan mode
activity.runOnUiThread(new Runnable() {
public void run() {
Expand All @@ -319,12 +358,31 @@ public void run() {
// Send out the initial routes
callback.onFilteredRouteUpdate();

if (timeout != null && timeout == 0) {
return;
}

// Add the callback in active scan mode
getMediaRouter().addCallback(new MediaRouteSelector.Builder()
.addControlCategory(CastMediaControlIntent.categoryForCast(appId))
.build(),
callback,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);

if (timeout != null) {
// remove the callback after timeout ms, and notify caller
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// And stop the scan for routes
getMediaRouter().removeCallback(callback);
// Notify
if (onTimeout != null) {
onTimeout.run();
}
}
}, timeout);
}
}
});
}
Expand Down Expand Up @@ -440,10 +498,9 @@ private void onFilteredRouteUpdate() {
if (stopped || mediaRouter == null) {
return;
}
List<RouteInfo> routes = mediaRouter.getRoutes();
List<RouteInfo> outRoutes = new ArrayList<>();
// Filter the routes
for (RouteInfo route : routes) {
for (RouteInfo route : mediaRouter.getRoutes()) {
// We don't want default routes, or duplicate active routes
// or multizone duplicates https://github.com/jellyfin/cordova-plugin-chromecast/issues/32
Bundle extras = route.getExtras();
Expand All @@ -453,7 +510,10 @@ private void onFilteredRouteUpdate() {
continue;
}
}
if (!route.isDefault() && !route.getDescription().equals("Google Cast Multizone Member")) {
if (!route.isDefault()
&& !route.getDescription().equals("Google Cast Multizone Member")
&& route.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_REMOTE
) {
outRoutes.add(route);
}
}
Expand Down
44 changes: 31 additions & 13 deletions tests/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
* The order of the tests is very important!
* Unfortunately using nested describes and beforeAll does not work correctly.
* So just be careful with the order of tests!
* Edit: TODO We should really switch to mocha.
*/

// We need a promise polyfill for Android < 4.4.3
// from https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js
/*eslint-disable */
!(function (e, n) { typeof exports === 'object' && typeof module !== 'undefined' ? n() : typeof define === 'function' && define.amd ? define(n) : n(); }(0, function () { 'use strict'; function e (e) { var n = this.constructor; return this.then(function (t) { return n.resolve(e()).then(function () { return t; }); }, function (t) { return n.resolve(e()).then(function () { return n.reject(t); }); }); } function n (e) { return !(!e || typeof e.length === 'undefined'); } function t () {} function o (e) { if (!(this instanceof o)) throw new TypeError('Promises must be constructed via new'); if (typeof e !== 'function') throw new TypeError('not a function'); this._state = 0, this._handled = !1, this._value = undefined, this._deferreds = [], c(e, this); } function r (e, n) { for (;e._state === 3;)e = e._value; e._state !== 0 ? (e._handled = !0, o._immediateFn(function () { var t = e._state === 1 ? n.onFulfilled : n.onRejected; if (t !== null) { var o; try { o = t(e._value); } catch (r) { return void f(n.promise, r); }i(n.promise, o); } else (e._state === 1 ? i : f)(n.promise, e._value); })) : e._deferreds.push(n); } function i (e, n) { try { if (n === e) throw new TypeError('A promise cannot be resolved with itself.'); if (n && (typeof n === 'object' || typeof n === 'function')) { var t = n.then; if (n instanceof o) return e._state = 3, e._value = n, void u(e); if (typeof t === 'function') return void c((function (e, n) { return function () { e.apply(n, arguments); }; }(t, n)), e); }e._state = 1, e._value = n, u(e); } catch (r) { f(e, r); } } function f (e, n) { e._state = 2, e._value = n, u(e); } function u (e) { e._state === 2 && e._deferreds.length === 0 && o._immediateFn(function () { e._handled || o._unhandledRejectionFn(e._value); }); for (var n = 0, t = e._deferreds.length; t > n; n++)r(e, e._deferreds[n]); e._deferreds = null; } function c (e, n) { var t = !1; try { e(function (e) { t || (t = !0, i(n, e)); }, function (e) { t || (t = !0, f(n, e)); }); } catch (o) { if (t) return; t = !0, f(n, o); } } var a = setTimeout; o.prototype['catch'] = function (e) { return this.then(null, e); }, o.prototype.then = function (e, n) { var o = new this.constructor(t); return r(this, new function (e, n, t) { this.onFulfilled = typeof e === 'function' ? e : null, this.onRejected = typeof n === 'function' ? n : null, this.promise = t; }(e, n, o)), o; }, o.prototype['finally'] = e, o.all = function (e) { return new o(function (t, o) { function r (e, n) { try { if (n && (typeof n === 'object' || typeof n === 'function')) { var u = n.then; if (typeof u === 'function') return void u.call(n, function (n) { r(e, n); }, o); }i[e] = n, --f == 0 && t(i); } catch (c) { o(c); } } if (!n(e)) return o(new TypeError('Promise.all accepts an array')); var i = Array.prototype.slice.call(e); if (i.length === 0) return t([]); for (var f = i.length, u = 0; i.length > u; u++)r(u, i[u]); }); }, o.resolve = function (e) { return e && typeof e === 'object' && e.constructor === o ? e : new o(function (n) { n(e); }); }, o.reject = function (e) { return new o(function (n, t) { t(e); }); }, o.race = function (e) { return new o(function (t, r) { if (!n(e)) return r(new TypeError('Promise.race accepts an array')); for (var i = 0, f = e.length; f > i; i++)o.resolve(e[i]).then(t, r); }); }, o._immediateFn = typeof setImmediate === 'function' && function (e) { setImmediate(e); } || function (e) { a(e, 0); }, o._unhandledRejectionFn = function (e) { void 0 !== console && console && console.warn('Possible Unhandled Promise Rejection:', e); }; var l = (function () { if (typeof self !== 'undefined') return self; if (typeof window !== 'undefined') return window; if (typeof global !== 'undefined') return global; throw Error('unable to locate global object'); }()); 'Promise' in l ? l.Promise.prototype['finally'] || (l.Promise.prototype['finally'] = e) : l.Promise = o; }));
/*eslint-enable */

/* eslint-env jasmine */
exports.defineAutoTests = function () {
/* eslint-disable no-undef */

jasmine.DEFAULT_TIMEOUT_INTERVAL = 9000;

var appId = chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID;
var videoUrl = 'https://archive.org/download/CosmosLaundromatFirstCycle/Cosmos%20Laundromat%20-%20First%20Cycle%20%281080p%29.mp4'
var videoUrl = 'https://archive.org/download/CosmosLaundromatFirstCycle/Cosmos%20Laundromat%20-%20First%20Cycle%20%281080p%29.mp4';

describe('chrome.cast', function () {

Expand Down Expand Up @@ -628,46 +635,57 @@ exports.defineAutoTests = function () {

function startRouteScan () {
return new Promise(function (resolve) {
var once = true;
var finished = false;
chrome.cast.cordova.startRouteScan(function routeUpdate (routes) {
if (once && routes.length > 0) {
once = false;

var route = routes[0];
if (finished) {
return;
}
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
test(route).toBeInstanceOf(chrome.cast.cordova.Route);
test(route.id).toBeDefined();
test(route.name).toBeDefined();

test(route.isNearbyDevice).toBeDefined();
if (!route.isNearbyDevice) {
finished = true;
}
}
if (finished) {
resolve(routes);
}
}, function (err) {
fail(err.code + ': ' + err.description);
resolve();
});
});
}

function stopRouteScan (routes) {
function stopRouteScan (arg) {
return new Promise(function (resolve) {
// Make sure we can stop the scan
chrome.cast.cordova.stopRouteScan(function () {
resolve(routes);
resolve(arg);
}, function (err) {
test().fail(err.code + ': ' + err.description);
resolve();
});
});
}

function selectRoute (routes) {
return new Promise(function (resolve) {
chrome.cast.cordova.selectRoute(routes[0], function (session) {
// Find a non-nearby device so that the join is automatic
var route;
for (var i = 0; i < routes.length; i++) {
route = routes[i];
if (!route.isNearbyDevice) {
break;
}
}
chrome.cast.cordova.selectRoute(route, function (session) {
Promise.resolve(session)
.then(sessionProperties)
.then(resolve);
}, function (err) {
fail(err.code + ': ' + err.description);
resolve(routes);
});
});
}
Expand Down
21 changes: 9 additions & 12 deletions www/chrome.cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,6 @@ chrome.cast = {
}
};

var _sessionRequest = null;
var _autoJoinPolicy = null;
var _defaultActionPolicy = null;
var _sessionListener = function () {};
var _receiverListener = function () {};

Expand All @@ -545,14 +542,12 @@ chrome.cast.initialize = function (apiConfig, successCallback, errorCallback) {
return;
}

_autoJoinPolicy = apiConfig.autoJoinPolicy;
_defaultActionPolicy = apiConfig.defaultActionPolicy;
_sessionRequest = apiConfig.sessionRequest;
_sessionListener = apiConfig.sessionListener;
_receiverListener = apiConfig.receiverListener;

execute('initialize', _sessionRequest.appId, _autoJoinPolicy, _defaultActionPolicy, function (err) {
execute('initialize', apiConfig.sessionRequest.appId, apiConfig.autoJoinPolicy, apiConfig.defaultActionPolicy, function (err) {
if (!err) {
// Don't set the listeners config until success
_sessionListener = apiConfig.sessionListener;
_receiverListener = apiConfig.receiverListener;

successCallback();
chrome.cast._.receiverUpdate(false);
} else {
Expand Down Expand Up @@ -1139,7 +1134,8 @@ chrome.cast.cordova = {
execute('startRouteScan', function (err, routes) {
if (!err) {
for (var i = 0; i < routes.length; i++) {
routes[i] = new chrome.cast.cordova.Route(routes[i].id, routes[i].name);
var route = routes[i];
routes[i] = new chrome.cast.cordova.Route(route.id, route.name, route.isNearbyDevice);
}
successCallback(routes);
} else {
Expand Down Expand Up @@ -1176,9 +1172,10 @@ chrome.cast.cordova = {
}
});
},
Route: function (id, name) {
Route: function (id, name, isNearbyDevice) {
this.id = id;
this.name = name;
this.isNearbyDevice = isNearbyDevice;
}
};

Expand Down

0 comments on commit 125f37f

Please sign in to comment.