ionic-chromecast 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,10 +5,12 @@ A Capacitor plugin for integrating Google Cast SDK (Chromecast) with Ionic/Capac
5
5
  ## Features
6
6
 
7
7
  - ✅ Initialize Google Cast SDK
8
+ - ✅ Session management (request, check status)
9
+ - ✅ Device discovery
10
+ - ✅ Media playback with rich metadata
8
11
  - ✅ Android support
12
+ - ✅ Event listeners
9
13
  - 🚧 iOS support (coming soon)
10
- - 🚧 Session management
11
- - 🚧 Media playback control
12
14
 
13
15
  ## Install
14
16
 
@@ -19,61 +21,315 @@ npx cap sync
19
21
 
20
22
  ## Android Configuration
21
23
 
22
- The plugin automatically configures the necessary permissions and Cast options. However, you may want to customize the default receiver application ID.
24
+ The plugin automatically configures the necessary permissions and Cast options:
25
+ - `INTERNET`
26
+ - `ACCESS_NETWORK_STATE`
27
+ - `ACCESS_WIFI_STATE`
23
28
 
24
- ### Default Receiver App ID
29
+ ### Requirements
30
+ - Android API 23+
31
+ - Google Play Services
32
+ - Chromecast device on the same WiFi network
25
33
 
26
- The plugin uses Google's default media receiver (`CC1AD845`) by default. You can override this by calling `initialize()` with your custom receiver application ID.
34
+ ## Quick Start
27
35
 
28
- ## Usage
36
+ ### 1. Initialize the Cast SDK
29
37
 
30
- ### Initialize the Cast SDK
38
+ ```typescript
39
+ import { IonicChromecast } from 'ionic-chromecast';
31
40
 
32
- Before using any Cast functionality, you must initialize the SDK:
41
+ // In your app.component.ts or main initialization
42
+ async initializeCast() {
43
+ const result = await IonicChromecast.initialize({
44
+ receiverApplicationId: 'CC1AD845' // Default Media Receiver
45
+ });
46
+
47
+ if (result.success) {
48
+ console.log('✅ Cast SDK ready!');
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### 2. Check for Available Devices
33
54
 
34
55
  ```typescript
35
- import { IonicChromecast } from 'ionic-chromecast';
56
+ async checkDevices() {
57
+ const { available } = await IonicChromecast.areDevicesAvailable();
58
+
59
+ if (available) {
60
+ console.log('📡 Chromecast devices found!');
61
+ } else {
62
+ console.log('❌ No devices available');
63
+ }
64
+ }
65
+ ```
36
66
 
37
- // Initialize with default media receiver
38
- await IonicChromecast.initialize({
39
- receiverApplicationId: 'CC1AD845' // Default Media Receiver
40
- });
67
+ ### 3. Start a Cast Session
41
68
 
42
- // Or use your custom receiver app ID
43
- await IonicChromecast.initialize({
44
- receiverApplicationId: 'YOUR_RECEIVER_APP_ID'
45
- });
69
+ ```typescript
70
+ async startCasting() {
71
+ const result = await IonicChromecast.requestSession();
72
+
73
+ if (result.success) {
74
+ console.log('🎬 Cast session started!');
75
+ }
76
+ }
46
77
  ```
47
78
 
48
- ### Best Practices
79
+ ### 4. Load Media
80
+
81
+ ```typescript
82
+ async playVideo() {
83
+ const result = await IonicChromecast.loadMedia({
84
+ url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
85
+ metadata: {
86
+ title: 'Big Buck Bunny',
87
+ subtitle: 'Blender Foundation',
88
+ images: ['https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg'],
89
+ contentType: 'video/mp4',
90
+ duration: 596
91
+ }
92
+ });
93
+
94
+ if (result.success) {
95
+ console.log('▶️ Video is playing on TV!');
96
+ }
97
+ }
98
+ ```
49
99
 
50
- - Call `initialize()` early in your app lifecycle (e.g., in `app.component.ts`)
51
- - Initialize only once per app session
52
- - Handle initialization errors appropriately
100
+ ## Complete Examples
53
101
 
54
- ## Example
102
+ ### Example 1: Basic Cast Integration
55
103
 
56
104
  ```typescript
