@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.
Files changed (33) hide show
  1. package/CapgoCapacitorJwPlayer.podspec +18 -0
  2. package/Package.swift +30 -0
  3. package/README.md +493 -0
  4. package/android/build.gradle +83 -0
  5. package/android/src/main/AndroidManifest.xml +37 -0
  6. package/android/src/main/java/ee/forgr/capacitor_jw_player/JwPlayerPlugin.java +529 -0
  7. package/android/src/main/java/ee/forgr/capacitor_jw_player/PlayerActivity.java +469 -0
  8. package/android/src/main/res/.gitkeep +0 -0
  9. package/android/src/main/res/drawable/close_button_background.xml +5 -0
  10. package/android/src/main/res/layout/activity_player.xml +28 -0
  11. package/android/src/main/res/values/colors.xml +16 -0
  12. package/android/src/main/res/values/strings.xml +3 -0
  13. package/android/src/main/res/values/styles.xml +32 -0
  14. package/dist/docs.json +459 -0
  15. package/dist/esm/definitions.d.ts +156 -0
  16. package/dist/esm/definitions.js +2 -0
  17. package/dist/esm/definitions.js.map +1 -0
  18. package/dist/esm/index.d.ts +4 -0
  19. package/dist/esm/index.js +7 -0
  20. package/dist/esm/index.js.map +1 -0
  21. package/dist/esm/web.d.ts +57 -0
  22. package/dist/esm/web.js +65 -0
  23. package/dist/esm/web.js.map +1 -0
  24. package/dist/plugin.cjs.js +79 -0
  25. package/dist/plugin.cjs.js.map +1 -0
  26. package/dist/plugin.js +82 -0
  27. package/dist/plugin.js.map +1 -0
  28. package/ios/Sources/JwPlayerPlugin/JwPlayer.swift +24 -0
  29. package/ios/Sources/JwPlayerPlugin/JwPlayerPlugin.swift +749 -0
  30. package/ios/Sources/JwPlayerPlugin/LinkedList.swift +219 -0
  31. package/ios/Sources/JwPlayerPlugin/VideosToPlay.swift +56 -0
  32. package/ios/Tests/JwPlayerPluginTests/JwPlayerPluginTests.swift +15 -0
  33. 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
+ }