ionic-chromecast 0.0.3 → 0.0.5

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.
@@ -6,23 +6,22 @@ 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.cast.framework.SessionManager;
10
+ import com.google.android.gms.common.ConnectionResult;
11
+ import com.google.android.gms.common.GoogleApiAvailability;
15
12
  import java.util.concurrent.CountDownLatch;
16
13
  import java.util.concurrent.TimeUnit;
17
14
  import java.util.concurrent.atomic.AtomicBoolean;
18
-
19
- // New imports for improved media loading
20
- import com.google.android.gms.cast.MediaLoadRequestData;
15
+ import androidx.mediarouter.media.MediaRouter;
16
+ import androidx.mediarouter.media.MediaRouteSelector;
17
+ import com.google.android.gms.cast.CastMediaControlIntent;
18
+ import com.google.android.gms.cast.MediaMetadata;
19
+ import com.google.android.gms.cast.MediaInfo;
20
+ import com.google.android.gms.cast.framework.CastSession;
21
21
  import com.google.android.gms.cast.framework.media.RemoteMediaClient;
22
+ import com.google.android.gms.cast.MediaLoadRequestData;
22
23
  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;
24
+ import com.google.android.gms.common.images.WebImage;
26
25
 
27
26
  public class IonicChromecast {
28
27
 
@@ -31,6 +30,11 @@ public class IonicChromecast {
31
30
  private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
32
31
  private CastContext castContext;
33
32
  private boolean isInitialized = false;
33
+ private Context appContext;
34
+ private String lastError = null;
35
+ private MediaRouter mediaRouter;
36
+ private MediaRouteSelector mediaRouteSelector;
37
+ private final Object discoveryLock = new Object();
34
38
 
35
39
  /**
36
40
  * Initialize the Google Cast SDK with the provided receiver application ID
@@ -40,6 +44,7 @@ public class IonicChromecast {
40
44
  */
41
45
  public boolean initialize(Context context, String receiverApplicationId) {
42
46
  try {
47
+ lastError = null;
43
48
  if (isInitialized) {
44
49
  Logger.info(TAG, "Cast SDK already initialized");
45
50
  return true;
@@ -49,275 +54,442 @@ public class IonicChromecast {
49
54
  Logger.error(TAG, "Receiver Application ID is required", null);
50
55
  return false;
51
56
  }
52
-
57
+
53
58
  Logger.info(TAG, "Initializing Cast SDK with receiver ID: " + receiverApplicationId);
59
+ Logger.info(TAG, "Thread at init: " + Thread.currentThread().getName());
54
60
 
55
- // Save the receiver app ID to SharedPreferences for CastOptionsProvider
56
61
  SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
57
62
  prefs.edit().putString(KEY_RECEIVER_APP_ID, receiverApplicationId).apply();
58
63
 
59
64
  // Also set it in the static variable for immediate use
60
65
  CastOptionsProvider.sReceiverApplicationId = receiverApplicationId;
61
66
 
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
+ int playStatus = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
68
+ Logger.info(TAG, "Google Play Services status=" + playStatus);
69
+ if (playStatus != ConnectionResult.SUCCESS) {
70
+ lastError = "Google Play Services status=" + playStatus;
71
+ Logger.error(TAG, lastError, null);
72
+ return false;
73
+ }
74
+ // Obtener CastContext y preparar MediaRouter siempre en el hilo principal
75
+ appContext = context.getApplicationContext();
76
+ Runnable initRunnable = () -> {
67
77
  try {
68
- // Use application context per Cast SDK recommendations
69
- Context appCtx = context.getApplicationContext();
70
- castContext = CastContext.getSharedInstance(appCtx);
78
+ castContext = CastContext.getSharedInstance(appContext);
71
79
  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);
80
+ mediaRouter = MediaRouter.getInstance(appContext);
81
+ mediaRouteSelector = new MediaRouteSelector.Builder()
82
+ .addControlCategory(CastMediaControlIntent.categoryForCast(CastOptionsProvider.sReceiverApplicationId != null ? CastOptionsProvider.sReceiverApplicationId : "CC1AD845"))
83
+ .build();
78
84
  }
79
85
  } catch (Exception e) {
80
- Logger.error(TAG, "Error initializing Cast SDK on main thread: " + e.getMessage(), e);
81
- initSuccess.set(false);
82
- } finally {
86
+ lastError = "Error initializing on main thread: " + e.getMessage();
87
+ Logger.error(TAG, lastError, e);
88
+ }
89
+ };
90
+
91
+ if (Looper.myLooper() == Looper.getMainLooper()) {
92
+ initRunnable.run();
93
+ } else {
94
+ final CountDownLatch latch = new CountDownLatch(1);
95
+ Handler mainHandler = new Handler(Looper.getMainLooper());
96
+ mainHandler.post(() -> {
97
+ initRunnable.run();
83
98
  latch.countDown();
99
+ });
100
+ boolean awaited = latch.await(6, TimeUnit.SECONDS);
101
+ if (!awaited && castContext == null && lastError == null) {
102
+ lastError = "Timed out waiting for CastContext on main thread";
103
+ Logger.error(TAG, lastError, null);
84
104
  }
85
- });
86
- // Wait up to 5 seconds for initialization to complete
87
- latch.await(5, TimeUnit.SECONDS);
88
- return initSuccess.get();
105
+ }
106
+
107
+ if (castContext != null) {
108
+ isInitialized = true;
109
+ Logger.info(TAG, "Cast SDK initialized successfully");
110
+ } else {
111
+ if (lastError == null) lastError = "Failed to get CastContext";
112
+ Logger.error(TAG, lastError, null);
113
+ }
114
+ return isInitialized;
89
115
 
90
116
  } catch (Exception e) {
91
- Logger.error(TAG, "Error initializing Cast SDK: " + e.getMessage(), e);
117
+ lastError = "Error initializing Cast SDK: " + e.getMessage();
118
+ Logger.error(TAG, lastError, e);
92
119
  return false;
93
120
  }
