ionic-chromecast 0.0.2 β†’ 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@ ext {
3
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
+ // Add a default version for MediaRouter if not provided by root project
7
+ androidxMediaRouterVersion = project.hasProperty('androidxMediaRouterVersion') ? rootProject.ext.androidxMediaRouterVersion : '1.6.0'
6
8
  }
7
9
 
8
10
  buildscript {
@@ -37,8 +39,9 @@ android {
37
39
  abortOnError false
38
40
  }
39
41
  compileOptions {
40
- sourceCompatibility JavaVersion.VERSION_21
41
- targetCompatibility JavaVersion.VERSION_21
42
+ // Use Java 17 for broader compatibility with current Android toolchains
43
+ sourceCompatibility JavaVersion.VERSION_17
44
+ targetCompatibility JavaVersion.VERSION_17
42
45
  }
43
46
  }
44
47
 
@@ -56,6 +59,12 @@ dependencies {
56
59
  // Google Cast SDK
57
60
  implementation 'com.google.android.gms:play-services-cast-framework:21.5.0'
58
61
 
62
+ // AndroidX MediaRouter (for Cast chooser/dialog)
63
+ implementation "androidx.mediarouter:mediarouter:$androidxMediaRouterVersion"
64
+
65
+ // AndroidX Media (required for MediaMetadataCompat)
66
+ implementation 'androidx.media:media:1.7.0'
67
+
59
68
  testImplementation "junit:junit:$junitVersion"
60
69
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
61
70
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -1,6 +1,7 @@
1
1
  package com.fabianacevedo.ionicchromecast;
2
2
 
3
3
  import android.content.Context;
4
+ import android.content.SharedPreferences;
4
5
  import com.google.android.gms.cast.framework.CastOptions;
5
6
  import com.google.android.gms.cast.framework.OptionsProvider;
6
7
  import com.google.android.gms.cast.framework.SessionProvider;
@@ -13,15 +14,32 @@ import java.util.List;
13
14
  */
14
15
  public class CastOptionsProvider implements OptionsProvider {
15
16
 
16
- // Default Media Receiver App ID
17
+ // Default Media Receiver App ID (Google's default receiver)
17
18
  private static final String DEFAULT_RECEIVER_APP_ID = "CC1AD845";
19
+ private static final String PREFS_NAME = "IonicChromecastPrefs";
20
+ private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
21
+
22
+ // Static variable to hold the receiver app ID before CastContext is initialized
23
+ public static String sReceiverApplicationId = null;
18
24
 
19
25
  @Override
20
26
  public CastOptions getCastOptions(Context context) {
21
- // Use default receiver app ID initially
22
- // This will be overridden when initialize() is called
27
+ String receiverAppId = DEFAULT_RECEIVER_APP_ID;
28
+
29
+ // Try to get from static variable first (set during initialize)
30
+ if (sReceiverApplicationId != null && !sReceiverApplicationId.isEmpty()) {
31
+ receiverAppId = sReceiverApplicationId;
32
+ } else {
33
+ // Fallback to SharedPreferences
34
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
35
+ String savedId = prefs.getString(KEY_RECEIVER_APP_ID, null);
36
+ if (savedId != null && !savedId.isEmpty()) {
37
+ receiverAppId = savedId;
38
+ }
39
+ }
40
+
23
41
  return new CastOptions.Builder()
24
- .setReceiverApplicationId(DEFAULT_RECEIVER_APP_ID)
42
+ .setReceiverApplicationId(receiverAppId)
25
43
  .build();
26
44
  }
27
45
 
@@ -1,17 +1,34 @@
1
1
  package com.fabianacevedo.ionicchromecast;
2
2
 
3
3
  import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+ import android.os.Handler;
6
+ import android.os.Looper;
4
7
  import com.getcapacitor.Logger;
5
8
  import com.google.android.gms.cast.framework.CastContext;
6
9
  import com.google.android.gms.cast.framework.CastOptions;
7
10
  import com.google.android.gms.cast.framework.OptionsProvider;
8
11
  import com.google.android.gms.cast.framework.SessionProvider;
9
12
  import com.google.android.gms.cast.CastMediaControlIntent;
13
+ import com.google.android.gms.common.images.WebImage;
10
14
  import java.util.List;
15
+ import java.util.concurrent.CountDownLatch;
16
+ import java.util.concurrent.TimeUnit;
17
+ import java.util.concurrent.atomic.AtomicBoolean;
18
+
19
+ // New imports for improved media loading
20
+ import com.google.android.gms.cast.MediaLoadRequestData;
21
+ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
22
+ import com.google.android.gms.common.api.PendingResult;
23
+ // Nuevos imports para esperar sesiΓ³n activa
24
+ import com.google.android.gms.cast.framework.CastSession;
25
+ import com.google.android.gms.cast.framework.SessionManagerListener;
11
26
 
12
27
  public class IonicChromecast {
13
28
 
14
29
  private static final String TAG = "IonicChromecast";
30
+ private static final String PREFS_NAME = "IonicChromecastPrefs";
31
+ private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
15
32
  private CastContext castContext;
16
33
  private boolean isInitialized = false;
17
34
 
@@ -29,23 +46,46 @@ public class IonicChromecast {
29
46
  }
30
47
 
31
48
  if (receiverApplicationId == null || receiverApplicationId.isEmpty()) {
32
- Logger.error(TAG, "Receiver Application ID is required");
49
+ Logger.error(TAG, "Receiver Application ID is required", null);
33
50
  return false;
34
51
  }
35
52
 
36
53
  Logger.info(TAG, "Initializing Cast SDK with receiver ID: " + receiverApplicationId);
37
54
 
38
- // Initialize CastContext
39
- castContext = CastContext.getSharedInstance(context);
55
+ // Save the receiver app ID to SharedPreferences for CastOptionsProvider
56
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
57
+ prefs.edit().putString(KEY_RECEIVER_APP_ID, receiverApplicationId).apply();
40
58
 
41
- if (castContext != null) {
42
- isInitialized = true;
43
- Logger.info(TAG, "Cast SDK initialized successfully");
44
- return true;
45
- } else {
46
- Logger.error(TAG, "Failed to get CastContext");
47
- return false;
48
- }
59
+ // Also set it in the static variable for immediate use
60
+ CastOptionsProvider.sReceiverApplicationId = receiverApplicationId;
61
+
62
+ // Ensure CastContext is obtained on the main thread
63
+ final AtomicBoolean initSuccess = new AtomicBoolean(false);
64
+ final CountDownLatch latch = new CountDownLatch(1);
65
+ Handler mainHandler = new Handler(Looper.getMainLooper());
66
+ mainHandler.post(() -> {
67
+ try {
68
+ // Use application context per Cast SDK recommendations
69
+ Context appCtx = context.getApplicationContext();
70
+ castContext = CastContext.getSharedInstance(appCtx);
71
+ if (castContext != null) {
72
+ isInitialized = true;
73
+ Logger.info(TAG, "Cast SDK initialized successfully");
74
+ initSuccess.set(true);
75
+ } else {
76
+ Logger.error(TAG, "Failed to get CastContext", null);
77
+ initSuccess.set(false);
78
+ }
79
+ } catch (Exception e) {
80
+ Logger.error(TAG, "Error initializing Cast SDK on main thread: " + e.getMessage(), e);
81
+ initSuccess.set(false);
82
+ } finally {
83
+ latch.countDown();
84
+ }
85
+ });
86
+ // Wait up to 5 seconds for initialization to complete
87
+ latch.await(5, TimeUnit.SECONDS);
88
+ return initSuccess.get();
49
89
 
50
90
  } catch (Exception e) {
51
91
  Logger.error(TAG, "Error initializing Cast SDK: " + e.getMessage(), e);
@@ -82,13 +122,12 @@ public class IonicChromecast {
82
122
  */
83
123
  public boolean requestSession(Context context) {
84
124
  if (!isInitialized || castContext == null) {
85
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
125
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
86
126
  return false;
87
127
  }
88
128
  try {
89
- // Show the Cast dialog
90
- castContext.getSessionManager().startSession(false);
91
- Logger.info(TAG, "Requested Cast session (dialog should appear)");
129
+ // No hay API pΓΊblica para forzar el diΓ‘logo de Cast, debe usarse el CastButton en la UI.
130
+ Logger.info(TAG, "Requested Cast session (UI CastButton should be used)");
92
131
  return true;
93
132
  } catch (Exception e) {
94
133
  Logger.error(TAG, "Error requesting Cast session: " + e.getMessage(), e);
@@ -102,12 +141,13 @@ public class IonicChromecast {
102
141
  */
103
142
  public boolean isSessionActive() {
104
143
  if (!isInitialized || castContext == null) {
105
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
144
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
106
145
  return false;
107
146
  }
108
147
  try {
109
- boolean active = castContext.getSessionManager().getCurrentCastSession() != null;
110
- Logger.info(TAG, "Session active: " + active);
148
+ com.google.android.gms.cast.framework.CastSession session = castContext.getSessionManager().getCurrentCastSession();
149
+ boolean active = (session != null && session.isConnected());
150
+ Logger.info(TAG, "isSessionActive check: session=" + (session != null) + ", connected=" + (session != null && session.isConnected()) + ", result=" + active);
111
151
  return active;
112
152
  } catch (Exception e) {
113
153
  Logger.error(TAG, "Error checking session status: " + e.getMessage(), e);
@@ -121,64 +161,163 @@ public class IonicChromecast {
121
161
  */
122
162
  public boolean areDevicesAvailable() {
123
163
  if (!isInitialized || castContext == null) {
124
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
125
- return false;
126
- }
127
- try {
128
- // Check if there are any Cast devices discovered
129
- int deviceCount = castContext.getDiscoveryManager().getCastDeviceCount();
130
- boolean available = deviceCount > 0;
131
- Logger.info(TAG, "Devices available: " + available + " (" + deviceCount + ")");
132
- return available;
133
- } catch (Exception e) {
134
- Logger.error(TAG, "Error checking device availability: " + e.getMessage(), e);
164
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
135
165
  return false;
136
166
  }
167
+ // No hay API pΓΊblica para contar dispositivos en CastContext
168
+ Logger.info(TAG, "areDevicesAvailable: Not supported by Cast SDK. Returning true if initialized.");
169
+ return true;
137
170
  }
138
171
 
139
172
  /**
140
173
  * Load media on the Cast device
141
174
  * @param url The media URL
142
- * @param metadata Optional metadata (title, images, etc)
175
+ * @param title Optional title
176
+ * @param subtitle Optional subtitle/artist
177
+ * @param imageUrl Optional image URL
178
+ * @param contentType Optional content type (default: video/mp4)
143
179
  * @return true if media loaded successfully
144
180
  */
145
- public boolean loadMedia(String url, MediaMetadataCompat metadata) {
181
+ public boolean loadMedia(String url, String title, String subtitle, String imageUrl, String contentType) {
146
182
  if (!isInitialized || castContext == null) {
147
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
183
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
148
184
  return false;
149
185
  }
150
186
  try {
151
187
  if (url == null || url.isEmpty()) {
152
- Logger.error(TAG, "Media URL is required");
188
+ Logger.error(TAG, "Media URL is required", null);
153
189
  return false;
154
190
  }
191
+
155
192
  // Build Cast media info
156
193
  com.google.android.gms.cast.MediaMetadata castMetadata = new com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_MOVIE);
157
- if (metadata != null) {
158
- if (metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE) != null)
159
- castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
160
- if (metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST) != null)
161
- castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_SUBTITLE, metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
162
- if (metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI) != null)
163
- castMetadata.addImage(new com.google.android.gms.cast.Image(android.net.Uri.parse(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI))));
164
- // Add more metadata fields as needed
194
+
195
+ // Add metadata if provided
196
+ if (title != null && !title.isEmpty()) {
197
+ Logger.info(TAG, "Setting title: " + title);
198
+ castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, title);
199
+ }
200
+ if (subtitle != null && !subtitle.isEmpty()) {
201
+ Logger.info(TAG, "Setting subtitle: " + subtitle);
202
+ castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_SUBTITLE, subtitle);
203
+ }
204
+ if (imageUrl != null && !imageUrl.isEmpty()) {
205
+ Logger.info(TAG, "Setting image: " + imageUrl);
206
+ castMetadata.addImage(new WebImage(android.net.Uri.parse(imageUrl)));
165
207
  }
208
+
209
+ // Use provided content type or default to video/mp4
210
+ String finalContentType = (contentType != null && !contentType.isEmpty()) ? contentType : "video/mp4";
211
+
212
+ Logger.info(TAG, "πŸ“Ή Building MediaInfo: URL=" + url + ", contentType=" + finalContentType);
213
+
166
214
  com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url)
167
215
  .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED)
168
- .setContentType(metadata != null && metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI) != null ? metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI) : "video/mp4")
216
+ .setContentType(finalContentType)
169
217
  .setMetadata(castMetadata)
170
218
  .build();
219
+
220
+ Logger.info(TAG, "βœ… MediaInfo built successfully");
221
+
171
222
  com.google.android.gms.cast.framework.CastSession session = castContext.getSessionManager().getCurrentCastSession();
172
223
  if (session == null || !session.isConnected()) {
173
- Logger.error(TAG, "No active Cast session");
224
+ Logger.info(TAG, "No active Cast session yet. Waiting up to 10s...");
225
+ boolean connected = waitForActiveSession(10_000);
226
+ if (!connected) {
227
+ Logger.error(TAG, "No active Cast session after waiting", null);
228
+ return false;
229
+ }
230
+ session = castContext.getSessionManager().getCurrentCastSession();
231
+ }
232
+
233
+ RemoteMediaClient rmc = session.getRemoteMediaClient();
234
+ if (rmc == null) {
235
+ Logger.error(TAG, "RemoteMediaClient is null (session not ready)", null);
174
236
  return false;
175
237
  }
176
- session.getRemoteMediaClient().load(mediaInfo, true, 0);
177
- Logger.info(TAG, "Media loaded to Cast device: " + url);
178
- return true;
238
+
239
+ // Build a load request with autoplay
240
+ MediaLoadRequestData requestData = new MediaLoadRequestData.Builder()
241
+ .setMediaInfo(mediaInfo)
242
+ .setAutoplay(true)
243
+ .setCurrentTime(0L)
244
+ .build();
245
+
246
+ Logger.info(TAG, "Sending media load request: URL=" + url + ", contentType=" + finalContentType);
247
+
248
+ // Send load and wait briefly for the result so we can report success/failure
249
+ final AtomicBoolean loadSuccess = new AtomicBoolean(false);
250
+ final CountDownLatch latch = new CountDownLatch(1);
251
+ try {
252
+ PendingResult<RemoteMediaClient.MediaChannelResult> pending = rmc.load(requestData);
253
+ pending.setResultCallback(result -> {
254
+ boolean ok = result != null && result.getStatus() != null && result.getStatus().isSuccess();
255
+ loadSuccess.set(ok);
256
+ if (ok) {
257
+ Logger.info(TAG, "Media load success");
258
+ } else {
259
+ String msg = (result != null && result.getStatus() != null) ? String.valueOf(result.getStatus().getStatusCode()) : "unknown";
260
+ Logger.error(TAG, "Media load failed. Status=" + msg, null);
261
+ }
262
+ latch.countDown();
263
+ });
264
+ // Wait up to 6 seconds for a result
265
+ latch.await(6, TimeUnit.SECONDS);
266
+ } catch (Exception e) {
267
+ Logger.error(TAG, "Error sending media load request: " + e.getMessage(), e);
268
+ return false;
269
+ }
270
+
271
+ return loadSuccess.get();
179
272
  } catch (Exception e) {
180
273
  Logger.error(TAG, "Error loading media: " + e.getMessage(), e);
181
274
  return false;
182
275
  }
183
276
  }
277
+
278
+ // Helper: espera una sesiΓ³n activa hasta timeoutMs
279
+ private boolean waitForActiveSession(long timeoutMs) {
280
+ if (castContext == null) return false;
281
+ final CountDownLatch latch = new CountDownLatch(1);
282
+ final AtomicBoolean active = new AtomicBoolean(false);
283
+
284
+ SessionManagerListener<CastSession> listener = new SessionManagerListener<CastSession>() {
285
+ @Override public void onSessionStarted(CastSession session, String sessionId) {
286
+ active.set(session != null && session.isConnected());
287
+ latch.countDown();
288
+ }
289
+ @Override public void onSessionResumed(CastSession session, boolean wasSuspended) {
290
+ active.set(session != null && session.isConnected());
291
+ latch.countDown();
292
+ }
293
+ @Override public void onSessionStartFailed(CastSession session, int error) { latch.countDown(); }
294
+ @Override public void onSessionResumeFailed(CastSession session, int error) { latch.countDown(); }
295
+ @Override public void onSessionSuspended(CastSession session, int reason) { }
296
+ @Override public void onSessionEnded(CastSession session, int error) { }
297
+ @Override public void onSessionEnding(CastSession session) { }
298
+ @Override public void onSessionStarting(CastSession session) { }
299
+ @Override public void onSessionResuming(CastSession session, String sessionId) { }
300
+ };
301
+
302
+ try {
303
+ // Si ya hay sesiΓ³n conectada, devolver inmediatamente
304
+ CastSession current = castContext.getSessionManager().getCurrentCastSession();
305
+ if (current != null && current.isConnected()) {
306
+ return true;
307
+ }
308
+ castContext.getSessionManager().addSessionManagerListener(listener, CastSession.class);
309
+ // Esperar hasta timeout
310
+ latch.await(timeoutMs, TimeUnit.MILLISECONDS);
311
+ // Verificar de nuevo
312
+ current = castContext.getSessionManager().getCurrentCastSession();
313
+ return current != null && current.isConnected() || active.get();
314
+ } catch (Exception e) {
315
+ Logger.error(TAG, "Error waiting for Cast session: " + e.getMessage(), e);
316
+ return false;
317
+ } finally {
318
+ try {
319
+ castContext.getSessionManager().removeSessionManagerListener(listener, CastSession.class);
320
+ } catch (Exception ignore) {}
321
+ }
322
+ }
184
323
  }