57
105
  import { Component, OnInit } from '@angular/core';
58
106
  import { IonicChromecast } from 'ionic-chromecast';
59
107
 
60
108
  @Component({
61
- selector: 'app-root',
62
- templateUrl: 'app.component.html',
109
+ selector: 'app-home',
110
+ templateUrl: 'home.page.html',
111
+ })
112
+ export class HomePage implements OnInit {
113
+
114
+ castAvailable = false;
115
+ sessionActive = false;
116
+
117
+ async ngOnInit() {
118
+ // Initialize Cast SDK
119
+ await IonicChromecast.initialize({
120
+ receiverApplicationId: 'CC1AD845'
121
+ });
122
+
123
+ // Check for devices
124
+ const { available } = await IonicChromecast.areDevicesAvailable();
125
+ this.castAvailable = available;
126
+ }
127
+
128
+ async connectToTV() {
129
+ const result = await IonicChromecast.requestSession();
130
+ if (result.success) {
131
+ this.sessionActive = true;
132
+ }
133
+ }
134
+
135
+ async checkSession() {
136
+ const { active } = await IonicChromecast.isSessionActive();
137
+ this.sessionActive = active;
138
+ return active;
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### Example 2: Video Player with Cast
144
+
145
+ ```typescript
146
+ import { Component } from '@angular/core';
147
+ import { IonicChromecast } from 'ionic-chromecast';
148
+
149
+ @Component({
150
+ selector: 'app-player',
151
+ templateUrl: 'player.page.html',
152
+ })
153
+ export class PlayerPage {
154
+
155
+ async castVideo(videoUrl: string, videoTitle: string, posterUrl: string) {
156
+ // Check if session is active
157
+ const { active } = await IonicChromecast.isSessionActive();
158
+
159
+ if (!active) {
160
+ // Request session first
161
+ await IonicChromecast.requestSession();
162
+ }
163
+
164
+ // Load the video
165
+ const result = await IonicChromecast.loadMedia({
166
+ url: videoUrl,
167
+ metadata: {
168
+ title: videoTitle,
169
+ subtitle: 'Your App Name',
170
+ images: [posterUrl],
171
+ contentType: 'video/mp4'
172
+ }
173
+ });
174
+
175
+ if (result.success) {
176
+ console.log('🎥 Now casting:', videoTitle);
177
+ }
178
+ }
179
+
180
+ async castFromLibrary() {
181
+ const videos = [
182
+ {
183
+ title: 'Big Buck Bunny',
184
+ url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
185
+ poster: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg'
186
+ },
187
+ {
188
+ title: 'Elephants Dream',
189
+ url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
190
+ poster: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg'
191
+ }
192
+ ];
193
+
194
+ // Cast first video
195
+ await this.castVideo(videos[0].url, videos[0].title, videos[0].poster);
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### Example 3: Advanced Cast Controls
201
+
202
+ ```typescript
203
+ import { Component, OnDestroy } from '@angular/core';
204
+ import { IonicChromecast } from 'ionic-chromecast';
205
+
206
+ @Component({
207
+ selector: 'app-cast-control',
208
+ templateUrl: 'cast-control.page.html',
63
209
  })
64
- export class AppComponent implements OnInit {
210
+ export class CastControlPage implements OnDestroy {
211
+
212
+ private eventListeners: any[] = [];
65
213
 
66
214
  async ngOnInit() {
67
- try {
68
- const result = await IonicChromecast.initialize({
69
- receiverApplicationId: 'CC1AD845'
70
- });
71
-
72
- if (result.success) {
73
- console.log('Cast SDK initialized successfully');
215
+ // Initialize
216
+ await IonicChromecast.initialize({
217
+ receiverApplicationId: 'CC1AD845'
218
+ });
219
+
220
+ // Listen to events
221
+ this.setupEventListeners();
222
+ }
223
+
224
+ async setupEventListeners() {
225
+ const sessionHandle = await IonicChromecast.addListener('sessionStarted', (event) => {
226
+ console.log('✅ Session started:', event);
227
+ });
228
+
229
+ const mediaHandle = await IonicChromecast.addListener('mediaLoaded', (event) => {
230
+ console.log('🎬 Media loaded:', event);
231
+ });
232
+
233
+ this.eventListeners.push(sessionHandle, mediaHandle);
234
+ }
235
+
236
+ async fullCastWorkflow() {
237
+ // 1. Check for devices
238
+ const devicesResult = await IonicChromecast.areDevicesAvailable();
239
+ if (!devicesResult.available) {
240
+ alert('No Chromecast devices found. Make sure you are on the same WiFi network.');
241
+ return;
242
+ }
243
+
244
+ // 2. Request session
245
+ const sessionResult = await IonicChromecast.requestSession();
246
+ if (!sessionResult.success) {
247
+ alert('Failed to connect to Chromecast');
248
+ return;
249
+ }
250
+
251
+ // 3. Wait a moment for session to establish
252
+ await new Promise(resolve => setTimeout(resolve, 1000));
253
+
254
+ // 4. Load media
255
+ const mediaResult = await IonicChromecast.loadMedia({
256
+ url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
257
+ metadata: {
258
+ title: 'Big Buck Bunny',
259
+ subtitle: 'A Blender Open Movie',
260
+ studio: 'Blender Foundation',
261
+ images: [
262
+ 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg'
263
+ ],
264
+ contentType: 'video/mp4',
265
+ duration: 596 // seconds
74
266
  }
75
- } catch (error) {
76
- console.error('Failed to initialize Cast SDK:', error);
267
+ });
268
+
269
+ if (mediaResult.success) {
270
+ console.log('🎉 Successfully casting video!');
271
+ }
272
+ }
273
+
274
+ ngOnDestroy() {
275
+ // Clean up listeners
276
+ this.eventListeners.forEach(handle => handle.remove());
277
+ }
278
+ }
279
+ ```
280
+
281
+ ### Example 4: Cast Button Component
282
+
283
+ ```typescript
284
+ // cast-button.component.ts
285
+ import { Component, OnInit } from '@angular/core';
286
+ import { IonicChromecast } from 'ionic-chromecast';
287
+
288
+ @Component({
289
+ selector: 'app-cast-button',
290
+ template: `
291
+ <ion-button
292
+ *ngIf="devicesAvailable"
293
+ (click)="toggleCast()"
294
+ [color]="sessionActive ? 'primary' : 'medium'">
295
+ <ion-icon [name]="sessionActive ? 'wifi' : 'wifi-outline'"></ion-icon>
296
+ {{ sessionActive ? 'Casting' : 'Cast' }}
297
+ </ion-button>
298
+ `
299
+ })
300
+ export class CastButtonComponent implements OnInit {
301
+
302
+ devicesAvailable = false;
303
+ sessionActive = false;
304
+
305
+ async ngOnInit() {
306
+ await this.checkDevices();
307
+ await this.checkSession();
308
+
309
+ // Check periodically
310
+ setInterval(() => {
311
+ this.checkDevices();
312
+ this.checkSession();
313
+ }, 5000);
314
+ }
315
+
316
+ async checkDevices() {
317
+ const result = await IonicChromecast.areDevicesAvailable();
318
+ this.devicesAvailable = result.available;
319
+ }
320
+
321
+ async checkSession() {
322
+ const result = await IonicChromecast.isSessionActive();
323
+ this.sessionActive = result.active;
324
+ }
325
+
326
+ async toggleCast() {
327
+ if (this.sessionActive) {
328
+ // Session is active - could implement endSession() here
329
+ console.log('Session is active');
330
+ } else {
331
+ // Request new session
332
+ await IonicChromecast.requestSession();
77
333
  }
78
334
  }
79
335
  }
@@ -3,6 +3,8 @@ ext {
3
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
+ // Add a default version for MediaRouter if not provided by root project
7
+ androidxMediaRouterVersion = project.hasProperty('androidxMediaRouterVersion') ? rootProject.ext.androidxMediaRouterVersion : '1.6.0'
6
8
  }
7
9
 
8
10
  buildscript {
@@ -37,8 +39,9 @@ android {
37
39
  abortOnError false
38
40
  }
39
41
  compileOptions {
40
- sourceCompatibility JavaVersion.VERSION_21
41
- targetCompatibility JavaVersion.VERSION_21
42
+ // Use Java 17 for broader compatibility with current Android toolchains
43
+ sourceCompatibility JavaVersion.VERSION_17
44
+ targetCompatibility JavaVersion.VERSION_17
42
45
  }
43
46
  }
44
47
 
@@ -56,6 +59,12 @@ dependencies {
56
59
  // Google Cast SDK
57
60
  implementation 'com.google.android.gms:play-services-cast-framework:21.5.0'
58
61
 
62
+ // AndroidX MediaRouter (for Cast chooser/dialog)
63
+ implementation "androidx.mediarouter:mediarouter:$androidxMediaRouterVersion"
64
+
65
+ // AndroidX Media (required for MediaMetadataCompat)
66
+ implementation 'androidx.media:media:1.7.0'
67
+
59
68
  testImplementation "junit:junit:$junitVersion"
60
69
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
61
70
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -1,6 +1,7 @@
1
1
  package com.fabianacevedo.ionicchromecast;
2
2
 
3
3
  import android.content.Context;
4
+ import android.content.SharedPreferences;
4
5
  import com.google.android.gms.cast.framework.CastOptions;
5
6
  import com.google.android.gms.cast.framework.OptionsProvider;
6
7
  import com.google.android.gms.cast.framework.SessionProvider;
@@ -13,15 +14,32 @@ import java.util.List;
13
14
  */
14
15
  public class CastOptionsProvider implements OptionsProvider {
15
16
 
16
- // Default Media Receiver App ID
17
+ // Default Media Receiver App ID (Google's default receiver)
17
18
  private static final String DEFAULT_RECEIVER_APP_ID = "CC1AD845";
19
+ private static final String PREFS_NAME = "IonicChromecastPrefs";
20
+ private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
21
+
22
+ // Static variable to hold the receiver app ID before CastContext is initialized
23
+ public static String sReceiverApplicationId = null;
18
24
 
19
25
  @Override
20
26
  public CastOptions getCastOptions(Context context) {
21
- // Use default receiver app ID initially
22
- // This will be overridden when initialize() is called
27
+ String receiverAppId = DEFAULT_RECEIVER_APP_ID;
28
+
29
+ // Try to get from static variable first (set during initialize)
30
+ if (sReceiverApplicationId != null && !sReceiverApplicationId.isEmpty()) {
31
+ receiverAppId = sReceiverApplicationId;
32
+ } else {
33
+ // Fallback to SharedPreferences
34
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
35
+ String savedId = prefs.getString(KEY_RECEIVER_APP_ID, null);
36
+ if (savedId != null && !savedId.isEmpty()) {
37
+ receiverAppId = savedId;
38
+ }
39
+ }
40
+
23
41
  return new CastOptions.Builder()
24
- .setReceiverApplicationId(DEFAULT_RECEIVER_APP_ID)
42
+ .setReceiverApplicationId(receiverAppId)
25
43
  .build();
26
44
  }
27
45
 
@@ -1,17 +1,34 @@
1
1
  package com.fabianacevedo.ionicchromecast;
2
2
 
3
3
  import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+ import android.os.Handler;
6
+ import android.os.Looper;
4
7
  import com.getcapacitor.Logger;
5
8
  import com.google.android.gms.cast.framework.CastContext;
6
9
  import com.google.android.gms.cast.framework.CastOptions;
7
10
  import com.google.android.gms.cast.framework.OptionsProvider;
8
11
  import com.google.android.gms.cast.framework.SessionProvider;
9
12
  import com.google.android.gms.cast.CastMediaControlIntent;
13
+ import com.google.android.gms.common.images.WebImage;
10
14
  import java.util.List;
15
+ import java.util.concurrent.CountDownLatch;
16
+ import java.util.concurrent.TimeUnit;
17
+ import java.util.concurrent.atomic.AtomicBoolean;
18
+
19
+ // New imports for improved media loading
20
+ import com.google.android.gms.cast.MediaLoadRequestData;
21
+ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
22
+ import com.google.android.gms.common.api.PendingResult;
23
+ // Nuevos imports para esperar sesión activa
24
+ import com.google.android.gms.cast.framework.CastSession;
25
+ import com.google.android.gms.cast.framework.SessionManagerListener;
11
26
 
12
27
  public class IonicChromecast {
13
28
 
14
29
  private static final String TAG = "IonicChromecast";
30
+ private static final String PREFS_NAME = "IonicChromecastPrefs";
31
+ private static final String KEY_RECEIVER_APP_ID = "receiverApplicationId";
15
32
  private CastContext castContext;
16
33
  private boolean isInitialized = false;
17
34
 
@@ -29,23 +46,46 @@ public class IonicChromecast {
29
46
  }
30
47
 
31
48
  if (receiverApplicationId == null || receiverApplicationId.isEmpty()) {
32
- Logger.error(TAG, "Receiver Application ID is required");
49
+ Logger.error(TAG, "Receiver Application ID is required", null);
33
50
  return false;
34
51
  }
35
52
 
36
53
  Logger.info(TAG, "Initializing Cast SDK with receiver ID: " + receiverApplicationId);
37
54
 
38
- // Initialize CastContext
39
- castContext = CastContext.getSharedInstance(context);
55
+ // Save the receiver app ID to SharedPreferences for CastOptionsProvider
56
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
57
+ prefs.edit().putString(KEY_RECEIVER_APP_ID, receiverApplicationId).apply();
40
58
 
41
- if (castContext != null) {
42
- isInitialized = true;
43
- Logger.info(TAG, "Cast SDK initialized successfully");
44
- return true;
45
- } else {
46
- Logger.error(TAG, "Failed to get CastContext");
47
- return false;
48
- }
59
+ // Also set it in the static variable for immediate use
60
+ CastOptionsProvider.sReceiverApplicationId = receiverApplicationId;
61
+
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
+ try {
68
+ // Use application context per Cast SDK recommendations
69
+ Context appCtx = context.getApplicationContext();
70
+ castContext = CastContext.getSharedInstance(appCtx);
71
+ 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);
78
+ }
79
+ } catch (Exception e) {
80
+ Logger.error(TAG, "Error initializing Cast SDK on main thread: " + e.getMessage(), e);
81
+ initSuccess.set(false);
82
+ } finally {
83
+ latch.countDown();
84
+ }
85
+ });
86
+ // Wait up to 5 seconds for initialization to complete
87
+ latch.await(5, TimeUnit.SECONDS);
88
+ return initSuccess.get();
49
89
 
50
90
  } catch (Exception e) {
51
91
  Logger.error(TAG, "Error initializing Cast SDK: " + e.getMessage(), e);
@@ -82,13 +122,12 @@ public class IonicChromecast {
82
122
  */
83
123
  public boolean requestSession(Context context) {
84
124
  if (!isInitialized || castContext == null) {
85
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
125
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
86
126
  return false;
87
127
  }
88
128
  try {
89
- // Show the Cast dialog
90
- castContext.getSessionManager().startSession(false);
91
- Logger.info(TAG, "Requested Cast session (dialog should appear)");
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)");
92
131
  return true;
93
132
  } catch (Exception e) {
94
133
  Logger.error(TAG, "Error requesting Cast session: " + e.getMessage(), e);
@@ -102,12 +141,13 @@ public class IonicChromecast {
102
141
  */
103
142
  public boolean isSessionActive() {
104
143
  if (!isInitialized || castContext == null) {
105
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
144
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
106
145
  return false;
107
146
  }
108
147
  try {
109
- boolean active = castContext.getSessionManager().getCurrentCastSession() != null;
110
- Logger.info(TAG, "Session active: " + active);
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);
111
151
  return active;
112
152
  } catch (Exception e) {
113
153
  Logger.error(TAG, "Error checking session status: " + e.getMessage(), e);
@@ -121,64 +161,163 @@ public class IonicChromecast {
121
161
  */
122
162
  public boolean areDevicesAvailable() {
123
163
  if (!isInitialized || castContext == null) {
124
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
125
- return false;
126
- }
127
- try {
128
- // Check if there are any Cast devices discovered
129
- int deviceCount = castContext.getDiscoveryManager().getCastDeviceCount();
130
- boolean available = deviceCount > 0;
131
- Logger.info(TAG, "Devices available: " + available + " (" + deviceCount + ")");
132
- return available;
133
- } catch (Exception e) {
134
- Logger.error(TAG, "Error checking device availability: " + e.getMessage(), e);
164
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
135
165
  return false;
136
166
  }
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;
137
170
  }
138
171
 
139
172
  /**
140
173
  * Load media on the Cast device
141
174
  * @param url The media URL
142
- * @param metadata Optional metadata (title, images, etc)
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)
143
179
  * @return true if media loaded successfully
144
180
  */
145
- public boolean loadMedia(String url, MediaMetadataCompat metadata) {
181
+ public boolean loadMedia(String url, String title, String subtitle, String imageUrl, String contentType) {
146
182
  if (!isInitialized || castContext == null) {
147
- Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.");
183
+ Logger.error(TAG, "Cast SDK not initialized. Call initialize() first.", null);
148
184
  return false;
149
185
  }
150
186
  try {
151
187
  if (url == null || url.isEmpty()) {
152
- Logger.error(TAG, "Media URL is required");
188
+ Logger.error(TAG, "Media URL is required", null);
153
189
  return false;
154
190
  }
191
+
155
192
  // Build Cast media info
156
193
  com.google.android.gms.cast.MediaMetadata castMetadata = new com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_MOVIE);
157
- if (metadata != null) {
158
- if (metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE) != null)
159
- castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
160
- if (metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST) != null)
161
- castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_SUBTITLE, metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
162
- if (metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI) != null)
163
- castMetadata.addImage(new com.google.android.gms.cast.Image(android.net.Uri.parse(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI))));
164
- // Add more metadata fields as needed
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)));
165
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
+
166
214
  com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url)
167
215
  .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED)
168
- .setContentType(metadata != null && metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI) != null ? metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI) : "video/mp4")
216
+ .setContentType(finalContentType)
169
217
  .setMetadata(castMetadata)
170
218
  .build();
219
+
220
+ Logger.info(TAG, "✅ MediaInfo built successfully");
221
+
171
222
  com.google.android.gms.cast.framework.CastSession session = castContext.getSessionManager().getCurrentCastSession();
172
223
  if (session == null || !session.isConnected()) {
173
- Logger.error(TAG, "No active Cast session");
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;
229
+ }
230
+ session = castContext.getSessionManager().getCurrentCastSession();
231
+ }
232
+
233
+ RemoteMediaClient rmc = session.getRemoteMediaClient();
234
+ if (rmc == null) {
235
+ Logger.error(TAG, "RemoteMediaClient is null (session not ready)", null);
174
236
  return false;
175
237
  }
