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 +290 -34
- package/android/build.gradle +11 -2
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/CastOptionsProvider.java +22 -4
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/IonicChromecast.java +185 -46
- package/android/src/main/java/com/fabianacevedo/ionicchromecast/IonicChromecastPlugin.java +220 -23
- package/package.json +1 -1
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
|
|
24
|
+
The plugin automatically configures the necessary permissions and Cast options:
|
|
25
|
+
- `INTERNET`
|
|
26
|
+
- `ACCESS_NETWORK_STATE`
|
|
27
|
+
- `ACCESS_WIFI_STATE`
|
|
23
28
|
|
|
24
|
-
###
|
|
29
|
+
### Requirements
|
|
30
|
+
- Android API 23+
|
|
31
|
+
- Google Play Services
|
|
32
|
+
- Chromecast device on the same WiFi network
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
## Quick Start
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
### 1. Initialize the Cast SDK
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
```typescript
|
|
39
|
+
import { IonicChromecast } from 'ionic-chromecast';
|
|
31
40
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
await IonicChromecast.initialize({
|
|
39
|
-
receiverApplicationId: 'CC1AD845' // Default Media Receiver
|
|
40
|
-
});
|
|
67
|
+
### 3. Start a Cast Session
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
51
|
-
- Initialize only once per app session
|
|
52
|
-
- Handle initialization errors appropriately
|
|
100
|
+
## Complete Examples
|
|
53
101
|
|
|
54
|
-
|
|
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-
|
|
62
|
-
templateUrl: '
|
|
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
|
|
210
|
+
export class CastControlPage implements OnDestroy {
|
|
211
|
+
|
|
212
|
+
private eventListeners: any[] = [];
|
|
65
213
|
|
|
66
214
|
async ngOnInit() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
}
|
|
76
|
-
|
|
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
|
}
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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(
|
|
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
|
-
//
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
//
|
|
90
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
call.
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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