94
121
  }
122
+
123
+ public String getLastError() {
124
+ return lastError;
125
+ }
95
126
 
96
- /**
97
- * Check if the Cast SDK is initialized
98
- * @return true if initialized
99
- */
100
127
  public boolean isInitialized() {
101
128
  return isInitialized;
102
129
  }
103
-
104
- /**
105
- * Get the CastContext instance
106
- * @return CastContext or null if not initialized
107
- */
108
130
  public CastContext getCastContext() {
109
131
  return castContext;
110
132
  }
111
133
 
112
- public String echo(String value) {
113
- Logger.info("Echo", value);
114
- return value;
115
- }
116
-
117
134
  /**
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
135
+ * Verifica si hay sesión activa
122
136
  */
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;
137
+ public boolean isSessionActive() {
138
+ if (!isInitialized || castContext == null) return false;
139
+
140
+ // Consultar SessionManager en el hilo principal para obtener el estado real
141
+ final AtomicBoolean active = new AtomicBoolean(false);
142
+ Runnable check = () -> {
143
+ try {
144
+ CastSession s = castContext.getSessionManager().getCurrentCastSession();
145
+ boolean connected = s != null && s.isConnected();
146
+
147
+ // Evita falsos positivos: requiere appId y RemoteMediaClient disponibles
148
+ if (connected) {
149
+ try {
150
+ String appId = s.getApplicationMetadata() != null ? s.getApplicationMetadata().getApplicationId() : "";
151
+ RemoteMediaClient rmc = s.getRemoteMediaClient();
152
+ if (rmc == null || appId == null || appId.isEmpty()) {
153
+ connected = false;
154
+ }
155
+ } catch (Exception ignored) {
156
+ connected = false;
157
+ }
158
+ }
159
+
160
+ active.set(connected);
161
+ } catch (Exception e) {
162
+ Logger.error(TAG, "Error checking session status: " + e.getMessage(), e);
163
+ active.set(false);
164
+ }
165
+ };
166
+
167
+ if (Looper.myLooper() == Looper.getMainLooper()) {
168
+ check.run();
169
+ return active.get();
127
170
  }
171
+
128
172
  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;
173
+ CountDownLatch latch = new CountDownLatch(1);
174
+ new Handler(Looper.getMainLooper()).post(() -> {
175
+ check.run();
176
+ latch.countDown();
177
+ });
178
+ latch.await(3, TimeUnit.SECONDS);
179
+ } catch (InterruptedException ie) {
180
+ Logger.error(TAG, "Interrupted while checking session", ie);
135
181
  }
182
+
183
+ return active.get();
136
184
  }
137
-
185
+
138
186
  /**
139
- * Check if there is an active Cast session
140
- * @return true if session is active, false otherwise
187
+ * Finaliza la sesión Cast actual, si existe
141
188
  */