176
- session.getRemoteMediaClient().load(mediaInfo, true, 0);
177
- Logger.info(TAG, "Media loaded to Cast device: " + url);
178
- return true;
238
+
239
+ // Build a load request with autoplay
240
+ MediaLoadRequestData requestData = new MediaLoadRequestData.Builder()
241
+ .setMediaInfo(mediaInfo)
242
+ .setAutoplay(true)
243
+ .setCurrentTime(0L)
244
+ .build();
245
+
246
+ Logger.info(TAG, "Sending media load request: URL=" + url + ", contentType=" + finalContentType);
247
+
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);
261
+ }
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
+ }
270
+
271
+ return loadSuccess.get();
179
272
  } catch (Exception e) {
180
273
  Logger.error(TAG, "Error loading media: " + e.getMessage(), e);
181
274
  return false;
182
275
  }
183
276
  }
277
+
278
+ // Helper: espera una sesión activa hasta timeoutMs
279
+ private boolean waitForActiveSession(long timeoutMs) {
280
+ if (castContext == null) return false;
281
+ final CountDownLatch latch = new CountDownLatch(1);
282
+ final AtomicBoolean active = new AtomicBoolean(false);
283
+
284
+ SessionManagerListener<CastSession> listener = new SessionManagerListener<CastSession>() {
285
+ @Override public void onSessionStarted(CastSession session, String sessionId) {
286
+ active.set(session != null && session.isConnected());
287
+ latch.countDown();
288
+ }
289
+ @Override public void onSessionResumed(CastSession session, boolean wasSuspended) {
290
+ active.set(session != null && session.isConnected());
291
+ latch.countDown();
292
+ }
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
+ };
301
+
302
+ try {
303
+ // Si ya hay sesión conectada, devolver inmediatamente
304
+ CastSession current = castContext.getSessionManager().getCurrentCastSession();
305
+ if (current != null && current.isConnected()) {
306
+ return true;
307
+ }
308
+ castContext.getSessionManager().addSessionManagerListener(listener, CastSession.class);
309
+ // Esperar hasta timeout
310
+ latch.await(timeoutMs, TimeUnit.MILLISECONDS);
311
+ // Verificar de nuevo
312
+ current = castContext.getSessionManager().getCurrentCastSession();
313
+ return current != null && current.isConnected() || active.get();
314
+ } catch (Exception e) {
315
+ Logger.error(TAG, "Error waiting for Cast session: " + e.getMessage(), e);
316
+ return false;
317
+ } finally {
318
+ try {
319
+ castContext.getSessionManager().removeSessionManagerListener(listener, CastSession.class);
320
+ } catch (Exception ignore) {}
321
+ }
322
+ }
184
323
  }
