ionic-chromecast 0.0.3 → 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.
@@ -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 -->
@@ -10,39 +10,38 @@ import java.util.List;
10
10
 
11
11
  /**
12
12
  * OptionsProvider for Google Cast SDK
13
- * This class is required by the Cast SDK to provide configuration options
14
13
  */
15
14
  public class CastOptionsProvider implements OptionsProvider {
16
-
17
- // Default Media Receiver App ID (Google's default receiver)
15
+
18
16
  private static final String DEFAULT_RECEIVER_APP_ID = "CC1AD845";
19
17
  private static final String PREFS_NAME = "IonicChromecastPrefs";
20
18
  private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
21
-
22
- // Static variable to hold the receiver app ID before CastContext is initialized
19
+
23
20
  public static String sReceiverApplicationId = null;
24
-
21
+
25
22
  @Override
26
23
  public CastOptions getCastOptions(Context context) {
27
24
  String receiverAppId = DEFAULT_RECEIVER_APP_ID;
28
-
29
- // Try to get from static variable first (set during initialize)
25
+
26
+ // Prefer static variable set by initialize()
30
27
  if (sReceiverApplicationId != null && !sReceiverApplicationId.isEmpty()) {
31
28
  receiverAppId = sReceiverApplicationId;
32
29
  } else {
33
- // Fallback to SharedPreferences
30
+ // Try from SharedPreferences
34
31
  SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
35
32
  String savedId = prefs.getString(KEY_RECEIVER_APP_ID, null);
36
33
  if (savedId != null && !savedId.isEmpty()) {
37
34
  receiverAppId = savedId;
38
35
  }
39
36
  }
40
-
37
+
41
38
  return new CastOptions.Builder()
42
39
  .setReceiverApplicationId(receiverAppId)
40
+ .setStopReceiverApplicationWhenEndingSession(true)
41
+ .setResumeSavedSession(false)
43
42
  .build();
44
43
  }
45
-
44
+
46
45
  @Override
47
46
  public List<SessionProvider> getAdditionalSessionProviders(Context context) {
48
47
  return null;
@@ -6,23 +6,21 @@ import android.os.Handler;
6
6
  import android.os.Looper;
7
7
  import com.getcapacitor.Logger;
8
8
  import com.google.android.gms.cast.framework.CastContext;
9
- import com.google.android.gms.cast.framework.CastOptions;
10
- import com.google.android.gms.cast.framework.OptionsProvider;
11
- import com.google.android.gms.cast.framework.SessionProvider;
12
- import com.google.android.gms.cast.CastMediaControlIntent;
13
- import com.google.android.gms.common.images.WebImage;
14
- import java.util.List;
9
+ import com.google.android.gms.common.ConnectionResult;
10
+ import com.google.android.gms.common.GoogleApiAvailability;
15
11
  import java.util.concurrent.CountDownLatch;
16
12
  import java.util.concurrent.TimeUnit;
17
13
  import java.util.concurrent.atomic.AtomicBoolean;
18
-
19
- // New imports for improved media loading
20
- import com.google.android.gms.cast.MediaLoadRequestData;
14
+ import androidx.mediarouter.media.MediaRouter;
15
+ import androidx.mediarouter.media.MediaRouteSelector;
16
+ import com.google.android.gms.cast.CastMediaControlIntent;
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;
21
20
  import com.google.android.gms.cast.framework.media.RemoteMediaClient;
21
+ import com.google.android.gms.cast.MediaLoadRequestData;
22
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;
23
+ import com.google.android.gms.common.images.WebImage;
26
24
 
27
25
  public class IonicChromecast {
28
26
 
@@ -31,6 +29,11 @@ public class IonicChromecast {
31
29
  private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
32
30
  private CastContext castContext;
33
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();
34
37
 
35
38
  /**
36
39
  * Initialize the Google Cast SDK with the provided receiver application ID
@@ -40,6 +43,7 @@ public class IonicChromecast {
40
43
  */
41
44
  public boolean initialize(Context context, String receiverApplicationId) {
42
45
  try {
46
+ lastError = null;
43
47
  if (isInitialized) {
44
48
  Logger.info(TAG, "Cast SDK already initialized");
45
49
  return true;
@@ -49,275 +53,350 @@ public class IonicChromecast {
49
53
  Logger.error(TAG, "Receiver Application ID is required", null);
50
54
  return false;
51
55
  }
52
-
56
+
53
57
  Logger.info(TAG, "Initializing Cast SDK with receiver ID: " + receiverApplicationId);
58
+ Logger.info(TAG, "Thread at init: " + Thread.currentThread().getName());
54
59
 
55
- // Save the receiver app ID to SharedPreferences for CastOptionsProvider
56
60
  SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
57
61
  prefs.edit().putString(KEY_RECEIVER_APP_ID, receiverApplicationId).apply();
58
62
 
59
63
  // Also set it in the static variable for immediate use
60
64
  CastOptionsProvider.sReceiverApplicationId = receiverApplicationId;
61
65
 
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(() -> {
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 = () -> {
67
76
  try {
68
- // Use application context per Cast SDK recommendations
69
- Context appCtx = context.getApplicationContext();
70
- castContext = CastContext.getSharedInstance(appCtx);
77
+ castContext = CastContext.getSharedInstance(appContext);
71
78
  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);
79
+ mediaRouter = MediaRouter.getInstance(appContext);
80
+ mediaRouteSelector = new MediaRouteSelector.Builder()
81
+ .addControlCategory(CastMediaControlIntent.categoryForCast(CastOptionsProvider.sReceiverApplicationId != null ? CastOptionsProvider.sReceiverApplicationId : "CC1AD845"))
82
+ .build();
78
83
  }
79
84
  } catch (Exception e) {
80
- Logger.error(TAG, "Error initializing Cast SDK on main thread: " + e.getMessage(), e);
81
- initSuccess.set(false);
82
- } finally {
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();
83
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);
84
103
  }
85
- });
86
- // Wait up to 5 seconds for initialization to complete
87
- latch.await(5, TimeUnit.SECONDS);
88
- return initSuccess.get();
104
+ }
105
+
106
+ if (castContext != null) {
107
+ isInitialized = true;
108
+ Logger.info(TAG, "Cast SDK initialized successfully");
109
+ } else {
110
+ if (lastError == null) lastError = "Failed to get CastContext";
111
+ Logger.error(TAG, lastError, null);
112
+ }
113
+ return isInitialized;
89
114
 
90
115
  } catch (Exception e) {
91
- Logger.error(TAG, "Error initializing Cast SDK: " + e.getMessage(), e);
116
+ lastError = "Error initializing Cast SDK: " + e.getMessage();
117
+ Logger.error(TAG, lastError, e);
92
118
  return false;
93
119
  }
94
120
  }
121
+
122
+ public String getLastError() {
123
+ return lastError;
124
+ }
95
125
 
96
- /**
97
- * Check if the Cast SDK is initialized
98
- * @return true if initialized
99
- */
100
126
  public boolean isInitialized() {
101
127
  return isInitialized;
102
128
  }
103
-
104
- /**
105
- * Get the CastContext instance
106
- * @return CastContext or null if not initialized
107
- */
108
129
  public CastContext getCastContext() {
109
130
  return castContext;
110
131
  }
111
132
 
112
- public String echo(String value) {
113
- Logger.info("Echo", value);
114
- return value;
115
- }
116
-
117
133
  /**
118
- * Request a Cast session
119
- * Shows the Cast dialog and starts a session if a device is selected
120
- * @param context The application context
121
- * @return true if session started, false otherwise
134
+ * Verifica si hay sesión activa
122
135
  */
123
- public boolean requestSession(Context context) {
124
- if (!isInitialized || castContext == null) {
125
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
126
- 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();
127
154
  }
155
+
128
156
  try {
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)");
131
- return true;
132
- } catch (Exception e) {
133
- Logger.error(TAG, "Error requesting Cast session: " + e.getMessage(), e);
134
- 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);
135
165
  }
166
+
167
+ return active.get();
136
168
  }