@@ -5,13 +5,34 @@ import com.getcapacitor.Plugin;
5
5
  import com.getcapacitor.PluginCall;
6
6
  import com.getcapacitor.PluginMethod;
7
7
  import com.getcapacitor.annotation.CapacitorPlugin;
8
- import androidx.media.MediaMetadataCompat;
8
+
9
+ import android.text.TextUtils;
10
+ import com.google.android.gms.cast.CastMediaControlIntent;
11
+ import androidx.appcompat.app.AppCompatActivity;
12
+ import androidx.mediarouter.app.MediaRouteChooserDialog;
13
+ import androidx.mediarouter.media.MediaRouteSelector;
14
+ import androidx.mediarouter.media.MediaRouter;
15
+ import android.content.DialogInterface;
16
+
17
+ // New imports for Cast session/media events
18
+ import com.google.android.gms.cast.framework.CastContext;
19
+ import com.google.android.gms.cast.framework.CastSession;
20
+ import com.google.android.gms.cast.framework.SessionManagerListener;
21
+ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
9
22
 
10
23
  @CapacitorPlugin(name = "IonicChromecast")
11
24
  public class IonicChromecastPlugin extends Plugin {
12
25
 
13
26
  private IonicChromecast implementation = new IonicChromecast();
14
27
 
28
+ // Session/media listeners
29
+ private SessionManagerListener<CastSession> sessionListener;
30
+ private RemoteMediaClient.Callback mediaCallback;
31
+
32
+ // MediaRouter for route selection observation
33
+ private MediaRouter mediaRouter;
34
+ private MediaRouter.Callback mediaRouterCallback;
35
+
15
36
  /**
16
37
  * Initialize the Google Cast SDK
17
38
  * This method must be called before using any other Cast functionality
@@ -31,12 +52,104 @@ public class IonicChromecastPlugin extends Plugin {
31
52
  ret.put("success", success);
32
53
 
33
54
  if (success) {
55
+ // Attach session listener to emit events
56
+ try {
57
+ CastContext cc = implementation.getCastContext();
58
+ if (cc != null) {
59
+ // Remove previous listener if any
60
+ if (sessionListener != null) {
61
+ cc.getSessionManager().removeSessionManagerListener(sessionListener, CastSession.class);
62
+ }
63
+ sessionListener = new SessionManagerListener<CastSession>() {
64
+ @Override
65
+ public void onSessionStarted(CastSession session, String sessionId) {
66
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionStarted called! sessionId=" + sessionId);
67
+ JSObject data = new JSObject();
68
+ data.put("sessionId", sessionId);
69
+ notifyListeners("sessionStarted", data);
70
+
71
+ attachRemoteMediaClient(session);
72
+ }
73
+ @Override
74
+ public void onSessionResumed(CastSession session, boolean wasSuspended) {
75
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionResumed called!");
76
+ JSObject data = new JSObject();
77
+ data.put("resumed", true);
78
+ notifyListeners("sessionStarted", data);
79
+
80
+ attachRemoteMediaClient(session);
81
+ }
82
+ @Override public void onSessionEnded(CastSession session, int error) {
83
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionEnded called! error=" + error);
84
+ JSObject data = new JSObject();
85
+ data.put("errorCode", error);
86
+ notifyListeners("sessionEnded", data);
87
+ detachRemoteMediaClient(session);
88
+ }
89
+ @Override public void onSessionStarting(CastSession session) {
90
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionStarting called!");
91
+ }
92
+ @Override public void onSessionEnding(CastSession session) {
93
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionEnding called!");
94
+ }
95
+ @Override public void onSessionStartFailed(CastSession session, int error) {
96
+ com.getcapacitor.Logger.error("IonicChromecast", "⭐ onSessionStartFailed! error=" + error, null);
97
+ }
98
+ @Override public void onSessionResumeFailed(CastSession session, int error) {
99
+ com.getcapacitor.Logger.error("IonicChromecast", "⭐ onSessionResumeFailed! error=" + error, null);
100
+ }
101
+ @Override public void onSessionSuspended(CastSession session, int reason) {
102
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionSuspended! reason=" + reason);
103
+ }
104
+ @Override public void onSessionResuming(CastSession session, String sessionId) {
105
+ com.getcapacitor.Logger.info("IonicChromecast", "⭐ onSessionResuming! sessionId=" + sessionId);
106
+ }
107
+ };
108
+ cc.getSessionManager().addSessionManagerListener(sessionListener, CastSession.class);
109
+ com.getcapacitor.Logger.info("IonicChromecast", "βœ… SessionManagerListener registered successfully");
110
+ }
111
+ } catch (Exception ignored) {}
112
+
34
113
  call.resolve(ret);
35
114
  } else {
36
115
  call.reject("Failed to initialize Cast SDK", ret);
37
116
  }
38
117
  }
39
118
 
119
+ private void attachRemoteMediaClient(CastSession session) {
120
+ try {
121
+ RemoteMediaClient rmc = session != null ? session.getRemoteMediaClient() : null;
122
+ if (rmc == null) return;
123
+ if (mediaCallback != null) {
124
+ rmc.unregisterCallback(mediaCallback);
125
+ }
126
+ mediaCallback = new RemoteMediaClient.Callback() {
127
+ @Override
128
+ public void onStatusUpdated() {
129
+ JSObject data = new JSObject();
130
+ data.put("status", "updated");
131
+ notifyListeners("playbackStatusChanged", data);
132
+ }
133
+ @Override
134
+ public void onMetadataUpdated() {
135
+ JSObject data = new JSObject();
136
+ data.put("status", "metadataUpdated");
137
+ notifyListeners("playbackStatusChanged", data);
138
+ }
139
+ };
140
+ rmc.registerCallback(mediaCallback);
141
+ } catch (Exception ignored) {}
142
+ }
143
+
144
+ private void detachRemoteMediaClient(CastSession session) {
145
+ try {
146
+ RemoteMediaClient rmc = session != null ? session.getRemoteMediaClient() : null;
147
+ if (rmc != null && mediaCallback != null) {
148
+ rmc.unregisterCallback(mediaCallback);
149
+ }
150
+ } catch (Exception ignored) {}
151
+ }
152
+
40
153
  @PluginMethod
41
154
  public void echo(PluginCall call) {
42
155
  String value = call.getString("value");
@@ -47,20 +160,71 @@ public class IonicChromecastPlugin extends Plugin {
47
160
  }
48
161
 
49
162
  /**
50
- * Request a Cast session from JavaScript
163
+ * Request a Cast session from JavaScript - Uses Cast SDK's built-in device picker
51
164
  */
52
165
  @PluginMethod
53
166
  public void requestSession(PluginCall call) {
54
- boolean success = implementation.requestSession(getContext());
55
- JSObject ret = new JSObject();
56
- ret.put("success", success);
57
- if (success) {
58
- ret.put("message", "Cast session requested. Dialog should appear.");
59
- call.resolve(ret);
60
- } else {
61
- ret.put("message", "Failed to request Cast session. Make sure SDK is initialized.");
62
- call.reject("Failed to request Cast session", ret);
167
+ // Ensure Cast SDK initialized
168
+ if (!implementation.isInitialized()) {
169
+ JSObject err = new JSObject();
170
+ err.put("success", false);
171
+ err.put("message", "Cast SDK not initialized. Call initialize() first.");
172
+ call.reject("Failed to request Cast session", err);
173
+ return;
63
174
  }
175
+
176
+ getActivity().runOnUiThread(() -> {
177
+ try {
178
+ CastContext cc = implementation.getCastContext();
179
+ if (cc == null) {
180
+ JSObject err = new JSObject();
181
+ err.put("success", false);
182
+ err.put("message", "CastContext is null");
183
+ call.reject("Failed to request Cast session", err);
184
+ return;
185
+ }
186
+
187
+ com.getcapacitor.Logger.info("IonicChromecast", "πŸš€ Showing Cast device selector via MediaRouteChooserDialog...");
188
+
189
+ // Use MediaRouteChooserDialog - the proper way (as per caprockapps implementation)
190
+ AppCompatActivity activity = (AppCompatActivity) getActivity();
191
+
192
+ String receiverId = CastOptionsProvider.sReceiverApplicationId;
193
+ if (TextUtils.isEmpty(receiverId)) {
194
+ receiverId = "CC1AD845";
195
+ }
196
+
197
+ MediaRouteSelector selector = new MediaRouteSelector.Builder()
198
+ .addControlCategory(CastMediaControlIntent.categoryForCast(receiverId))
199
+ .build();
200
+
201
+ MediaRouteChooserDialog chooserDialog = new MediaRouteChooserDialog(activity, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar);
202
+ chooserDialog.setRouteSelector(selector);
203
+ chooserDialog.setCanceledOnTouchOutside(true);
204
+ chooserDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
205
+ @Override
206
+ public void onCancel(DialogInterface dialog) {
207
+ com.getcapacitor.Logger.info("IonicChromecast", "User cancelled Cast device selection");
208
+ }
209
+ });
210
+
211
+ // Show the dialog
212
+ chooserDialog.show();
213
+
214
+ com.getcapacitor.Logger.info("IonicChromecast", "βœ… Cast device selector dialog displayed");
215
+
216
+ JSObject ret = new JSObject();
217
+ ret.put("success", true);
218
+ ret.put("message", "Cast chooser displayed.");
219
+ call.resolve(ret);
220
+ } catch (Exception e) {
221
+ com.getcapacitor.Logger.error("IonicChromecast", "❌ Error showing Cast chooser: " + e.getMessage(), e);
222
+ JSObject err = new JSObject();
223
+ err.put("success", false);
224
+ err.put("message", "Error showing Cast chooser: " + e.getMessage());
225
+ call.reject("Failed to request Cast session", err);
226
+ }
227
+ });
64
228
  }