142
- public boolean isSessionActive() {
189
+ public boolean endSession() {
143
190
  if (!isInitialized || castContext == null) {
144
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
191
+ lastError = "Cast SDK not initialized. Call initialize() first.";
192
+ Logger.error(TAG, lastError, null);
145
193
  return false;
146
194
  }
195
+
196
+ lastError = null;
197
+ final AtomicBoolean success = new AtomicBoolean(false);
198
+ final CountDownLatch latch = new CountDownLatch(1);
199
+ Handler mainHandler = new Handler(Looper.getMainLooper());
200
+
201
+ Runnable endRunnable = () -> {
202
+ try {
203
+ SessionManager sm = castContext.getSessionManager();
204
+ if (sm == null) {
205
+ lastError = "SessionManager is null";
206
+ Logger.error(TAG, lastError, null);
207
+ latch.countDown();
208
+ return;
209
+ }
210
+
211
+ CastSession session = sm.getCurrentCastSession();
212
+ if (session == null || !session.isConnected()) {
213
+ lastError = "No active Cast session to end";
214
+ Logger.error(TAG, lastError, null);
215
+ latch.countDown();
216
+ return;
217
+ }
218
+
219
+ sm.endCurrentSession(true);
220
+ success.set(true);
221
+ Logger.info(TAG, "Cast session ended by request");
222
+ } catch (Exception e) {
223
+ lastError = "Error ending session: " + e.getMessage();
224
+ Logger.error(TAG, lastError, e);
225
+ } finally {
226
+ latch.countDown();
227
+ }
228
+ };
229
+
230
+ mainHandler.post(endRunnable);
231
+
147
232
  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;
155
- }
233
+ latch.await(4, TimeUnit.SECONDS);
234
+ } catch (InterruptedException ignored) {}
235
+
236
+ return success.get();
156
237
  }
157
-
238
+
158
239
  /**
159
- * Check if there are available Cast devices
160
- * @return true if devices are available, false otherwise
240
+ * Verifica si hay dispositivos Cast disponibles mediante MediaRouter
161
241
  */
162
242
  public boolean areDevicesAvailable() {
163
- if (!isInitialized || castContext == null) {
243
+ if (!isInitialized || castContext == null || appContext == null || mediaRouter == null || mediaRouteSelector == null) {
164
244
  Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
165
245
  return false;
166
246
  }
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;
247
+
248
+ // MediaRouter debe consultarse en el hilo principal para obtener rutas válidas
249
+ final AtomicBoolean result = new AtomicBoolean(false);
250
+ Runnable scanRunnable = () -> result.set(runDeviceScan());
251
+
252
+ if (Looper.myLooper() == Looper.getMainLooper()) {
253
+ scanRunnable.run();
254
+ return result.get();
255
+ }
256
+
257
+ try {
258
+ CountDownLatch latch = new CountDownLatch(1);
259
+ new Handler(Looper.getMainLooper()).post(() -> {
260
+ scanRunnable.run();
261
+ latch.countDown();
262
+ });
263
+ latch.await(6, TimeUnit.SECONDS);
264
+ } catch (InterruptedException ie) {
265
+ Logger.error(TAG, "Interrupted while checking devices", ie);
266
+ }
267
+
268
+ return result.get();
170
269
  }
171
-
270
+
172
271
  /**
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
272
+ * Realiza el escaneo de rutas Cast usando MediaRouter.
180
273
  */
181
- public boolean loadMedia(String url, String title, String subtitle, String imageUrl, String contentType) {
182
- if (!isInitialized || castContext == null) {
183
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
184
- return false;
185
- }
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;
274
+ private boolean runDeviceScan() {
275
+ final AtomicBoolean found = new AtomicBoolean(false);
276
+ final CountDownLatch latch = new CountDownLatch(1);
277
+ MediaRouter.Callback discoveryCallback = new MediaRouter.Callback() {
278
+ @Override
279
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
280
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
281
+ found.set(true);
282
+ latch.countDown();
229
283
  }
230
- session = castContext.getSessionManager().getCurrentCastSession();
231
284
  }
232
285
 
233
- RemoteMediaClient rmc = session.getRemoteMediaClient();
234
- if (rmc == null) {
235
- Logger.error(TAG, "RemoteMediaClient is null (session not ready)", null);
236
- return false;
286
+ @Override
287
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
288
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
289
+ found.set(true);
290
+ latch.countDown();
291
+ }
237
292
  }
293
+ };
238
294
 