@@ -5,13 +5,34 @@ import com.getcapacitor.Plugin;
5
5
  import com.getcapacitor.PluginCall;
6
6
  import com.getcapacitor.PluginMethod;
7
7
  import com.getcapacitor.annotation.CapacitorPlugin;
8
- import androidx.media.MediaMetadataCompat;
8
+
9
+ import android.text.TextUtils;
10
+ import com.google.android.gms.cast.CastMediaControlIntent;
11
+ import androidx.appcompat.app.AppCompatActivity;
12
+ import androidx.mediarouter.app.MediaRouteChooserDialog;
13
+ import androidx.mediarouter.media.MediaRouteSelector;
14
+ import androidx.mediarouter.media.MediaRouter;
15
+ import android.content.DialogInterface;
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;
9
22
 
10
23
  @CapacitorPlugin(name = "IonicChromecast")
11
24
  public class IonicChromecastPlugin extends Plugin {
12
25
 
13
26
  private IonicChromecast implementation = new IonicChromecast();
14
27
 
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
+
15
36
  /**
16
37
  * Initialize the Google Cast SDK
17
38
  * This method must be called before using any other Cast functionality
@@ -31,12 +52,104 @@ public class IonicChromecastPlugin extends Plugin {
31
52
  ret.put("success", success);
32
53
 
33
54
  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
+
34
113
  call.resolve(ret);
35
114
  } else {
36
115
  call.reject("Failed to initialize Cast SDK", ret);
37
116
  }
38
117
  }
39
118
 
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
+
40
153
  @PluginMethod
41
154
  public void echo(PluginCall call) {
42
155
  String value = call.getString("value");
@@ -47,20 +160,71 @@ public class IonicChromecastPlugin extends Plugin {
47
160
  }
48
161
 
49
162
  /**
50
- * Request a Cast session from JavaScript
163
+ * Request a Cast session from JavaScript - Uses Cast SDK's built-in device picker
51
164
  */
52
165
  @PluginMethod
53
166
  public void requestSession(PluginCall call) {
54
- boolean success = implementation.requestSession(getContext());
55
- JSObject ret = new JSObject();
56
- ret.put("success", success);
57
- if (success) {
58
- ret.put("message", "Cast session requested. Dialog should appear.");
59
- call.resolve(ret);
60
- } else {
61
- ret.put("message", "Failed to request Cast session. Make sure SDK is initialized.");
62
- call.reject("Failed to request Cast session", ret);
167
+ // Ensure Cast SDK initialized
168
+ if (!implementation.isInitialized()) {
169
+ JSObject err = new JSObject();
170
+ err.put("success", false);
171
+ err.put("message", "Cast SDK not initialized. Call initialize() first.");
172
+ call.reject("Failed to request Cast session", err);
173
+ return;
63
174
  }
175
+
176
+ getActivity().runOnUiThread(() -> {
177
+ 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
+ AppCompatActivity activity = (AppCompatActivity) getActivity();
191
+
192
+ String receiverId = CastOptionsProvider.sReceiverApplicationId;
193
+ if (TextUtils.isEmpty(receiverId)) {
194
+ receiverId = "CC1AD845";
195
+ }
196
+
197
+ MediaRouteSelector selector = new MediaRouteSelector.Builder()
198
+ .addControlCategory(CastMediaControlIntent.categoryForCast(receiverId))
199
+ .build();
200
+
201
+ MediaRouteChooserDialog chooserDialog = new MediaRouteChooserDialog(activity, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar);
202
+ chooserDialog.setRouteSelector(selector);
203
+ chooserDialog.setCanceledOnTouchOutside(true);
204
+ 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
+ }
209
+ });
210
+
211
+ // Show the dialog
212
+ chooserDialog.show();
213
+
214
+ com.getcapacitor.Logger.info("IonicChromecast", "✅ Cast device selector dialog displayed");
215
+
216
+ JSObject ret = new JSObject();
217
+ ret.put("success", true);
218
+ ret.put("message", "Cast chooser displayed.");
219
+ call.resolve(ret);
220
+ } catch (Exception e) {
221
+ com.getcapacitor.Logger.error("IonicChromecast", "❌ Error showing Cast chooser: " + e.getMessage(), e);
222
+ JSObject err = new JSObject();
223
+ err.put("success", false);
224
+ err.put("message", "Error showing Cast chooser: " + e.getMessage());
225
+ call.reject("Failed to request Cast session", err);
226
+ }
227
+ });
64
228
  }
