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.
- package/README.md +34 -1
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/CastOptionsProvider.java +10 -11
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/IonicChromecast.java +383 -211
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/IonicChromecastPlugin.java +120 -206
- package/dist/docs.json +10 -0
- package/dist/esm/definitions.d.ts +7 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +4 -0
- package/dist/esm/web.js +4 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +4 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +4 -0
- package/dist/plugin.js.map +1 -1
- package/package.json +5 -2
|
@@ -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.
|
|
10
|
-
import com.google.android.gms.
|
|
11
|
-
import com.google.android.gms.
|
|
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
|
-
|
|
20
|
-
import com.google.android.gms.cast.
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
Context appCtx = context.getApplicationContext();
|
|
70
|
-
castContext = CastContext.getSharedInstance(appCtx);
|
|
78
|
+
castContext = CastContext.getSharedInstance(appContext);
|
|
71
79
|
if (castContext != null) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
124
|
-
if (!isInitialized || castContext == null)
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
*
|
|
140
|
-
* @return true if session is active, false otherwise
|
|
187
|
+
* Finaliza la sesión Cast actual, si existe
|
|
141
188
|
*/
|
|
142
|
-
public boolean
|
|
189
|
+
public boolean endSession() {
|
|
143
190
|
if (!isInitialized || castContext == null) {
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
331
|
+
Logger.error(TAG, "Error checking available devices: " + e.getMessage(), e);
|
|
274
332
|
return false;
|
|
275
333
|
}
|
|
276
334
|
}
|
|
277
335
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!awaited && !success.get()) {
|
|
318
471
|
try {
|
|
319
|
-
castContext.getSessionManager().
|
|
320
|
-
|
|
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
|
}
|