239
- // Build a load request with autoplay
240
- MediaLoadRequestData requestData = new MediaLoadRequestData.Builder()
241
- .setMediaInfo(mediaInfo)
242
- .setAutoplay(true)
243
- .setCurrentTime(0L)
244
- .build();
295
+ try {
296
+ synchronized (discoveryLock) {
297
+ mediaRouter.addCallback(
298
+ mediaRouteSelector,
299
+ discoveryCallback,
300
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY | MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
301
+ );
245
302
 
246
- Logger.info(TAG, "Sending media load request: URL=" + url + ", contentType=" + finalContentType);
303
+ // Revisar rutas conocidas inmediatamente
304
+ for (MediaRouter.RouteInfo route : mediaRouter.getRoutes()) {
305
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
306
+ found.set(true);
307
+ break;
308
+ }
309
+ }
247
310
 
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);
311
+ // Esperar algo de tiempo para descubrimiento activo
312
+ if (!found.get()) {
313
+ latch.await(4000, TimeUnit.MILLISECONDS);
314
+ }
315
+
316
+ // Revisión final de rutas conocidas antes de salir
317
+ if (!found.get()) {
318
+ for (MediaRouter.RouteInfo route : mediaRouter.getRoutes()) {
319
+ if (route != null && route.matchesSelector(mediaRouteSelector) && !route.isDefault()) {
320
+ found.set(true);
321
+ break;
322
+ }
261
323
  }
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
- }
324
+ }
270
325
 
271
- return loadSuccess.get();
326
+ mediaRouter.removeCallback(discoveryCallback);
327
+ }
328
+ Logger.info(TAG, "areDevicesAvailable: found=" + found.get());
329
+ return found.get();
272
330
  } catch (Exception e) {
273
- Logger.error(TAG, "Error loading media: " + e.getMessage(), e);
331
+ Logger.error(TAG, "Error checking available devices: " + e.getMessage(), e);
274
332
  return false;
275
333
  }
276
334
  }
277
335
 
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);
336
+ /**
337
+ * Envía media al dispositivo Cast (flujo básico)
338
+ */
339
+ public boolean loadMedia(String url, String title, String subtitle, String imageUrl, String contentType) {
340
+ if (!isInitialized || castContext == null) {
341
+ lastError = "Cast SDK not initialized. Call initialize() first.";
342
+ Logger.error(TAG, lastError, null);
343
+ return false;
344
+ }
283
345
 
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();
346
+ lastError = null;
347
+ final AtomicBoolean success = new AtomicBoolean(false);
348
+ final CountDownLatch done = new CountDownLatch(1);
349
+ Handler mainHandler = new Handler(Looper.getMainLooper());
350
+
351
+ Runnable loadRunnable = () -> {
352
+ try {
353
+ if (url == null || url.isEmpty()) {
354
+ lastError = "Media URL is required";
355
+ Logger.error(TAG, lastError, null);
356
+ done.countDown();
357
+ return;
358
+ }
359
+
360
+ // Cache buster para evitar que el receiver siga mostrando el media anterior
361
+ String effectiveUrl = url;
362
+ try {
363
+ String suffix = (url != null && url.contains("?")) ? "&" : "?";
364
+ effectiveUrl = url + suffix + "_cb=" + System.currentTimeMillis();
365
+ } catch (Exception ignored) {}
366
+
367
+ Logger.info(TAG, "loadMedia: url=" + effectiveUrl + ", contentType=" + contentType);
368
+
369
+ MediaMetadata md = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
370
+ if (title != null && !title.isEmpty()) md.putString(MediaMetadata.KEY_TITLE, title);
371
+ if (subtitle != null && !subtitle.isEmpty()) md.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
372
+ if (imageUrl != null && !imageUrl.isEmpty()) md.addImage(new WebImage(android.net.Uri.parse(imageUrl)));
373
+
374
+ String ct = (contentType != null && !contentType.isEmpty()) ? contentType : "video/mp4";
375
+ MediaInfo mediaInfo = new MediaInfo.Builder(effectiveUrl)
376
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
377
+ .setContentType(ct)
378
+ .setMetadata(md)
379
+ .build();
380
+
381
+ CastSession session = castContext.getSessionManager().getCurrentCastSession();
382
+ if (session == null || !session.isConnected()) {
383
+ lastError = "No active Cast session";
384
+ Logger.error(TAG, lastError, null);
385
+ done.countDown();
386
+ return;
387
+ }
388
+
389
+ // Si la sesión es de otro appId, registra aviso pero intenta cargar igualmente
390
+ try {
391
+ String currentAppId = session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
392
+ String desiredAppId = CastOptionsProvider.sReceiverApplicationId;
393
+ if (desiredAppId != null && !desiredAppId.isEmpty() && !desiredAppId.equals(currentAppId)) {
394
+ Logger.warn(TAG, "Session appId=" + currentAppId + " differs from desired=" + desiredAppId + "; attempting load on current session");
395
+ }
396
+ } catch (Exception ignored) {}
397
+
398
+ try {
399
+ String appId = session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
400
+ String deviceName = session.getCastDevice() != null ? session.getCastDevice().getFriendlyName() : "";
401
+ Logger.info(TAG, "Session connected. appId=" + appId + ", device=" + deviceName);
402
+ } catch (Exception ignored) {}
403
+
404
+ RemoteMediaClient rmc = session.getRemoteMediaClient();
405
+ if (rmc == null) {
406
+ lastError = "RemoteMediaClient is null";
407
+ Logger.error(TAG, lastError, null);
408
+ done.countDown();
409
+ return;
410
+ }
411
+
412
+ // Detener lo que esté reproduciendo antes de cargar
413
+ try {
414
+ PendingResult<RemoteMediaClient.MediaChannelResult> stopPending = rmc.stop();
415
+ if (stopPending != null) {
416
+ stopPending.await(3, TimeUnit.SECONDS);
417
+ }
418
+ } catch (Exception ignored) {}
419
+
420
+ MediaLoadRequestData req = new MediaLoadRequestData.Builder()
421
+ .setMediaInfo(mediaInfo)
422
+ .setAutoplay(true)
423
+ .setCurrentTime(0L)
424
+ .build();
425
+
426
+ try {
427
+ PendingResult<RemoteMediaClient.MediaChannelResult> pending = rmc.load(req);
428
+ if (pending == null) {
429
+ lastError = "rmc.load() returned null";
430
+ Logger.error(TAG, lastError, null);
431
+ done.countDown();
432
+ return;
433
+ }
434
+
435
+ pending.setResultCallback(result1 -> {
436
+ if (result1 != null && result1.getStatus() != null && result1.getStatus().isSuccess()) {
437
+ success.set(true);
438
+ Logger.info(TAG, "Media load success");
439
+ } else {
440
+ int statusCode = (result1 != null && result1.getStatus() != null) ? result1.getStatus().getStatusCode() : -1;
441
+ lastError = "Media load failed, statusCode=" + statusCode;
442
+ Logger.error(TAG, lastError, null);
443
+ }
444
+ done.countDown();
445
+ });
446
+ } catch (Exception e) {
447
+ lastError = "Error sending media load request: " + e.getMessage();
448
+ Logger.error(TAG, lastError, e);
449
+ done.countDown();
450
+ }
451
+ } catch (Exception e) {
452
+ lastError = "Error loading media: " + e.getMessage();
453
+ Logger.error(TAG, lastError, e);
454
+ done.countDown();
292
455
  }
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
456
  };
301
457
 
458
+ // Always dispatch load to the main thread but wait off-main for completion
459
+ mainHandler.post(loadRunnable);
460
+
461
+ boolean awaited = false;
302
462
  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);
