@capgo/capacitor-jw-player 0.0.1
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/CapgoCapacitorJwPlayer.podspec +18 -0
- package/Package.swift +30 -0
- package/README.md +493 -0
- package/android/build.gradle +83 -0
- package/android/src/main/AndroidManifest.xml +37 -0
- package/android/src/main/java/ee/forgr/capacitor_jw_player/JwPlayerPlugin.java +529 -0
- package/android/src/main/java/ee/forgr/capacitor_jw_player/PlayerActivity.java +469 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/drawable/close_button_background.xml +5 -0
- package/android/src/main/res/layout/activity_player.xml +28 -0
- package/android/src/main/res/values/colors.xml +16 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +32 -0
- package/dist/docs.json +459 -0
- package/dist/esm/definitions.d.ts +156 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +57 -0
- package/dist/esm/web.js +65 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +79 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +82 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/JwPlayerPlugin/JwPlayer.swift +24 -0
- package/ios/Sources/JwPlayerPlugin/JwPlayerPlugin.swift +749 -0
- package/ios/Sources/JwPlayerPlugin/LinkedList.swift +219 -0
- package/ios/Sources/JwPlayerPlugin/VideosToPlay.swift +56 -0
- package/ios/Tests/JwPlayerPluginTests/JwPlayerPluginTests.swift +15 -0
- package/package.json +80 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import JWPlayerKit
|
|
4
|
+
import AVKit // Import AVKit for AVAudioSession
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Please read the Capacitor iOS Plugin Development Guide
|
|
8
|
+
* here: https://capacitorjs.com/docs/plugins/ios
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Protocol for callback handling
|
|
12
|
+
protocol CallbackHandler {
|
|
13
|
+
func notifyEventListener(_ eventName: String, data: [String: Any]?)
|
|
14
|
+
func onPlayerDismissed(isPiPDismissal: Bool) // Track dismissal source
|
|
15
|
+
func rePresentPlayerRequested() // Request plugin to re-present
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@objc(JwPlayerPlugin)
|
|
20
|
+
public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
21
|
+
public let identifier = "JwPlayerPlugin"
|
|
22
|
+
public let jsName = "JwPlayer"
|
|
23
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
24
|
+
CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
|
|
25
|
+
CAPPluginMethod(name: "play", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "pause", returnType: CAPPluginReturnPromise),
|
|
27
|
+
CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
|
|
28
|
+
CAPPluginMethod(name: "seekTo", returnType: CAPPluginReturnPromise),
|
|
29
|
+
CAPPluginMethod(name: "setVolume", returnType: CAPPluginReturnPromise),
|
|
30
|
+
CAPPluginMethod(name: "getPosition", returnType: CAPPluginReturnPromise),
|
|
31
|
+
CAPPluginMethod(name: "setSpeed", returnType: CAPPluginReturnPromise),
|
|
32
|
+
CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise),
|
|
33
|
+
CAPPluginMethod(name: "loadPlaylist", returnType: CAPPluginReturnPromise),
|
|
34
|
+
CAPPluginMethod(name: "loadPlaylistWithItems", returnType: CAPPluginReturnPromise),
|
|
35
|
+
CAPPluginMethod(name: "getAudioTracks", returnType: CAPPluginReturnPromise),
|
|
36
|
+
CAPPluginMethod(name: "getCurrentAudioTrack", returnType: CAPPluginReturnPromise),
|
|
37
|
+
CAPPluginMethod(name: "setCurrentAudioTrack", returnType: CAPPluginReturnPromise),
|
|
38
|
+
CAPPluginMethod(name: "getCaptions", returnType: CAPPluginReturnPromise),
|
|
39
|
+
CAPPluginMethod(name: "getCurrentCaptions", returnType: CAPPluginReturnPromise),
|
|
40
|
+
CAPPluginMethod(name: "setCurrentCaptions", returnType: CAPPluginReturnPromise),
|
|
41
|
+
CAPPluginMethod(name: "setPlaylistIndex", returnType: CAPPluginReturnPromise),
|
|
42
|
+
CAPPluginMethod(name: "currentPlaylist", returnType: CAPPluginReturnPromise),
|
|
43
|
+
CAPPluginMethod(name: "resume", returnType: CAPPluginReturnPromise)
|
|
44
|
+
]
|
|
45
|
+
private var viewController: CustomPlayerViewController? = nil
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
override public func load() {
|
|
49
|
+
// Configure the audio session when the plugin loads
|
|
50
|
+
setupAudioSession()
|
|
51
|
+
super.load()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private func setupAudioSession() {
|
|
55
|
+
print("[JWPlayer] Setting up AVAudioSession")
|
|
56
|
+
do {
|
|
57
|
+
// Configure audio session for playback and PiP
|
|
58
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback, options: [])
|
|
59
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
60
|
+
print("[JWPlayer] AVAudioSession configured successfully for playback")
|
|
61
|
+
} catch {
|
|
62
|
+
print("[JWPlayer] Error setting up AVAudioSession: \(error.localizedDescription)")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@objc func initialize(_ call: CAPPluginCall) {
|
|
67
|
+
print("[JWPlayer] initialize called")
|
|
68
|
+
guard let licenseKey = call.getString("licenseKey") else {
|
|
69
|
+
print("[JWPlayer] Error: licenseKey is missing")
|
|
70
|
+
call.reject("licenseKey is required for initialize")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
print("[JWPlayer] Setting license key")
|
|
75
|
+
JWPlayerKitLicense.setLicenseKey(licenseKey)
|
|
76
|
+
print("[JWPlayer] License key set successfully")
|
|
77
|
+
|
|
78
|
+
call.resolve()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@objc func play(_ call: CAPPluginCall) {
|
|
82
|
+
print("[JWPlayer] play called with options: \(call.options)")
|
|
83
|
+
guard let mediaUrlStr = call.getString("mediaUrl") else {
|
|
84
|
+
print("[JWPlayer] Error: mediaUrl is missing")
|
|
85
|
+
call.reject("mediaUrl is required for initialize")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
guard let mediaUrl = URL(string: mediaUrlStr) else {
|
|
90
|
+
print("[JWPlayer] Error: mediaUrl is invalid URL: \(mediaUrlStr)")
|
|
91
|
+
call.reject("mediaUrl is invalid URL")
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
guard let mediaType = call.getString("mediaType") else {
|
|
96
|
+
print("[JWPlayer] Error: mediaType is missing")
|
|
97
|
+
call.reject("mediaType is required for initialize")
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if self.viewController != nil {
|
|
102
|
+
print("[JWPlayer] Error: player is already active")
|
|
103
|
+
call.reject("the player is already active")
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
print("[JWPlayer] Media type: \(mediaType), URL: \(mediaUrl)")
|
|
108
|
+
|
|
109
|
+
// Create configuration directly
|
|
110
|
+
var config: JWPlayerConfiguration? = nil
|
|
111
|
+
do {
|
|
112
|
+
switch mediaType {
|
|
113
|
+
case "video":
|
|
114
|
+
print("[JWPlayer] Creating video configuration")
|
|
115
|
+
let item = try JWPlayerItemBuilder()
|
|
116
|
+
.file(mediaUrl)
|
|
117
|
+
.build()
|
|
118
|
+
config = try JWPlayerConfigurationBuilder()
|
|
119
|
+
.playlist(items: [item])
|
|
120
|
+
.build()
|
|
121
|
+
case "playlist":
|
|
122
|
+
print("[JWPlayer] Creating playlist configuration")
|
|
123
|
+
config = try JWPlayerConfigurationBuilder()
|
|
124
|
+
.playlist(url: mediaUrl)
|
|
125
|
+
.build()
|
|
126
|
+
default:
|
|
127
|
+
print("[JWPlayer] Error: Invalid mediaType: \(mediaType)")
|
|
128
|
+
call.reject("Invalid mediaType. Must be either video or playlist")
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
print("[JWPlayer] Error creating configuration: \(error.localizedDescription)")
|
|
133
|
+
call.reject("Error creating player configuration: \(error.localizedDescription)")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
guard let finalConfig = config else {
|
|
138
|
+
print("[JWPlayer] Error: Configuration is nil after creation")
|
|
139
|
+
call.reject("Failed to create player configuration")
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
144
|
+
print("[JWPlayer] Creating player view controller with config")
|
|
145
|
+
// Ensure any previous instance is fully dismissed first
|
|
146
|
+
if self.viewController != nil {
|
|
147
|
+
print("[JWPlayer] Warning: Play called while a player VC might still exist. Forcing cleanup.")
|
|
148
|
+
self.viewController = nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let viewController = CustomPlayerViewController(config: finalConfig, callbackHandler: self)
|
|
152
|
+
viewController.modalPresentationStyle = .fullScreen
|
|
153
|
+
self.viewController = viewController // Keep strong reference
|
|
154
|
+
print("[JWPlayer] Resolving play call")
|
|
155
|
+
call.resolve()
|
|
156
|
+
print("[JWPlayer] Presenting view controller")
|
|
157
|
+
self.bridge?.viewController?.present(viewController, animated: true)
|
|
158
|
+
print("[JWPlayer] View controller presented")
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@objc func pause(_ call: CAPPluginCall) {
|
|
163
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
164
|
+
guard let viewController = self.viewController else {
|
|
165
|
+
call.reject("No active player")
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
viewController.player.pause()
|
|
170
|
+
call.resolve()
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@objc func stop(_ call: CAPPluginCall) {
|
|
175
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
176
|
+
guard let viewController = self.viewController else {
|
|
177
|
+
call.reject("No active player")
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
viewController.player.stop()
|
|
182
|
+
call.resolve()
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@objc func seekTo(_ call: CAPPluginCall) {
|
|
187
|
+
guard let time = call.getDouble("time") else {
|
|
188
|
+
call.reject("time parameter is required")
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
193
|
+
guard let viewController = self.viewController else {
|
|
194
|
+
call.reject("No active player")
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
viewController.player.seek(to: TimeInterval(time))
|
|
199
|
+
call.resolve()
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@objc func setVolume(_ call: CAPPluginCall) {
|
|
204
|
+
guard let volume = call.getDouble("volume") else {
|
|
205
|
+
call.reject("volume parameter is required")
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
210
|
+
guard let viewController = self.viewController else {
|
|
211
|
+
call.reject("No active player")
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
viewController.player.volume = volume
|
|
216
|
+
call.resolve()
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@objc func getPosition(_ call: CAPPluginCall) {
|
|
221
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
222
|
+
guard let viewController = self.viewController else {
|
|
223
|
+
call.reject("No active player")
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
call.resolve(["position": viewController.player.time.position])
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@objc func getState(_ call: CAPPluginCall) {
|
|
232
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
233
|
+
guard let viewController = self.viewController else {
|
|
234
|
+
call.reject("No active player")
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
call.resolve(["state": viewController.player.getState().rawValue])
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@objc func setSpeed(_ call: CAPPluginCall) {
|
|
243
|
+
guard let speed = call.getDouble("speed") else {
|
|
244
|
+
call.reject("speed parameter is required")
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
249
|
+
guard let viewController = self.viewController else {
|
|
250
|
+
call.reject("No active player")
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
viewController.player.playbackRate = speed
|
|
255
|
+
call.resolve()
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@objc func setPlaylistIndex(_ call: CAPPluginCall) {
|
|
260
|
+
guard let index = call.getInt("index") else {
|
|
261
|
+
call.reject("index parameter is required")
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
266
|
+
guard let viewController = self.viewController else {
|
|
267
|
+
call.reject("No active player")
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Use the loadPlayerItemAt method which should be available
|
|
272
|
+
do {
|
|
273
|
+
viewController.player.loadPlayerItemAt(index: index)
|
|
274
|
+
call.resolve()
|
|
275
|
+
} catch {
|
|
276
|
+
call.reject("Failed to set playlist index: \(error.localizedDescription)")
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@objc func loadPlaylist(_ call: CAPPluginCall) {
|
|
282
|
+
guard let playlistUrlStr = call.getString("playlistUrl") else {
|
|
283
|
+
call.reject("playlistUrl parameter is required")
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
guard let playlistUrl = URL(string: playlistUrlStr) else {
|
|
288
|
+
call.reject("playlistUrl is invalid URL")
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
293
|
+
guard let viewController = self.viewController else {
|
|
294
|
+
call.reject("No active player")
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
viewController.player.loadPlaylist(url: playlistUrl)
|
|
299
|
+
call.resolve()
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@objc func loadPlaylistWithItems(_ call: CAPPluginCall) {
|
|
304
|
+
guard let playlist = call.getArray("playlist", [String: Any].self) else {
|
|
305
|
+
call.reject("playlist parameter is required")
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
310
|
+
guard let viewController = self.viewController else {
|
|
311
|
+
call.reject("No active player")
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var items: [JWPlayerItem] = []
|
|
316
|
+
|
|
317
|
+
for item in playlist {
|
|
318
|
+
if let fileStr = item["file"] as? String, let url = URL(string: fileStr) {
|
|
319
|
+
do {
|
|
320
|
+
let playerItem = try JWPlayerItemBuilder()
|
|
321
|
+
.file(url)
|
|
322
|
+
.build()
|
|
323
|
+
items.append(playerItem)
|
|
324
|
+
} catch {
|
|
325
|
+
call.reject("Error creating playlist item: \(error.localizedDescription)")
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if !items.isEmpty {
|
|
332
|
+
viewController.player.loadPlaylist(items: items)
|
|
333
|
+
call.resolve()
|
|
334
|
+
} else {
|
|
335
|
+
call.reject("No valid playlist items found")
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@objc func getAudioTracks(_ call: CAPPluginCall) {
|
|
341
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
342
|
+
guard let viewController = self.viewController else {
|
|
343
|
+
call.reject("No active player")
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let audioTracks = viewController.player.audioTracks
|
|
348
|
+
|
|
349
|
+
if audioTracks != nil && audioTracks.count > 0 {
|
|
350
|
+
var results: [[String: Any]] = []
|
|
351
|
+
for track in audioTracks {
|
|
352
|
+
let trackDict: [String: Any] = [
|
|
353
|
+
"language": track.extendedLanguageTag ?? "UNKNOWN",
|
|
354
|
+
"defaultTrack": track.defaultOption,
|
|
355
|
+
"name": track.name
|
|
356
|
+
]
|
|
357
|
+
results.append(trackDict)
|
|
358
|
+
}
|
|
359
|
+
call.resolve(["tracks": results])
|
|
360
|
+
} else {
|
|
361
|
+
call.resolve(["tracks": []])
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@objc func getCurrentAudioTrack(_ call: CAPPluginCall) {
|
|
367
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
368
|
+
guard let viewController = self.viewController else {
|
|
369
|
+
call.reject("No active player")
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
call.resolve(["index": viewController.player.currentAudioTrack])
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@objc func setCurrentAudioTrack(_ call: CAPPluginCall) {
|
|
378
|
+
guard let index = call.getInt("index") else {
|
|
379
|
+
call.reject("index parameter is required")
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
384
|
+
guard let viewController = self.viewController else {
|
|
385
|
+
call.reject("No active player")
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
viewController.player.currentAudioTrack = index
|
|
390
|
+
call.resolve()
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@objc func getCaptions(_ call: CAPPluginCall) {
|
|
395
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
396
|
+
guard let viewController = self.viewController else {
|
|
397
|
+
call.reject("No active player")
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// JWPlayer iOS SDK doesn't have a direct way to get all caption tracks
|
|
402
|
+
// But we can use the advanced settings to access them
|
|
403
|
+
let captionsTrackId = viewController.player.currentCaptionsTrack
|
|
404
|
+
|
|
405
|
+
// Create a basic response with just the current caption track
|
|
406
|
+
let captionsArray: [[String: Any]] = [
|
|
407
|
+
// Caption track 0 is usually "Off"
|
|
408
|
+
["index": 0, "label": "Off"],
|
|
409
|
+
// If there's a current caption track that's not 0, add it
|
|
410
|
+
["index": captionsTrackId, "label": "Current"]
|
|
411
|
+
].filter { dict in
|
|
412
|
+
// Only include the current track if it's not 0 (Off)
|
|
413
|
+
if let index = dict["index"] as? Int, index == 0 || index == captionsTrackId {
|
|
414
|
+
return true
|
|
415
|
+
}
|
|
416
|
+
return false
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
call.resolve(["captions": captionsArray])
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
@objc func getCurrentCaptions(_ call: CAPPluginCall) {
|
|
424
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
425
|
+
guard let viewController = self.viewController else {
|
|
426
|
+
call.reject("No active player")
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
call.resolve(["index": viewController.player.currentCaptionsTrack])
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@objc func setCurrentCaptions(_ call: CAPPluginCall) {
|
|
435
|
+
guard let index = call.getInt("index") else {
|
|
436
|
+
call.reject("index parameter is required")
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
441
|
+
guard let viewController = self.viewController else {
|
|
442
|
+
call.reject("No active player")
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
do {
|
|
447
|
+
try viewController.player.setCaptionTrack(index: index)
|
|
448
|
+
call.resolve()
|
|
449
|
+
} catch {
|
|
450
|
+
call.reject("Failed to set captions: \(error.localizedDescription)")
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
@objc func currentPlaylist(_ call: CAPPluginCall) {
|
|
456
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
457
|
+
guard let viewController = self.viewController else {
|
|
458
|
+
call.reject("No active player")
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Just return an empty array since we can't safely access playlist information
|
|
463
|
+
call.resolve([
|
|
464
|
+
"playlist": []
|
|
465
|
+
])
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@objc func resume(_ call: CAPPluginCall) {
|
|
470
|
+
print("[JWPlayer] resume called")
|
|
471
|
+
DispatchQueue.main.async(execute: DispatchWorkItem {
|
|
472
|
+
guard let viewController = self.viewController else {
|
|
473
|
+
print("[JWPlayer] Error: Cannot resume, no active player")
|
|
474
|
+
call.reject("No active player to resume")
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
print("[JWPlayer] Resuming playback")
|
|
479
|
+
viewController.player.play()
|
|
480
|
+
call.resolve()
|
|
481
|
+
print("[JWPlayer] Play command sent to player")
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Extend JwPlayerPlugin to implement the CallbackHandler protocol
|
|
487
|
+
extension JwPlayerPlugin: CallbackHandler {
|
|
488
|
+
func notifyEventListener(_ eventName: String, data: [String: Any]?) {
|
|
489
|
+
print("[JWPlayer] Event: \(eventName), Data: \(String(describing: data))")
|
|
490
|
+
notifyListeners(eventName, data: data)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
func onPlayerDismissed(isPiPDismissal: Bool) {
|
|
494
|
+
print("[JWPlayer] Player dismissed callback. Was PiP dismissal: \(isPiPDismissal)")
|
|
495
|
+
// Only nil out the reference if it wasn't a dismissal for starting PiP
|
|
496
|
+
if !isPiPDismissal {
|
|
497
|
+
print("[JWPlayer] Manual dismissal detected, releasing VC reference.")
|
|
498
|
+
self.viewController = nil
|
|
499
|
+
self.notifyListeners("playerDismissed", data: nil)
|
|
500
|
+
} else {
|
|
501
|
+
print("[JWPlayer] PiP dismissal detected, keeping VC reference for potential restore.")
|
|
502
|
+
// Optionally notify JS that PiP started if needed
|
|
503
|
+
self.notifyListeners("pipStarted", data: nil)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
func rePresentPlayerRequested() {
|
|
508
|
+
print("[JWPlayer] Re-present player requested")
|
|
509
|
+
guard let vc = self.viewController else {
|
|
510
|
+
print("[JWPlayer] Error: Cannot re-present, view controller is nil.")
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
guard let bridgeVC = self.bridge?.viewController else {
|
|
514
|
+
print("[JWPlayer] Error: Cannot re-present, bridge view controller is nil.")
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Ensure it's not already being presented or presenting something else
|
|
519
|
+
if vc.isBeingPresented || vc.presentingViewController != nil || bridgeVC.presentedViewController != nil {
|
|
520
|
+
print("[JWPlayer] Warning: Cannot re-present, presentation context busy.")
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
DispatchQueue.main.async {
|
|
525
|
+
print("[JWPlayer] Re-presenting player view controller")
|
|
526
|
+
bridgeVC.present(vc, animated: true)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Add AVPictureInPictureControllerDelegate conformance
|
|
532
|
+
class CustomPlayerViewController: JWPlayerViewController, JWPlayerViewControllerUIDelegate {
|
|
533
|
+
func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, sizeChangedFrom oldSize: CGSize, to newSize: CGSize) {
|
|
534
|
+
print("[JWPlayer] Size changed from \(oldSize) to \(newSize)")
|
|
535
|
+
let sizeData: [String: Any] = [
|
|
536
|
+
"oldSize": ["width": oldSize.width, "height": oldSize.height],
|
|
537
|
+
"newSize": ["width": newSize.width, "height": newSize.height]
|
|
538
|
+
]
|
|
539
|
+
callbackHandler?.notifyEventListener("playerSizeChange", data: sizeData)
|
|
540
|
+
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, screenTappedAt position: CGPoint) {
|
|
544
|
+
print("[JWPlayer] Screen tapped at: \(position)")
|
|
545
|
+
let positionData: [String: Any] = ["x": position.x, "y": position.y]
|
|
546
|
+
callbackHandler?.notifyEventListener("screenTapped", data: positionData)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
private var callbackHandler: CallbackHandler?
|
|
551
|
+
private var playerConfig: JWPlayerConfiguration?
|
|
552
|
+
private var closeButton: UIButton! // Custom close button
|
|
553
|
+
|
|
554
|
+
// Standard init is unavailable
|
|
555
|
+
@available(*, unavailable)
|
|
556
|
+
init() {
|
|
557
|
+
fatalError("init() is unavailable, use init(config:callbackHandler:)")
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Designated initializer
|
|
561
|
+
init(config: JWPlayerConfiguration, callbackHandler: CallbackHandler? = nil) {
|
|
562
|
+
print("[JWPlayer] CustomPlayerViewController init with config")
|
|
563
|
+
self.playerConfig = config
|
|
564
|
+
self.callbackHandler = callbackHandler
|
|
565
|
+
super.init(nibName: nil, bundle: nil)
|
|
566
|
+
|
|
567
|
+
// Set UI delegate
|
|
568
|
+
self.uiDelegate = self
|
|
569
|
+
print("[JWPlayer] Set self as JWPlayerViewControllerUIDelegate")
|
|
570
|
+
|
|
571
|
+
// Enable PiP
|
|
572
|
+
self.allowsPictureInPicturePlayback = true
|
|
573
|
+
print("[JWPlayer] allowsPictureInPicturePlayback set to true")
|
|
574
|
+
|
|
575
|
+
setupFullscreenConfig()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
required init?(coder: NSCoder) {
|
|
579
|
+
print("[JWPlayer] CustomPlayerViewController init from coder - Not recommended")
|
|
580
|
+
self.playerConfig = nil
|
|
581
|
+
super.init(coder: coder)
|
|
582
|
+
|
|
583
|
+
// Set UI delegate
|
|
584
|
+
self.uiDelegate = self
|
|
585
|
+
print("[JWPlayer] Set self as JWPlayerViewControllerUIDelegate (coder init)")
|
|
586
|
+
|
|
587
|
+
// Enable PiP even if initialized from coder
|
|
588
|
+
self.allowsPictureInPicturePlayback = true
|
|
589
|
+
print("[JWPlayer] allowsPictureInPicturePlayback set to true (coder init)")
|
|
590
|
+
|
|
591
|
+
setupFullscreenConfig()
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private func setupFullscreenConfig() {
|
|
595
|
+
print("[JWPlayer] Setting up fullscreen config")
|
|
596
|
+
// Ensure player is always fullscreen
|
|
597
|
+
modalPresentationStyle = .fullScreen
|
|
598
|
+
|
|
599
|
+
// Configure the player view to fill the screen
|
|
600
|
+
view.frame = UIScreen.main.bounds
|
|
601
|
+
self.forceFullScreenOnLandscape = false
|
|
602
|
+
self.forceLandscapeOnFullScreen = false
|
|
603
|
+
print("[JWPlayer] Fullscreen config set")
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
override func viewDidLoad() {
|
|
607
|
+
print("[JWPlayer] viewDidLoad")
|
|
608
|
+
super.viewDidLoad()
|
|
609
|
+
|
|
610
|
+
// Apply the configuration passed during initialization
|
|
611
|
+
if let config = self.playerConfig {
|
|
612
|
+
print("[JWPlayer] Configuring player from init config")
|
|
613
|
+
player.configurePlayer(with: config)
|
|
614
|
+
print("[JWPlayer] Player configured successfully from init config")
|
|
615
|
+
|
|
616
|
+
} else {
|
|
617
|
+
print("[JWPlayer] Error: Player configuration is missing in viewDidLoad")
|
|
618
|
+
self.callbackHandler?.notifyEventListener("error", data: ["message": "Player configuration missing"])
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Add the custom close button
|
|
622
|
+
setupCloseButton()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
override func viewDidAppear(_ animated: Bool) {
|
|
626
|
+
print("[JWPlayer] viewDidAppear")
|
|
627
|
+
super.viewDidAppear(animated)
|
|
628
|
+
|
|
629
|
+
// Start playback automatically
|
|
630
|
+
// print("[JWPlayer] Starting playback")
|
|
631
|
+
// // player.play()
|
|
632
|
+
// print("[JWPlayer] Playback started")
|
|
633
|
+
print("[JWPlayer] Button exists \(view.viewWithTag(2136) !== nil)")
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Setup and add the custom close button
|
|
637
|
+
private func setupCloseButton() {
|
|
638
|
+
closeButton = UIButton(type: .custom)
|
|
639
|
+
closeButton.setTitle("✕", for: .normal)
|
|
640
|
+
closeButton.setTitleColor(.white, for: .normal)
|
|
641
|
+
closeButton.backgroundColor = UIColor(white: 0, alpha: 0.6)
|
|
642
|
+
closeButton.layer.cornerRadius = 15 // Smaller corner radius
|
|
643
|
+
closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold)
|
|
644
|
+
closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .allTouchEvents)
|
|
645
|
+
closeButton.tag = 2136
|
|
646
|
+
|
|
647
|
+
// Position the button in the top-left corner
|
|
648
|
+
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
649
|
+
view.addSubview(closeButton)
|
|
650
|
+
|
|
651
|
+
NSLayoutConstraint.activate([
|
|
652
|
+
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15),
|
|
653
|
+
closeButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 15),
|
|
654
|
+
closeButton.widthAnchor.constraint(equalToConstant: 30),
|
|
655
|
+
closeButton.heightAnchor.constraint(equalToConstant: 30)
|
|
656
|
+
])
|
|
657
|
+
|
|
658
|
+
// Make sure it's always on top
|
|
659
|
+
closeButton.layer.zPosition = 1000
|
|
660
|
+
// Initially visible, will be updated by delegate method
|
|
661
|
+
closeButton.alpha = 1.0
|
|
662
|
+
print("[JWPlayer] Custom close button added")
|
|
663
|
+
|
|
664
|
+
self.setVisibility(.hidden, for: [.fullscreenButton])
|
|
665
|
+
print("[JWPlayer] Fullscreen button hidden")
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Action for the custom close button
|
|
669
|
+
@objc private func closeButtonTapped() {
|
|
670
|
+
print("[JWPlayer] Custom close button tapped - dismissing player manually")
|
|
671
|
+
self.dismiss(animated: true) { [weak self] in
|
|
672
|
+
// Use the new callback signature
|
|
673
|
+
self?.callbackHandler?.onPlayerDismissed(isPiPDismissal: false)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// MARK: - JWPlayerViewControllerUIDelegate Method
|
|
678
|
+
|
|
679
|
+
func playerViewController(_ controller: JWPlayerViewController, controlBarVisibilityChanged isVisible: Bool, frame: CGRect) {
|
|
680
|
+
print("[JWPlayer] Control bar visibility changed: \(isVisible)")
|
|
681
|
+
let targetAlpha: CGFloat = isVisible ? 1.0 : 0.0
|
|
682
|
+
|
|
683
|
+
// Set alpha directly without animation
|
|
684
|
+
// Ensure closeButton is not nil before accessing
|
|
685
|
+
guard let button = self.closeButton else { return }
|
|
686
|
+
button.alpha = targetAlpha
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// MARK: - AVPictureInPictureControllerDelegate Methods
|
|
690
|
+
|
|
691
|
+
override func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
|
692
|
+
print("[JWPlayer] Delegate: PiP Will Start")
|
|
693
|
+
if #available(iOS 14.2, *) {
|
|
694
|
+
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true
|
|
695
|
+
print("[JWPlayer] Set canStartPictureInPictureAutomaticallyFromInline to true. It's current value: \(pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline)")
|
|
696
|
+
}
|
|
697
|
+
super.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
override func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
|
701
|
+
print("[JWPlayer] Delegate: PiP Did Start")
|
|
702
|
+
super.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
|
|
706
|
+
print("[JWPlayer] Delegate: PiP Failed to Start: \(error.localizedDescription)")
|
|
707
|
+
super.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
override func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
|
711
|
+
print("[JWPlayer] Delegate: PiP Will Stop")
|
|
712
|
+
super.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
override func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
|
716
|
+
print("[JWPlayer] Delegate: PiP Did Stop")
|
|
717
|
+
super.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
|
|
718
|
+
|
|
719
|
+
// If PiP is stopped directly by the user without restoration, notify the plugin
|
|
720
|
+
// print("[JWPlayer] PiP stopped, cleaning up plugin reference.")
|
|
721
|
+
// self.callbackHandler?.onPlayerDismissed(isPiPDismissal: false)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
|
725
|
+
// print("[JWPlayer] Delegate: PiP Restore UI Requested")
|
|
726
|
+
// Ask the plugin to re-present this view controller instance
|
|
727
|
+
// callbackHandler?.rePresentPlayerRequested()
|
|
728
|
+
|
|
729
|
+
// Here I will REMOVE the button to close and readd it
|
|
730
|
+
if let closeButton = self.closeButton {
|
|
731
|
+
closeButton.removeFromSuperview()
|
|
732
|
+
self.setupCloseButton()
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// MARK: - IMPORTANT-
|
|
736
|
+
// Make sure to call the super method when you have restored the UI, it is important to notify the system of this.
|
|
737
|
+
super.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
741
|
+
super.viewWillDisappear(animated)
|
|
742
|
+
|
|
743
|
+
// Only trigger the dismissal callback if it's a manual dismissal
|
|
744
|
+
if (self.isBeingDismissed || self.isMovingFromParent) {
|
|
745
|
+
print("[JWPlayer] Manual dismissal detected in viewWillDisappear")
|
|
746
|
+
callbackHandler?.onPlayerDismissed(isPiPDismissal: false)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|