137
-
169
+
138
170
  /**
139
- * Check if there is an active Cast session
140
- * @return true if session is active, false otherwise
171
+ * Verifica si hay dispositivos Cast disponibles mediante MediaRouter
141
172
  */
142
- public boolean isSessionActive() {
143
- if (!isInitialized || castContext == null) {
173
+ public boolean areDevicesAvailable() {
174
+ if (!isInitialized || castContext == null || appContext == null || mediaRouter == null || mediaRouteSelector == null) {
144
175
  Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
145
176
  return false;
146
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
+
147
188
  try {
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);
151
- return active;
152
- } catch (Exception e) {
153
- Logger.error(TAG, "Error checking session status: " + e.getMessage(), e);
154
- 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);
155
197
  }
198
+
199
+ return result.get();
156
200
  }
157
-
201
+
158
202
  /**
159
- * Check if there are available Cast devices
160
- * @return true if devices are available, false otherwise
203
+ * Realiza el escaneo de rutas Cast usando MediaRouter.
161
204
  */
162
- public boolean areDevicesAvailable() {
163
- if (!isInitialized || castContext == null) {
164
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
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
+
226
+ try {
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();
261
+ } catch (Exception e) {
262
+ Logger.error(TAG, "Error checking available devices: " + e.getMessage(), e);
165
263
  return false;
166
264
  }
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;
170
265
  }
171
-
266
+
172
267
  /**
173
- * Load media on the Cast device
174
- * @param url The media URL
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)
179
- * @return true if media loaded successfully
268
+ * Envía media al dispositivo Cast (flujo básico)
180
269
  */
181
270
  public boolean loadMedia(String url, String title, String subtitle, String imageUrl, String contentType) {
182
271
  if (!isInitialized || castContext == null) {
183
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
272
+ lastError = "Cast SDK not initialized. Call initialize() first.";
273
+ Logger.error(TAG, lastError, null);
184
274
  return false;
185
275
  }
186
- try {
187
- if (url == null || url.isEmpty()) {
188
- Logger.error(TAG, "Media URL is required", null);
189
- return false;
190
- }
191
-
192
- // Build Cast media info
193
- com.google.android.gms.cast.MediaMetadata castMetadata = new com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_MOVIE);
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)));
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
-
214
- com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url)
215
- .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED)
216
- .setContentType(finalContentType)
217
- .setMetadata(castMetadata)
218
- .build();
219
-
220
- Logger.info(TAG, "✅ MediaInfo built successfully");
221
-
222
- com.google.android.gms.cast.framework.CastSession session = castContext.getSessionManager().getCurrentCastSession();
223
- if (session == null || !session.isConnected()) {
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;
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;
229
289
  }