65
229
 
66
230
  /**
@@ -102,30 +266,63 @@ public class IonicChromecastPlugin extends Plugin {
102
266
  public void loadMedia(PluginCall call) {
103
267
  String url = call.getString("url");
104
268
  JSObject metadataObj = call.getObject("metadata");
105
- MediaMetadataCompat metadata = null;
269
+
270
+ com.getcapacitor.Logger.info("IonicChromecast", "πŸ“₯ loadMedia called with URL: " + url);
271
+ com.getcapacitor.Logger.info("IonicChromecast", "πŸ“₯ metadata object: " + (metadataObj != null ? metadataObj.toString() : "null"));
272
+
273
+ // Extract metadata fields
274
+ String title = null;
275
+ String subtitle = null;
276
+ String imageUrl = null;
277
+ String contentType = null;
278
+
106
279
  if (metadataObj != null) {
107
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
108
- if (metadataObj.has("title")) builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, metadataObj.getString("title"));
109
- if (metadataObj.has("subtitle")) builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, metadataObj.getString("subtitle"));
280
+ title = metadataObj.getString("title");
281
+ subtitle = metadataObj.getString("subtitle");
282
+ contentType = metadataObj.getString("contentType");
283
+
284
+ com.getcapacitor.Logger.info("IonicChromecast", "πŸ“₯ Extracted - title: " + title + ", subtitle: " + subtitle + ", contentType: " + contentType);
285
+
286
+ // Get the first image if available
110
287
  if (metadataObj.has("images")) {
111
- // Only use the first image for now
112
- String img = metadataObj.getJSONArray("images").optString(0, null);
113
- if (img != null) builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, img);
288
+ try {
289
+ imageUrl = metadataObj.getJSONArray("images").optString(0, null);
290
+ com.getcapacitor.Logger.info("IonicChromecast", "πŸ“₯ Extracted imageUrl: " + imageUrl);
291
+ } catch (Exception e) {
292
+ // Ignore if images array is malformed
293
+ com.getcapacitor.Logger.error("IonicChromecast", "Error parsing images array", e);
294
+ }
114
295
  }
115
- if (metadataObj.has("studio")) builder.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, metadataObj.getString("studio"));
116
- if (metadataObj.has("contentType")) builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, metadataObj.getString("contentType"));
117
- if (metadataObj.has("duration")) builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, metadataObj.getLong("duration"));
118
- metadata = builder.build();
119
296
  }
120
- boolean success = implementation.loadMedia(url, metadata);
297
+
298
+ boolean success = implementation.loadMedia(url, title, subtitle, imageUrl, contentType);
299
+
121
300
  JSObject ret = new JSObject();
122
301
  ret.put("success", success);
123
302
  if (success) {
124
303
  ret.put("message", "Media sent to Cast device.");
304
+ notifyListeners("mediaLoaded", ret);
125
305
  call.resolve(ret);
126
306
  } else {
127
307
  ret.put("message", "Failed to send media. Check session and device.");
308
+ notifyListeners("mediaError", ret);
128
309
  call.reject("Failed to send media", ret);
129
310
  }
130
311
  }
312
+
313
+ @Override
314
+ protected void handleOnDestroy() {
315
+ super.handleOnDestroy();
316
+ try {
317
+ CastContext cc = implementation.getCastContext();
318
+ if (cc != null && sessionListener != null) {
319
+ cc.getSessionManager().removeSessionManagerListener(sessionListener, CastSession.class);
320
+ }
321
+ } catch (Exception ignored) {}
322
+ try {
323
+ if (mediaRouter != null && mediaRouterCallback != null) {
324
+ mediaRouter.removeCallback(mediaRouterCallback);
325
+ }
326
+ } catch (Exception ignored) {}
327
+ }
131
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionic-chromecast",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Capacitor plugin for Google Cast SDK (Chromecast) integration with Android support",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",