65
229
 
66
230
  /**
@@ -102,30 +266,63 @@ public class IonicChromecastPlugin extends Plugin {
102
266
  public void loadMedia(PluginCall call) {
103
267
  String url = call.getString("url");
104
268
  JSObject metadataObj = call.getObject("metadata");
105
- MediaMetadataCompat metadata = null;
269
+
270
+ com.getcapacitor.Logger.info("IonicChromecast", "📥 loadMedia called with URL: " + url);
271
+ com.getcapacitor.Logger.info("IonicChromecast", "📥 metadata object: " + (metadataObj != null ? metadataObj.toString() : "null"));
272
+
273
+ // Extract metadata fields
274
+ String title = null;
275
+ String subtitle = null;
276
+ String imageUrl = null;
277
+ String contentType = null;
278
+
106
279
  if (metadataObj != null) {
107
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
108
- if (metadataObj.has("title")) builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, metadataObj.getString("title"));
109
- if (metadataObj.has("subtitle")) builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, metadataObj.getString("subtitle"));
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
110
287
  if (metadataObj.has("images")) {
111
- // Only use the first image for now
112
- String img = metadataObj.getJSONArray("images").optString(0, null);
113
- if (img != null) builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, img);
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
+ }
114
295
  }
115
- if (metadataObj.has("studio")) builder.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, metadataObj.getString("studio"));
116
- if (metadataObj.has("contentType")) builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, metadataObj.getString("contentType"));
117
- if (metadataObj.has("duration")) builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, metadataObj.getLong("duration"));
118
- metadata = builder.build();
119
296
  }
120
- boolean success = implementation.loadMedia(url, metadata);
297
+
298
+ boolean success = implementation.loadMedia(url, title, subtitle, imageUrl, contentType);
299
+
121
300
  JSObject ret = new JSObject();
122
301
  ret.put("success", success);
123
302
  if (success) {
124
303
  ret.put("message", "Media sent to Cast device.");
304
+ notifyListeners("mediaLoaded", ret);
125
305
  call.resolve(ret);
126
306
  } else {
127
307
  ret.put("message", "Failed to send media. Check session and device.");
308
+ notifyListeners("mediaError", ret);
128
309
  call.reject("Failed to send media", ret);
129
310
  }
130
311
  }
312
+
313
+ @Override
314
+ protected void handleOnDestroy() {
315
+ super.handleOnDestroy();
316
+ try {
317
+ CastContext cc = implementation.getCastContext();
318
+ if (cc != null && sessionListener != null) {
319
+ cc.getSessionManager().removeSessionManagerListener(sessionListener, CastSession.class);
320
+ }
321
+ } catch (Exception ignored) {}
322
+ try {
323
+ if (mediaRouter != null && mediaRouterCallback != null) {
324
+ mediaRouter.removeCallback(mediaRouterCallback);
325
+ }
326
+ } catch (Exception ignored) {}
327
+ }
131
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionic-chromecast",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
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",