230
- session = castContext.getSessionManager().getCurrentCastSession();
231
- }
232
290
 
233
- RemoteMediaClient rmc = session.getRemoteMediaClient();
234
- if (rmc == null) {
235
- Logger.error(TAG, "RemoteMediaClient is null (session not ready)", null);
236
- return false;
237
- }
291
+ Logger.info(TAG, "loadMedia: url=" + url + ", contentType=" + contentType);
238
292
 
239
- // Build a load request with autoplay
240
- MediaLoadRequestData requestData = new MediaLoadRequestData.Builder()
241
- .setMediaInfo(mediaInfo)
242
- .setAutoplay(true)
243
- .setCurrentTime(0L)
244
- .build();
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)));
245
297
 
246
- Logger.info(TAG, "Sending media load request: URL=" + url + ", contentType=" + finalContentType);
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();
247
304
 
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);
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;
261
340
  }
262
- latch.countDown();
263
- });
264
- // Wait up to 6 seconds for a result
265
- latch.await(6, TimeUnit.SECONDS);
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
+ }
266
358
  } catch (Exception e) {
267
- Logger.error(TAG, "Error sending media load request: " + e.getMessage(), e);
268
- return false;
359
+ lastError = "Error loading media: " + e.getMessage();
360
+ Logger.error(TAG, lastError, e);
361
+ done.countDown();
269
362
  }
363
+ };
270
364
 
