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 +7 -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 +293 -214
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/IonicChromecastPlugin.java +37 -221
- package/package.json +5 -2
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
|
-
|
|
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
|
-
//
|
|
25
|
+
|
|
26
|
+
// Prefer static variable set by initialize()
|
|
30
27
|
if (sReceiverApplicationId != null && !sReceiverApplicationId.isEmpty()) {
|
|
31
28
|
receiverAppId = sReceiverApplicationId;
|
|
32
29
|
} else {
|
|
33
|
-
//
|
|
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.
|
|
10
|
-
import com.google.android.gms.
|
|
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
|
-
|
|
20
|
-
import com.google.android.gms.cast.
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
Context appCtx = context.getApplicationContext();
|
|
70
|
-
castContext = CastContext.getSharedInstance(appCtx);
|
|
77
|
+
castContext = CastContext.getSharedInstance(appContext);
|
|
71
78
|
if (castContext != null) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
124
|
-
if (!isInitialized || castContext == null)
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
*
|
|
140
|
-
* @return true if session is active, false otherwise
|
|
171
|
+
* Verifica si hay dispositivos Cast disponibles mediante MediaRouter
|
|
141
172
|
*/
|
|
142
|
-
public boolean
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
*
|
|
160
|
-
* @return true if devices are available, false otherwise
|
|
203
|
+
* Realiza el escaneo de rutas Cast usando MediaRouter.
|
|
161
204
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
272
|
+
lastError = "Cast SDK not initialized. Call initialize() first.";
|
|
273
|
+
Logger.error(TAG, lastError, null);
|
|
184
274
|
return false;
|
|
185
275
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
.
|
|
242
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
359
|
+
lastError = "Error loading media: " + e.getMessage();
|
|
360
|
+
Logger.error(TAG, lastError, e);
|
|
361
|
+
done.countDown();
|
|
269
362
|
}
|
|
363
|
+
};
|
|
270
364
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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().
|
|
320
|
-
|
|
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
|
-
*
|
|
52
|
+
* Muestra el selector nativo de dispositivos Cast
|
|
164
53
|
*/
|
|
165
54
|
@PluginMethod
|
|
166
55
|
public void requestSession(PluginCall call) {
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
299
|
-
|
|
121
|
+
|
|
122
|
+
boolean ok = implementation.loadMedia(url, title, subtitle, imageUrl, contentType);
|
|
300
123
|
JSObject ret = new JSObject();
|
|
301
|
-
ret.put("success",
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
"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",
|