463
+ awaited = done.await(14, TimeUnit.SECONDS);
464
+ } catch (InterruptedException ie) {
465
+ lastError = "Interrupted while loading media";
466
+ Logger.error(TAG, lastError, ie);
316
467
  return false;
317
- } finally {
468
+ }
469
+
470
+ if (!awaited && !success.get()) {
318
471
  try {
319
- castContext.getSessionManager().removeSessionManagerListener(listener, CastSession.class);
320
- } catch (Exception ignore) {}
472
+ CastSession session = castContext.getSessionManager().getCurrentCastSession();
473
+ String appId = session != null && session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
474
+ String deviceName = session != null && session.getCastDevice() != null ? session.getCastDevice().getFriendlyName() : "";
475
+ lastError = "Media load timed out (appId=" + appId + ", device=" + deviceName + ")";
476
+ } catch (Exception e) {
477
+ lastError = "Media load timed out";
478
+ }
479
+ Logger.error(TAG, lastError, null);
480
+ }
481
+
482
+ if (!success.get() && (lastError == null || lastError.isEmpty())) {
483
+ try {
484
+ CastSession session = castContext.getSessionManager().getCurrentCastSession();
485
+ String appId = session != null && session.getApplicationMetadata() != null ? session.getApplicationMetadata().getApplicationId() : "";
486
+ String deviceName = session != null && session.getCastDevice() != null ? session.getCastDevice().getFriendlyName() : "";
487
+ lastError = "Media load failed (unknown reason, appId=" + appId + ", device=" + deviceName + ")";
488
+ } catch (Exception e) {
489
+ lastError = "Media load failed (unknown reason)";
490
+ }
491
+ Logger.error(TAG, lastError, null);
321
492
  }
493
+ return success.get();
322
494
  }
323
495
  }