271
- return loadSuccess.get();
272
- } catch (Exception e) {
273
- Logger.error(TAG, "Error loading media: " + e.getMessage(), e);
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);
274
374
  return false;
275
375
  }
276
- }
277
376
 
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();
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";
292
385
  }
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
- };
386
+ Logger.error(TAG, lastError, null);
387
+ }
301
388
 
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 {
389
+ if (!success.get() && (lastError == null || lastError.isEmpty())) {
318
390
  try {
319
- castContext.getSessionManager().removeSessionManagerListener(listener, CastSession.class);
320
- } catch (Exception ignore) {}
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)";
397
+ }
398
+ Logger.error(TAG, lastError, null);
321
399
  }
400
+ return success.get();
322
401
  }
323
402
  }
@@ -14,25 +14,11 @@ import androidx.mediarouter.media.MediaRouteSelector;
14
14
  import androidx.mediarouter.media.MediaRouter;
15
15
  import android.content.DialogInterface;
16
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;
22
-
23
17
  @CapacitorPlugin(name = "IonicChromecast")
24
18
  public class IonicChromecastPlugin extends Plugin {
25
19
 
26
20
  private IonicChromecast implementation = new IonicChromecast();
27
21
 
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
-
36
22
  /**
37
23
  * Initialize the Google Cast SDK
38
24
  * This method must be called before using any other Cast functionality
@@ -50,279 +36,109 @@ public class IonicChromecastPlugin extends Plugin {
50
36
 
51
37
  JSObject ret = new JSObject();
52
38
  ret.put("success", success);
39
+ String initError = implementation.getLastError();
40
+ if (initError != null && !initError.isEmpty()) {
41
+ ret.put("error", initError);
42
+ }
53
43
 
54
44
  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
-
113
45
  call.resolve(ret);
114
46
  } else {
115
47
  call.reject("Failed to initialize Cast SDK", ret);
116
48
  }
117
49
  }
118
50
 
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
-
153
- @PluginMethod
154
- public void echo(PluginCall call) {
155
- String value = call.getString("value");
156
-
157
- JSObject ret = new JSObject();
158
- ret.put("value", implementation.echo(value));
159
- call.resolve(ret);
160
- }
161
-
162
51
  /**
163
- * Request a Cast session from JavaScript - Uses Cast SDK's built-in device picker
52
+ * Muestra el selector nativo de dispositivos Cast
164
53
  */
165
54
  @PluginMethod
166
55
  public void requestSession(PluginCall call) {
167
- // Ensure Cast SDK initialized
168
- if (!implementation.isInitialized()) {
56
+ if (!implementation.isInitialized() || implementation.getCastContext() == null) {
169
57
  JSObject err = new JSObject();
170
58
  err.put("success", false);
171
- err.put("message", "Cast SDK not initialized. Call initialize() first.");
59
+ err.put("message", "Cast SDK not initialized");
172
60
  call.reject("Failed to request Cast session", err);
173
61
  return;
174
62
  }
175
63
 
176
64
  getActivity().runOnUiThread(() -> {
177
65
  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
66
  AppCompatActivity activity = (AppCompatActivity) getActivity();
191
-
192
67
  String receiverId = CastOptionsProvider.sReceiverApplicationId;
193
- if (TextUtils.isEmpty(receiverId)) {
194
- receiverId = "CC1AD845";
195
- }
196
-
68
+ if (TextUtils.isEmpty(receiverId)) receiverId = "CC1AD845";
69
+
197
70
  MediaRouteSelector selector = new MediaRouteSelector.Builder()
198
71
  .addControlCategory(CastMediaControlIntent.categoryForCast(receiverId))
199
72
  .build();
200
-
73
+
201
74
  MediaRouteChooserDialog chooserDialog = new MediaRouteChooserDialog(activity, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar);
202
75
  chooserDialog.setRouteSelector(selector);
203
76
  chooserDialog.setCanceledOnTouchOutside(true);
204
77
  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
- }
78
+ @Override public void onCancel(DialogInterface dialog) {}
209
79
  });
210
-
211
- // Show the dialog
212
80
  chooserDialog.show();
213
-
214
- com.getcapacitor.Logger.info("IonicChromecast", "✅ Cast device selector dialog displayed");
215
81
 
216
82
  JSObject ret = new JSObject();
217
83
  ret.put("success", true);
218
- ret.put("message", "Cast chooser displayed.");
84
+ ret.put("message", "Cast chooser displayed");
219
85
  call.resolve(ret);
220
86
  } catch (Exception e) {
221
- com.getcapacitor.Logger.error("IonicChromecast", "❌ Error showing Cast chooser: " + e.getMessage(), e);
222
87
  JSObject err = new JSObject();
223
88
  err.put("success", false);
224
- err.put("message", "Error showing Cast chooser: " + e.getMessage());
89
+ err.put("message", "Error showing chooser: " + e.getMessage());
225
90
  call.reject("Failed to request Cast session", err);
226
91
  }
227
92
  });
