ionic-chromecast 0.0.2 → 0.0.4

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.
package/README.md CHANGED
@@ -41,7 +41,8 @@ import { IonicChromecast } from 'ionic-chromecast';
41
41
  // In your app.component.ts or main initialization
42
42
  async initializeCast() {
43
43
  const result = await IonicChromecast.initialize({
44
- receiverApplicationId: 'CC1AD845' // Default Media Receiver
44
+ // Prefer CastVideos CAF receiver for reliable UI on TV. Use 'CC1AD845' if you need the default.
45
+ receiverApplicationId: '4F8B3483'
45
46
  });
46
47
 
47
48
  if (result.success) {
@@ -538,3 +539,8 @@ Listen to Chromecast events (Android only)
538
539
  <code>'sessionStarted' | 'sessionEnded' | 'mediaLoaded' | 'mediaError' | 'deviceAvailable' | 'deviceUnavailable' | 'volumeChanged' | 'playbackStatusChanged'</code>
539
540
 
540
541
  </docgen-api>
542
+
543
+ ## Troubleshooting
544
+ - No Cast UI on TV after connecting: use the CastVideos CAF receiver ID `4F8B3483` instead of the default `CC1AD845` when calling `initialize`.
545
+ - Media returns success but nothing plays: confirm a session is active (`isSessionActive`), then retry `loadMedia` with a known-good HTTPS MP4 (e.g., BigBuckBunny).
546
+ - Devices not found: ensure the phone and Chromecast are on the same WiFi and Google Play Services is up to date.
@@ -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"
@@ -4,6 +4,11 @@
4
4
  <uses-permission android:name="android.permission.INTERNET" />
5
5
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6
6
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
7
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
8
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
9
+ <uses-permission
10
+ android:name="android.permission.NEARBY_WIFI_DEVICES"
11
+ android:usesPermissionFlags="neverForLocation" />
7
12
 
8
13
  <application>
9
14
  <!-- Google Cast OptionsProvider -->
@@ -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;
@@ -9,22 +10,38 @@ import java.util.List;
9
10
 
10
11
  /**
11
12
  * OptionsProvider for Google Cast SDK
12
- * This class is required by the Cast SDK to provide configuration options
13
13
  */
14
14
  public class CastOptionsProvider implements OptionsProvider {
15
-
16
- // Default Media Receiver App ID
15
+
17
16
  private static final String DEFAULT_RECEIVER_APP_ID = "CC1AD845";
18
-
17
+ private static final String PREFS_NAME = "IonicChromecastPrefs";
18
+ private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
19
+
20
+ public static String sReceiverApplicationId = null;
21
+
19
22
  @Override
20
23
  public CastOptions getCastOptions(Context context) {
21
- // Use default receiver app ID initially
22
- // This will be overridden when initialize() is called
24
+ String receiverAppId = DEFAULT_RECEIVER_APP_ID;
25
+
26
+ // Prefer static variable set by initialize()
27
+ if (sReceiverApplicationId != null && !sReceiverApplicationId.isEmpty()) {
28
+ receiverAppId = sReceiverApplicationId;
29
+ } else {
30
+ // Try from SharedPreferences
31
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
32
+ String savedId = prefs.getString(KEY_RECEIVER_APP_ID, null);
33
+ if (savedId != null && !savedId.isEmpty()) {
34
+ receiverAppId = savedId;
35
+ }
36
+ }
37
+
23
38
  return new CastOptions.Builder()
24
- .setReceiverApplicationId(DEFAULT_RECEIVER_APP_ID)
39
+ .setReceiverApplicationId(receiverAppId)
40
+ .setStopReceiverApplicationWhenEndingSession(true)
41
+ .setResumeSavedSession(false)
25
42
  .build();
26
43
  }
27
-
44
+
28
45
  @Override
29
46
  public List<SessionProvider> getAdditionalSessionProviders(Context context) {
30
47
  return null;
@@ -1,19 +1,39 @@
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
- import com.google.android.gms.cast.framework.CastOptions;
7
- import com.google.android.gms.cast.framework.OptionsProvider;
8
- import com.google.android.gms.cast.framework.SessionProvider;
9
+ import com.google.android.gms.common.ConnectionResult;
10
+ import com.google.android.gms.common.GoogleApiAvailability;
11
+ import java.util.concurrent.CountDownLatch;
12
+ import java.util.concurrent.TimeUnit;
13
+ import java.util.concurrent.atomic.AtomicBoolean;
14
+ import androidx.mediarouter.media.MediaRouter;
15
+ import androidx.mediarouter.media.MediaRouteSelector;
9
16
  import com.google.android.gms.cast.CastMediaControlIntent;
10
- import java.util.List;
17
+ import com.google.android.gms.cast.MediaMetadata;
18
+ import com.google.android.gms.cast.MediaInfo;
19
+ import com.google.android.gms.cast.framework.CastSession;
20
+ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
21
+ import com.google.android.gms.cast.MediaLoadRequestData;
22
+ import com.google.android.gms.common.api.PendingResult;
23
+ import com.google.android.gms.common.images.WebImage;
11
24
 
12
25
  public class IonicChromecast {
13
26
 
14
27
  private static final String TAG = "IonicChromecast";
28
+ private static final String PREFS_NAME = "IonicChromecastPrefs";
29
+ private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
15
30
  private CastContext castContext;
16
31
  private boolean isInitialized = false;
32
+ private Context appContext;
33
+ private String lastError = null;
34
+ private MediaRouter mediaRouter;
35
+ private MediaRouteSelector mediaRouteSelector;
36
+ private final Object discoveryLock = new Object();
17
37
 
18
38
  /**
19
39
  * Initialize the Google Cast SDK with the provided receiver application ID
@@ -23,162 +43,360 @@ public class IonicChromecast {
23
43
  */
24
44
  public boolean initialize(Context context, String receiverApplicationId) {
25
45
  try {
46
+ lastError = null;
26
47
  if (isInitialized) {
27
48
  Logger.info(TAG, "Cast SDK already initialized");
28
49
  return true;
29
50
  }
30
51
 
31
52
  if (receiverApplicationId == null || receiverApplicationId.isEmpty()) {
32
- Logger.error(TAG, "Receiver Application ID is required");
53
+ Logger.error(TAG, "Receiver Application ID is required", null);
33
54
  return false;
34
55
  }
35
-
56
+
36
57
  Logger.info(TAG, "Initializing Cast SDK with receiver ID: " + receiverApplicationId);
58
+ Logger.info(TAG, "Thread at init: " + Thread.currentThread().getName());
59
+
60
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
61
+ prefs.edit().putString(KEY_RECEIVER_APP_ID, receiverApplicationId).apply();
37
62
 
38
- // Initialize CastContext
39
- castContext = CastContext.getSharedInstance(context);
63
+ // Also set it in the static variable for immediate use
64
+ CastOptionsProvider.sReceiverApplicationId = receiverApplicationId;
40
65
 
66
+ int playStatus = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
67
+ Logger.info(TAG, "Google Play Services status=" + playStatus);
68
+ if (playStatus != ConnectionResult.SUCCESS) {
69
+ lastError = "Google Play Services status=" + playStatus;
70
+ Logger.error(TAG, lastError, null);
71
+ return false;
72
+ }
73
+ // Obtener CastContext y preparar MediaRouter siempre en el hilo principal
74
+ appContext = context.getApplicationContext();
75
+ Runnable initRunnable = () -> {
76
+ try {
77
+ castContext = CastContext.getSharedInstance(appContext);
78
+ if (castContext != null) {
79
+ mediaRouter = MediaRouter.getInstance(appContext);
80
+ mediaRouteSelector = new MediaRouteSelector.Builder()
81
+ .addControlCategory(CastMediaControlIntent.categoryForCast(CastOptionsProvider.sReceiverApplicationId != null ? CastOptionsProvider.sReceiverApplicationId : "CC1AD845"))
82
+ .build();
83
+ }
84
+ } catch (Exception e) {
85
+ lastError = "Error initializing on main thread: " + e.getMessage();
86
+ Logger.error(TAG, lastError, e);
87
+ }
88
+ };
89
+
90
+ if (Looper.myLooper() == Looper.getMainLooper()) {
91
+ initRunnable.run();
92
+ } else {
93
+ final CountDownLatch latch = new CountDownLatch(1);
94
+ Handler mainHandler = new Handler(Looper.getMainLooper());
95
+ mainHandler.post(() -> {
96
+ initRunnable.run();
97
+ latch.countDown();
98
+ });
99
+ boolean awaited = latch.await(6, TimeUnit.SECONDS);
100
+ if (!awaited && castContext == null && lastError == null) {
101
+ lastError = "Timed out waiting for CastContext on main thread";
102
+ Logger.error(TAG, lastError, null);
103
+ }
104
+ }
105
+
41
106
  if (castContext != null) {
42
107
  isInitialized = true;
43
108
  Logger.info(TAG, "Cast SDK initialized successfully");
44
- return true;
45
109
  } else {
46
- Logger.error(TAG, "Failed to get CastContext");
47
- return false;
110
+ if (lastError == null) lastError = "Failed to get CastContext";
111
+ Logger.error(TAG, lastError, null);
48
112
  }
113
+ return isInitialized;
49
114
 
50
115
  } catch (Exception e) {
51
- Logger.error(TAG, "Error initializing Cast SDK: " + e.getMessage(), e);
116
+ lastError = "Error initializing Cast SDK: " + e.getMessage();
117
+ Logger.error(TAG, lastError, e);
52
118
  return false;
53
119
  }
54
120
  }
121
+
122
+ public String getLastError() {
123
+ return lastError;
124
+ }
55
125
 
56
- /**
57
- * Check if the Cast SDK is initialized
58
- * @return true if initialized
59
- */
60
126
  public boolean isInitialized() {
61
127
  return isInitialized;
62
128
  }
63
-
64
- /**
65
- * Get the CastContext instance
66
- * @return CastContext or null if not initialized
67
- */
68
129
  public CastContext getCastContext() {
69
130
  return castContext;
70
131
  }
71
132
 
72
- public String echo(String value) {
73
- Logger.info("Echo", value);
74
- return value;
75
- }
76
-
77
133
  /**
78
- * Request a Cast session
79
- * Shows the Cast dialog and starts a session if a device is selected
80
- * @param context The application context
81
- * @return true if session started, false otherwise
134
+ * Verifica si hay sesión activa
82
135
  */
83
- public boolean requestSession(Context context) {
84
- if (!isInitialized || castContext == null) {
85
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
86
- return false;
136
+ public boolean isSessionActive() {
137
+ if (!isInitialized || castContext == null) return false;
138
+
139
+ // Consultar SessionManager en el hilo principal para obtener el estado real
140
+ final AtomicBoolean active = new AtomicBoolean(false);
141
+ Runnable check = () -> {
142
+ try {
143
+ CastSession s = castContext.getSessionManager().getCurrentCastSession();
144
+ active.set(s != null && s.isConnected());
145
+ } catch (Exception e) {
146
+ Logger.error(TAG, "Error checking session status: " + e.getMessage(), e);
147
+ active.set(false);
148
+ }
149
+ };
150
+
151
+ if (Looper.myLooper() == Looper.getMainLooper()) {
152
+ check.run();
153
+ return active.get();
87
154
  }
155
+
88
156
  try {
89
- // Show the Cast dialog
90
- castContext.getSessionManager().startSession(false);
91
- Logger.info(TAG, "Requested Cast session (dialog should appear)");
92
- return true;
93
- } catch (Exception e) {
94
- Logger.error(TAG, "Error requesting Cast session: " + e.getMessage(), e);
95
- return false;
157
+ CountDownLatch latch = new CountDownLatch(1);
158
+ new Handler(Looper.getMainLooper()).post(() -> {
159
+ check.run();
160
+ latch.countDown();
161
+ });
162
+ latch.await(3, TimeUnit.SECONDS);
163
+ } catch (InterruptedException ie) {
164
+ Logger.error(TAG, "Interrupted while checking session", ie);
96
165
  }
166
+
167
+ return active.get();
97
168
  }
98
-
169
+
99
170
  /**
100
- * Check if there is an active Cast session
101
- * @return true if session is active, false otherwise
171
+ * Verifica si hay dispositivos Cast disponibles mediante MediaRouter
102
172
  */
103
- public boolean isSessionActive() {
104
- if (!isInitialized || castContext == null) {
105
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
173
+ public boolean areDevicesAvailable() {
174
+ if (!isInitialized || castContext == null || appContext == null || mediaRouter == null || mediaRouteSelector == null) {
175
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
106
176
  return false;
107
177
  }
178
+
179
+ // MediaRouter debe consultarse en el hilo principal para obtener rutas válidas
180
+ final AtomicBoolean result = new AtomicBoolean(false);
181
+ Runnable scanRunnable = () -> result.set(runDeviceScan());
182
+
183
+ if (Looper.myLooper() == Looper.getMainLooper()) {
184
+ scanRunnable.run();
185
+ return result.get();
186
+ }
187
+
108
188
  try {
109
- boolean active = castContext.getSessionManager().getCurrentCastSession() != null;
110
- Logger.info(TAG, "Session active: " + active);
111
- return active;
112
- } catch (Exception e) {
113
- Logger.error(TAG, "Error checking session status: " + e.getMessage(), e);
114
- return false;
189
+ CountDownLatch latch = new CountDownLatch(1);
190
+ new Handler(Looper.getMainLooper()).post(() -> {
191
+ scanRunnable.run();
192
+ latch.countDown();
193
+ });
194
+ latch.await(6, TimeUnit.SECONDS);
195
+ } catch (InterruptedException ie) {
196
+ Logger.error(TAG, "Interrupted while checking devices", ie);
115
197
  }
198
+
199
+ return result.get();
116
200
  }
117
-
201
+
118
202
  /**
119
- * Check if there are available Cast devices
120
- * @return true if devices are available, false otherwise
203
+ * Realiza el escaneo de rutas Cast usando MediaRouter.
121
204
  */
122
- public boolean areDevicesAvailable() {
123
- if (!isInitialized || castContext == null) {
124
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
125
- return false;
126
- }
205
+ private boolean runDeviceScan() {
206
+ final AtomicBoolean found = new AtomicBoolean(false);
207
+ final CountDownLatch latch = new CountDownLatch(1);
208
+ MediaRouter.Callback discoveryCallback = new MediaRouter.Callback() {
209
+ @Override
210
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
211
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
212
+ found.set(true);
213
+ latch.countDown();
214
+ }
215
+ }
216
+
217
+ @Override
218
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
219
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
220
+ found.set(true);
221
+ latch.countDown();
222
+ }
223
+ }
224
+ };
225
+
127
226
  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;
227
+ synchronized (discoveryLock) {
228
+ mediaRouter.addCallback(
229
+ mediaRouteSelector,
230
+ discoveryCallback,
231
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY | MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
232
+ );
233
+
234
+ // Revisar rutas conocidas inmediatamente
235
+ for (MediaRouter.RouteInfo route : mediaRouter.getRoutes()) {
236
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
237
+ found.set(true);
238
+ break;
239
+ }
240
+ }
241
+
242
+ // Esperar algo de tiempo para descubrimiento activo
243
+ if (!found.get()) {
244
+ latch.await(4000, TimeUnit.MILLISECONDS);
245
+ }
246
+
247
+ // Revisión final de rutas conocidas antes de salir
248
+ if (!found.get()) {
249
+ for (MediaRouter.RouteInfo route : mediaRouter.getRoutes()) {
250
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
251
+ found.set(true);
252
+ break;
253
+ }
254
+ }
255
+ }
256
+
257
+ mediaRouter.removeCallback(discoveryCallback);
258
+ }
259
+ Logger.info(TAG, "areDevicesAvailable: found=" + found.get());
260
+ return found.get();
133
261
  } catch (Exception e) {
134
- Logger.error(TAG, "Error checking device availability: " + e.getMessage(), e);
262
+ Logger.error(TAG, "Error checking available devices: " + e.getMessage(), e);
135
263
  return false;
136
264
  }
137
265
  }
138
-
266
+
139
267
  /**
140
- * Load media on the Cast device
141
- * @param url The media URL
142
- * @param metadata Optional metadata (title, images, etc)
143
- * @return true if media loaded successfully
268
+ * Envía media al dispositivo Cast (flujo básico)
144
269
  */
145
- public boolean loadMedia(String url, MediaMetadataCompat metadata) {
270
+ public boolean loadMedia(String url, String title, String subtitle, String imageUrl, String contentType) {
146
271
  if (!isInitialized || castContext == null) {
147
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
272
+ lastError = "Cast SDK not initialized. Call initialize() first.";
273
+ Logger.error(TAG, lastError, null);
148
274
  return false;
149
275
  }
150
- try {
151
- if (url == null || url.isEmpty()) {
152
- Logger.error(TAG, "Media URL is required");
153
- return false;
276
+
277
+ lastError = null;
278
+ final AtomicBoolean success = new AtomicBoolean(false);
279
+ final CountDownLatch done = new CountDownLatch(1);
280
+ Handler mainHandler = new Handler(Looper.getMainLooper());
281
+
282
+ Runnable loadRunnable = () -> {
283
+ try {
284
+ if (url == null || url.isEmpty()) {
285
+ lastError = "Media URL is required";
286
+ Logger.error(TAG, lastError, null);
287
+ done.countDown();
288
+ return;
289
+ }
290
+
291
+ Logger.info(TAG, "loadMedia: url=" + url + ", contentType=" + contentType);
292
+
293
+ MediaMetadata md = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
294
+ if (title != null && !title.isEmpty()) md.putString(MediaMetadata.KEY_TITLE, title);
295
+ if (subtitle != null && !subtitle.isEmpty()) md.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
296
+ if (imageUrl != null && !imageUrl.isEmpty()) md.addImage(new WebImage(android.net.Uri.parse(imageUrl)));
297
+
298
+ String ct = (contentType != null && !contentType.isEmpty()) ? contentType : "video/mp4";
299
+ MediaInfo mediaInfo = new MediaInfo.Builder(url)
300
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
301
+ .setContentType(ct)
302
+ .setMetadata(md)
303
+ .build();
304
+
305
+ CastSession session = castContext.getSessionManager().getCurrentCastSession();
306
+ if (session == null || !session.isConnected()) {
307
+ lastError = "No active Cast session";
308
+ Logger.error(TAG, lastError, null);
309
+ done.countDown();
310
+ return;
311
+ }
312
+
313
+ try {
314
+ String appId = session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
315
+ String deviceName = session.getCastDevice() != null ? session.getCastDevice().getFriendlyName() : "";
316
+ Logger.info(TAG, "Session connected. appId=" + appId + ", device=" + deviceName);
317
+ } catch (Exception ignored) {}
318
+
319
+ RemoteMediaClient rmc = session.getRemoteMediaClient();
320
+ if (rmc == null) {
321
+ lastError = "RemoteMediaClient is null";
322
+ Logger.error(TAG, lastError, null);
323
+ done.countDown();
324
+ return;
325
+ }
326
+
327
+ MediaLoadRequestData req = new MediaLoadRequestData.Builder()
328
+ .setMediaInfo(mediaInfo)
329
+ .setAutoplay(true)
330
+ .setCurrentTime(0L)
331
+ .build();
332
+
333
+ try {
334
+ PendingResult<RemoteMediaClient.MediaChannelResult> pending = rmc.load(req);
335
+ if (pending == null) {
336
+ lastError = "rmc.load() returned null";
337
+ Logger.error(TAG, lastError, null);
338
+ done.countDown();
339
+ return;
340
+ }
341
+
342
+ pending.setResultCallback(result1 -> {
343
+ if (result1 != null && result1.getStatus() != null && result1.getStatus().isSuccess()) {
344
+ success.set(true);
345
+ Logger.info(TAG, "Media load success");
346
+ } else {
347
+ int statusCode = (result1 != null && result1.getStatus() != null) ? result1.getStatus().getStatusCode() : -1;
348
+ lastError = "Media load failed, statusCode=" + statusCode;
349
+ Logger.error(TAG, lastError, null);
350
+ }
351
+ done.countDown();
352
+ });
353
+ } catch (Exception e) {
354
+ lastError = "Error sending media load request: " + e.getMessage();
355
+ Logger.error(TAG, lastError, e);
356
+ done.countDown();
357
+ }
358
+ } catch (Exception e) {
359
+ lastError = "Error loading media: " + e.getMessage();
360
+ Logger.error(TAG, lastError, e);
361
+ done.countDown();
154
362
  }
155
- // Build Cast media info
156
- 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
363
+ };
364
+
365
+ // Always dispatch load to the main thread but wait off-main for completion
366
+ mainHandler.post(loadRunnable);
367
+
368
+ boolean awaited = false;
369
+ try {
370
+ awaited = done.await(14, TimeUnit.SECONDS);
371
+ } catch (InterruptedException ie) {
372
+ lastError = "Interrupted while loading media";
373
+ Logger.error(TAG, lastError, ie);
374
+ return false;
375
+ }
376
+
377
+ if (!awaited && !success.get()) {
378
+ try {
379
+ CastSession session = castContext.getSessionManager().getCurrentCastSession();
380
+ String appId = session != null && session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
381
+ String deviceName = session != null && session.getCastDevice() != null ? session.getCastDevice().getFriendlyName() : "";
382
+ lastError = "Media load timed out (appId=" + appId + ", device=" + deviceName + ")";
383
+ } catch (Exception e) {
384
+ lastError = "Media load timed out";
165
385
  }
166
- com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url)
167
- .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")
169
- .setMetadata(castMetadata)
170
- .build();
171
- com.google.android.gms.cast.framework.CastSession session = castContext.getSessionManager().getCurrentCastSession();
172
- if (session == null || !session.isConnected()) {
173
- Logger.error(TAG, "No active Cast session");
174
- return false;
386
+ Logger.error(TAG, lastError, null);
387
+ }
388
+
389
+ if (!success.get() && (lastError == null || lastError.isEmpty())) {
390
+ try {
391
+ CastSession session = castContext.getSessionManager().getCurrentCastSession();
392
+ String appId = session != null && session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
393
+ String deviceName = session != null && session.getCastDevice() != null ? session.getCastDevice().getFriendlyName() : "";
394
+ lastError = "Media load failed (unknown reason, appId=" + appId + ", device=" + deviceName + ")";
395
+ } catch (Exception e) {
396
+ lastError = "Media load failed (unknown reason)";
175
397
  }
176
- session.getRemoteMediaClient().load(mediaInfo, true, 0);
177
- Logger.info(TAG, "Media loaded to Cast device: " + url);
178
- return true;
179
- } catch (Exception e) {
180
- Logger.error(TAG, "Error loading media: " + e.getMessage(), e);
181
- return false;
398
+ Logger.error(TAG, lastError, null);
182
399
  }
400
+ return success.get();
183
401
  }
184
402
  }
@@ -5,7 +5,14 @@ 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;
9
16
 
10
17
  @CapacitorPlugin(name = "IonicChromecast")
11
18
  public class IonicChromecastPlugin extends Plugin {
@@ -29,6 +36,10 @@ public class IonicChromecastPlugin extends Plugin {
29
36
 
30
37
  JSObject ret = new JSObject();
31
38
  ret.put("success", success);
39
+ String initError = implementation.getLastError();
40
+ if (initError != null && !initError.isEmpty()) {
41
+ ret.put("error", initError);
42
+ }
32
43
 
33
44
  if (success) {
34
45
  call.resolve(ret);
@@ -37,95 +48,97 @@ public class IonicChromecastPlugin extends Plugin {
37
48
  }
38
49
  }
39
50
 
40
- @PluginMethod
41
- public void echo(PluginCall call) {
42
- String value = call.getString("value");
43
-
44
- JSObject ret = new JSObject();
45
- ret.put("value", implementation.echo(value));
46
- call.resolve(ret);
47
- }
48
-
49
51
  /**
50
- * Request a Cast session from JavaScript
52
+ * Muestra el selector nativo de dispositivos Cast
51
53
  */
52
54
  @PluginMethod
53
55
  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);
56
+ if (!implementation.isInitialized() || implementation.getCastContext() == null) {
57
+ JSObject err = new JSObject();
58
+ err.put("success", false);
59
+ err.put("message", "Cast SDK not initialized");
60
+ call.reject("Failed to request Cast session", err);
61
+ return;
63
62
  }
63
+
64
+ getActivity().runOnUiThread(() -> {
65
+ try {
66
+ AppCompatActivity activity = (AppCompatActivity) getActivity();
67
+ String receiverId = CastOptionsProvider.sReceiverApplicationId;
68
+ if (TextUtils.isEmpty(receiverId)) receiverId = "CC1AD845";
69
+
70
+ MediaRouteSelector selector = new MediaRouteSelector.Builder()
71
+ .addControlCategory(CastMediaControlIntent.categoryForCast(receiverId))
72
+ .build();
73
+
74
+ MediaRouteChooserDialog chooserDialog = new MediaRouteChooserDialog(activity, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar);
75
+ chooserDialog.setRouteSelector(selector);
76
+ chooserDialog.setCanceledOnTouchOutside(true);
77
+ chooserDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
78
+ @Override public void onCancel(DialogInterface dialog) {}
79
+ });
80
+ chooserDialog.show();
81
+
82
+ JSObject ret = new JSObject();
83
+ ret.put("success", true);
84
+ ret.put("message", "Cast chooser displayed");
85
+ call.resolve(ret);
86
+ } catch (Exception e) {
87
+ JSObject err = new JSObject();
88
+ err.put("success", false);
89
+ err.put("message", "Error showing chooser: " + e.getMessage());
90
+ call.reject("Failed to request Cast session", err);
91
+ }
92
+ });
64
93
  }
65
94
 
66
95
  /**
67
- * Check if there is an active Cast session from JavaScript
96
+ * Estado de sesión activa
68
97
  */
69
98
  @PluginMethod
70
99
  public void isSessionActive(PluginCall call) {
71
100
  boolean active = implementation.isSessionActive();
72
101
  JSObject ret = new JSObject();
73
102
  ret.put("active", active);
74
- if (active) {
75
- ret.put("message", "There is an active Cast session.");
76
- } else {
77
- ret.put("message", "No active Cast session.");
78
- }
79
103
  call.resolve(ret);
80
104
  }
81
105
 
82
106
  /**
83
- * Check if there are available Cast devices from JavaScript
107
+ * Carga media en el dispositivo
84
108
  */
85
109
  @PluginMethod
86
- public void areDevicesAvailable(PluginCall call) {
87
- boolean available = implementation.areDevicesAvailable();
110
+ public void loadMedia(PluginCall call) {
111
+ String url = call.getString("url");
112
+ JSObject metadataObj = call.getObject("metadata");
113
+
114
+ String title = metadataObj != null ? metadataObj.getString("title") : null;
115
+ String subtitle = metadataObj != null ? metadataObj.getString("subtitle") : null;
116
+ String contentType = metadataObj != null ? metadataObj.getString("contentType") : null;
117
+ String imageUrl = null;
118
+ if (metadataObj != null && metadataObj.has("images")) {
119
+ try { imageUrl = metadataObj.getJSONArray("images").optString(0, null); } catch (Exception ignore) {}
120
+ }
121
+
122
+ boolean ok = implementation.loadMedia(url, title, subtitle, imageUrl, contentType);
88
123
  JSObject ret = new JSObject();
89
- ret.put("available", available);
90
- if (available) {
91
- ret.put("message", "There are Cast devices available.");
124
+ ret.put("success", ok);
125
+ String err = implementation.getLastError();
126
+ if (err != null && !err.isEmpty()) ret.put("error", err);
127
+ if (ok) {
128
+ call.resolve(ret);
92
129
  } else {
93
- ret.put("message", "No Cast devices available.");
130
+ call.reject("Failed to load media", ret);
94
131
  }
95
- call.resolve(ret);
96
132
  }
97
133
 
98
134
  /**
99
- * Load media on the Cast device from JavaScript
135
+ * Revisa si hay dispositivos disponibles
100
136
  */
101
137
  @PluginMethod
102
- public void loadMedia(PluginCall call) {
103
- String url = call.getString("url");
104
- JSObject metadataObj = call.getObject("metadata");
105
- MediaMetadataCompat metadata = null;
106
- 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"));
110
- 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);
114
- }
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
- }
120
- boolean success = implementation.loadMedia(url, metadata);
138
+ public void areDevicesAvailable(PluginCall call) {
139
+ boolean available = implementation.areDevicesAvailable();
121
140
  JSObject ret = new JSObject();
122
- ret.put("success", success);
123
- if (success) {
124
- ret.put("message", "Media sent to Cast device.");
125
- call.resolve(ret);
126
- } else {
127
- ret.put("message", "Failed to send media. Check session and device.");
128
- call.reject("Failed to send media", ret);
129
- }
141
+ ret.put("available", available);
142
+ call.resolve(ret);
130
143
  }
131
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionic-chromecast",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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",
@@ -50,7 +50,10 @@
50
50
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
51
51
  "clean": "rimraf ./dist",
52
52
  "watch": "tsc --watch",
53
- "prepublishOnly": "npm run build"
53
+ "prepublishOnly": "npm run build",
54
+ "android:run": "cd example-app && npm install && npx cap sync android && npx cap run android",
55
+ "android:serve": "cd example-app && npm install && npm run dev -- --host --port 5173",
56
+ "android:run:live": "cd example-app && npm install && npx cap sync android && npx cap run android --livereload --external --livereload-url=http://localhost:5173"
54
57
  },
55
58
  "devDependencies": {
56
59
  "@capacitor/android": "^7.0.0",