From eff248012f3426cd4bd4f3898e66930acd85f239 Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Tue, 18 Jun 2024 09:25:30 +0200 Subject: [PATCH] fix: remove sharedPreferences and NSUserDefaults for storing the app id * Remove sharedPreferences and NSUserDefaults for storing the app id * Update readme and add warning for appId in JS API --- README.md | 97 ++++++++++++++++++++++++++- plugin.xml | 1 + src/android/CastOptionsProvider.java | 22 +++--- src/android/Chromecast.java | 5 +- src/android/ChromecastConnection.java | 64 ++---------------- src/ios/MLPChromecast.m | 66 +++--------------- www/chrome.cast.js | 6 +- 7 files changed, 133 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 858ec2e..fe0b72b 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,37 @@ # Installation -> Use a commit hash to prevent dependency hijacking +To install this fork, you can use the full GitHub URL with commit hash. Always try using a commit hash to prevent +installing malicious code. ``` cordova plugin add https://github.com/Videodock/cordova-plugin-chromecast.git#d82f07e26a53df6322043403e4dc98bb2eeb4e39 ``` -If you have trouble installing the plugin or running the project for iOS, from `/platforms/ios/` try running: +## Android + +Add the following metadata to your AndroidManifest.xml inside the `` tag. + +Replace `_APP_ID_` with your receiver application id. + +```xml + + + + ... + + + + +``` + + +## iOS + +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 @@ -50,6 +74,66 @@ The "*Description" key strings will be used when asking the user for permission ``` +1. In AppDelegate.m (or AppDelegate.swift) add + +``` +#import +``` + +```swift +import GoogleCast +``` + +and insert the following in the `application:didFinishLaunchingWithOptions` method, ideally at the beginning: + +``` +NSString *receiverAppID = kGCKDefaultMediaReceiverApplicationID; // or @"ABCD1234" +GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc] initWithApplicationID:receiverAppID]; +GCKCastOptions* options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria]; +[GCKCastContext setSharedInstanceWithOptions:options]; +``` + +```swift +let receiverAppID = kGCKDefaultMediaReceiverApplicationID // or "ABCD1234" +let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID) +let options = GCKCastOptions(discoveryCriteria: criteria) +GCKCastContext.setSharedInstanceWith(options) +``` + +If using a custom receiver, replace kGCKDefaultMediaReceiverApplicationID with your receiver app id. + +You can also automatically set the receiver ID by parsing the Bonjour Services string. This is useful when you have +multiple Info.plist files for different environments. + +```swift + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + initializeCastSender() + return true + } + + func initializeCastSender() { + if let value = Bundle.init(for: AppDelegate.self).infoDictionary?["NSBonjourServices"] as? [String] { + + if let rule = value.first(where: { $0.hasSuffix("._googlecast._tcp") }) { + let startIndex = rule.index(rule.startIndex, offsetBy: 1) + let endIndex = rule.index(rule.startIndex, offsetBy: 9) + let receiverAppID = String(rule[startIndex..._googlecast._tcp in the Array"); + } + } else { + print("Couldn't initialize Cast SDK, NSBonjourServices is missing from the Info.plist"); + } + } +``` + ## Chromecast Icon Assets [chromecast-assets.zip](https://github.com/jellyfin/cordova-plugin-chromecast/wiki/chromecast-assets.zip) @@ -86,6 +170,15 @@ document.addEventListener("deviceready", function () { }); ``` +The `SessionRequest#appId` property is not used to define the receiver app and can be set to an empty string to disable the warning. +Reason for this is that the Cast SDK can be initialized on app startup. Also, for iOS, the Info.plist must be configured with your receiver ID. This makes it easy to mis align the application ID in the initialization and native config. +```js +var sessionRequest = new chrome.cast.SessionRequest('A11B22C'); // doesn't use A11B22C (triggers console warning) +var sessionRequest = new chrome.cast.SessionRequest(''); // disables console warning + +var apiConfig = new chrome.cast.ApiConfig(sessionRequest); +``` + ### Example Usage Here is a simple [example](doc/example.js) that loads a video, pauses it, and ends the session. diff --git a/plugin.xml b/plugin.xml index d192f8d..7651789 100644 --- a/plugin.xml +++ b/plugin.xml @@ -32,6 +32,7 @@ + diff --git a/src/android/CastOptionsProvider.java b/src/android/CastOptionsProvider.java index 7d73110..df81fd8 100644 --- a/src/android/CastOptionsProvider.java +++ b/src/android/CastOptionsProvider.java @@ -2,31 +2,35 @@ import java.util.List; +import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.OptionsProvider; import com.google.android.gms.cast.framework.SessionProvider; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; public final class CastOptionsProvider implements OptionsProvider { - /** The app id. */ - private static String appId; + protected String getReceiverApplicationId(Context context) { + String appId = null; + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_META_DATA); + appId = packageInfo.applicationInfo.metaData.getString("com.google.android.gms.cast.framework.RECEIVER_APPLICATION_ID"); + } catch (PackageManager.NameNotFoundException e) { + } - /** - * Sets the app ID. - * @param applicationId appId - */ - public static void setAppId(String applicationId) { - appId = applicationId; + return appId != null ? appId : CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; } @Override public CastOptions getCastOptions(Context context) { return new CastOptions.Builder() - .setReceiverApplicationId(appId) + .setReceiverApplicationId(getReceiverApplicationId(context)) .build(); } + @Override public List getAdditionalSessionProviders(Context context) { return null; diff --git a/src/android/Chromecast.java b/src/android/Chromecast.java index ef61e7c..2627f37 100644 --- a/src/android/Chromecast.java +++ b/src/android/Chromecast.java @@ -171,14 +171,13 @@ public void run() { /** * Initialize all of the MediaRouter stuff with the AppId. * For now, ignore the autoJoinPolicy and defaultActionPolicy; those will come later - * @param appId The appId we're going to use for ALL session requests * @param autoJoinPolicy tab_and_origin_scoped | origin_scoped | page_scoped * @param defaultActionPolicy create_session | cast_this_tab * @param callbackContext called with .success or .error depending on the result * @return true for cordova */ - public boolean initialize(final String appId, String autoJoinPolicy, String defaultActionPolicy, final CallbackContext callbackContext) { - connection.initialize(appId, callbackContext); + public boolean initialize(String autoJoinPolicy, String defaultActionPolicy, final CallbackContext callbackContext) { + connection.initialize(callbackContext); return true; } diff --git a/src/android/ChromecastConnection.java b/src/android/ChromecastConnection.java index a475ecd..8345d2f 100644 --- a/src/android/ChromecastConnection.java +++ b/src/android/ChromecastConnection.java @@ -6,9 +6,11 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import androidx.arch.core.util.Function; import androidx.mediarouter.app.MediaRouteChooserDialog; +import androidx.mediarouter.media.MediaControlIntent; import androidx.mediarouter.media.MediaRouteSelector; import androidx.mediarouter.media.MediaRouter; import androidx.mediarouter.media.MediaRouter.RouteInfo; @@ -46,9 +48,6 @@ public class ChromecastConnection { /** The Listener callback. */ private Listener listener; - /** Initialize lifetime variable. */ - private String appId; - /** * Constructor. * @param act the current context @@ -56,14 +55,9 @@ public class ChromecastConnection { */ ChromecastConnection(Activity act, Listener connectionListener) { this.activity = act; - this.settings = activity.getSharedPreferences("CORDOVA-PLUGIN-CHROMECAST_ChromecastConnection", 0); - this.appId = settings.getString("appId", CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID); this.listener = connectionListener; this.chromecastSession = new ChromecastSession(activity, listener); - // Set the initial appId - CastOptionsProvider.setAppId(appId); - // This is the first call to getContext which will start up the // CastContext and prep it for searching for a session to rejoin // Also adds the receiver update callback @@ -93,25 +87,11 @@ ChromecastSession getChromecastSession() { /** * Must be called each time the appId changes and at least once before any other method is called. - * @param applicationId the app id to use * @param callback called when initialization is complete */ - public void initialize(String applicationId, CallbackContext callback) { + public void initialize(CallbackContext callback) { activity.runOnUiThread(new Runnable() { public void run() { - // If the app Id changed - if (applicationId == null || !applicationId.equals(appId)) { - // If app Id is valid - if (isValidAppId(applicationId)) { - // Set the new app Id - setAppId(applicationId); - } else { - // Else, just return - callback.success(); - return; - } - } - // Tell the client that initialization was a success callback.success(); @@ -157,38 +137,6 @@ private CastSession getSession() { return getSessionManager().getCurrentCastSession(); } - private void setAppId(String applicationId) { - this.appId = applicationId; - this.settings.edit().putString("appId", appId).apply(); - getContext().setReceiverApplicationId(appId); - } - - /** - * Tests if an application receiver id is valid. - * @param applicationId - application receiver id - * @return true if valid - */ - private boolean isValidAppId(String applicationId) { - try { - ScanCallback cb = new ScanCallback() { - @Override - void onRouteUpdate(List routes) { } - }; - // This will throw if the applicationId is invalid - getMediaRouter().addCallback(new MediaRouteSelector.Builder() - .addControlCategory(CastMediaControlIntent.categoryForCast(applicationId)) - .build(), - cb, - MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - // If no exception we passed, so remove the callback - getMediaRouter().removeCallback(cb); - return true; - } catch (IllegalArgumentException e) { - // Don't set the appId if it is not a valid receiverApplicationID - return false; - } - } - /** * This will create a new session or seamlessly selectRoute an existing one if we created it. * @param routeId the id of the route to selectRoute @@ -343,7 +291,8 @@ public void run() { // TODO accept theme as a config.xml option MediaRouteChooserDialog builder = new MediaRouteChooserDialog(activity, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar); builder.setRouteSelector(new MediaRouteSelector.Builder() - .addControlCategory(CastMediaControlIntent.categoryForCast(appId)) + .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) + .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .build()); builder.setCanceledOnTouchOutside(true); builder.setOnCancelListener(new DialogInterface.OnCancelListener() { @@ -431,7 +380,8 @@ public void run() { // Add the callback in active scan mode getMediaRouter().addCallback(new MediaRouteSelector.Builder() - .addControlCategory(CastMediaControlIntent.categoryForCast(appId)) + .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) + .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .build(), callback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); diff --git a/src/ios/MLPChromecast.m b/src/ios/MLPChromecast.m index 7db8d5d..f2a0847 100644 --- a/src/ios/MLPChromecast.m +++ b/src/ios/MLPChromecast.m @@ -12,53 +12,21 @@ @interface MLPChromecast() @end @implementation MLPChromecast -NSString* appId = nil; CDVInvokedUrlCommand* scanCommand = nil; int scansRunning = 0; - (void)pluginInitialize { [super pluginInitialize]; self.currentSession = [MLPChromecastSession alloc]; - - NSString* applicationId = [NSUserDefaults.standardUserDefaults stringForKey:@"appId"]; - if (applicationId == nil) { - applicationId = kGCKDefaultMediaReceiverApplicationID; - } - [self setAppId:applicationId]; -} -- (void)setAppId:(NSString*)applicationId { - // If the applicationId is invalid or has not changed, don't do anything - if ([self isValidAppId:applicationId] && [applicationId isEqualToString:appId]) { - return; - } - appId = applicationId; - - GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc] - initWithApplicationID:appId]; - GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria]; - options.physicalVolumeButtonsWillControlDeviceVolume = YES; - options.disableDiscoveryAutostart = NO; - [GCKCastContext setSharedInstanceWithOptions:options]; - - // Enable chromecast logger. -// [GCKLogger sharedInstance].delegate = self; - // Ensure we have only 1 listener attached [GCKCastContext.sharedInstance.discoveryManager removeListener:self]; [GCKCastContext.sharedInstance.discoveryManager addListener:self]; - + [GCKCastContext.sharedInstance.sessionManager removeListener: self]; [GCKCastContext.sharedInstance.sessionManager addListener: self]; - - self.currentSession = [self.currentSession initWithListener:self cordovaDelegate:self.commandDelegate]; -} -- (BOOL)isValidAppId:(NSString*)applicationId { - if (applicationId == (id)[NSNull null] || applicationId.length == 0) { - return NO; - } - return YES; + self.currentSession = [self.currentSession initWithListener:self cordovaDelegate:self.commandDelegate]; } // Override CDVPlugin onReset @@ -75,21 +43,10 @@ - (void)setup:(CDVInvokedUrlCommand*) command { } -(void) initialize:(CDVInvokedUrlCommand*)command { - NSString* applicationId = command.arguments[0]; - - // If the app id is invalid just send success and return - if (![self isValidAppId:applicationId]) { - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; - return; - } - - [self setAppId:applicationId]; - // Initialize success CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; - + // Search for existing session [self findAvailableReceiver:^{ [self.currentSession tryRejoin]; @@ -268,7 +225,7 @@ - (void)loadMedia:(CDVInvokedUrlCommand*) command { NSDictionary* metadata = command.arguments[7]; NSDictionary* textTrackStyle = command.arguments[8]; GCKMediaInformation* mediaInfo = [MLPCastUtilities buildMediaInformation:contentId customData:customData contentType:contentType duration:duration streamType:streamType startTime:currentTime metaData:metadata textTrackStyle:textTrackStyle]; - + [self.currentSession loadMediaWithCommand:command mediaInfo:mediaInfo autoPlay:autoplay currentTime:currentTime]; } @@ -280,7 +237,7 @@ - (void)addMessageListener:(CDVInvokedUrlCommand*)command { - (void)sendMessage:(CDVInvokedUrlCommand*) command { NSString* namespace = command.arguments[0]; NSString* message = command.arguments[1]; - + [self.currentSession sendMessageWithCommand:command namespace:namespace message:message]; } @@ -306,7 +263,7 @@ - (void)mediaStop:(CDVInvokedUrlCommand*)command { - (void)mediaEditTracksInfo:(CDVInvokedUrlCommand*)command { NSArray* activeTrackIds = command.arguments[0]; NSData* textTrackStyle = command.arguments[1]; - + GCKMediaTextTrackStyle* textTrackStyleObject = [MLPCastUtilities buildTextTrackStyle:textTrackStyle]; [self.currentSession setActiveTracksWithCommand:command activeTrackIds:activeTrackIds textTrackStyle:textTrackStyleObject]; } @@ -318,11 +275,11 @@ - (void)selectRoute:(CDVInvokedUrlCommand*)command { [self sendError:@"session_error" message:@"Leave or stop current session before attempting to join new session." command:command]; return; } - + NSString* routeID = command.arguments[0]; // Ensure the scan is running [self startRouteScan]; - + [MLPCastUtilities retry:^BOOL{ GCKDevice* device = [[GCKCastContext sharedInstance].discoveryManager deviceWithUniqueID:routeID]; if (device != nil) { @@ -354,9 +311,6 @@ - (void) didUpdateDeviceList { #pragma GCKSessionManagerListener - (void)sessionManager:(GCKSessionManager *)sessionManager didStartSession:(GCKSession *)session { - // Only save the app Id after a session for that appId has been successfully created/joined - [NSUserDefaults.standardUserDefaults setObject:appId forKey:@"appId"]; - [[NSUserDefaults standardUserDefaults] synchronize]; } #pragma CastSessionListener @@ -406,9 +360,9 @@ - (void)sendEvent:(NSString *)eventName args:(NSArray *)args{ } - (void)sendError:(NSString *)code message:(NSString *)message command:(CDVInvokedUrlCommand*)command{ - + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[MLPCastUtilities createError:code message:message]]; - + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } diff --git a/www/chrome.cast.js b/www/chrome.cast.js index 0d6cc5c..17b996e 100644 --- a/www/chrome.cast.js +++ b/www/chrome.cast.js @@ -544,7 +544,11 @@ var _session; * @param {function} errorCallback */ chrome.cast.initialize = function (apiConfig, successCallback, errorCallback) { - execute('initialize', apiConfig.sessionRequest.appId, apiConfig.autoJoinPolicy, apiConfig.defaultActionPolicy, function (err) { + if (apiConfig.sessionRequest.appId && 'warn' in window.console) { + console.warn('cordova-plugin-chromecast', 'The initialize command doesn\'t respect the `SessionId#appId` (see readme for more information)'); + } + + execute('initialize', apiConfig.autoJoinPolicy, apiConfig.defaultActionPolicy, function (err) { if (!err) { // Don't set the listeners config until success _initialized = true;