228
93
  }
229
94
 
230
95
  /**
231
- * Check if there is an active Cast session from JavaScript
96
+ * Estado de sesión activa
232
97
  */
233
98
  @PluginMethod
234
99
  public void isSessionActive(PluginCall call) {
235
100
  boolean active = implementation.isSessionActive();
236
101
  JSObject ret = new JSObject();
237
102
  ret.put("active", active);
238
- if (active) {
239
- ret.put("message", "There is an active Cast session.");
240
- } else {
241
- ret.put("message", "No active Cast session.");
242
- }
243
103
  call.resolve(ret);
244
104
  }
245
105
 
246
106
  /**
247
- * Check if there are available Cast devices from JavaScript
248
- */
249
- @PluginMethod
250
- public void areDevicesAvailable(PluginCall call) {
251
- boolean available = implementation.areDevicesAvailable();
252
- JSObject ret = new JSObject();
253
- ret.put("available", available);
254
- if (available) {
255
- ret.put("message", "There are Cast devices available.");
256
- } else {
257
- ret.put("message", "No Cast devices available.");
258
- }
259
- call.resolve(ret);
260
- }
261
-
262
- /**
263
- * Load media on the Cast device from JavaScript
107
+ * Carga media en el dispositivo
264
108
  */
265
109
  @PluginMethod
266
110
  public void loadMedia(PluginCall call) {
267
111
  String url = call.getString("url");
268
112
  JSObject metadataObj = call.getObject("metadata");
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;
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;
276
117
  String imageUrl = null;
277
- String contentType = null;
278
-
279
- if (metadataObj != null) {
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
287
- if (metadataObj.has("images")) {
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
- }
295
- }
118
+ if (metadataObj != null && metadataObj.has("images")) {
119
+ try { imageUrl = metadataObj.getJSONArray("images").optString(0, null); } catch (Exception ignore) {}
296
120
  }
297
-
298
- boolean success = implementation.loadMedia(url, title, subtitle, imageUrl, contentType);
299
-
121
+
122
+ boolean ok = implementation.loadMedia(url, title, subtitle, imageUrl, contentType);
300
123
  JSObject ret = new JSObject();
301
- ret.put("success", success);
302
- if (success) {
303
- ret.put("message", "Media sent to Cast device.");
304
- notifyListeners("mediaLoaded", ret);
124
+ ret.put("success", ok);
125
+ String err = implementation.getLastError();
126
+ if (err != null && !err.isEmpty()) ret.put("error", err);
127
+ if (ok) {
305
128
  call.resolve(ret);
306
129
  } else {
307
- ret.put("message", "Failed to send media. Check session and device.");
308
- notifyListeners("mediaError", ret);
309
- call.reject("Failed to send media", ret);
130
+ call.reject("Failed to load media", ret);
310
131
  }
311
132
  }
312
133
 
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) {}
134
+ /**
135
+ * Revisa si hay dispositivos disponibles
136
+ */
137
+ @PluginMethod
138
+ public void areDevicesAvailable(PluginCall call) {
139
+ boolean available = implementation.areDevicesAvailable();
140
+ JSObject ret = new JSObject();
141
+ ret.put("available", available);
142
+ call.resolve(ret);
327
143
  }
328
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionic-chromecast",
3
- "version": "0.0.3",
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",