@capgo/capacitor-jw-player 0.0.1 → 0.0.12

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.
@@ -2,7 +2,7 @@ import Foundation
2
2
  import Capacitor
3
3
  import JWPlayerKit
4
4
  import AVKit // Import AVKit for AVAudioSession
5
-
5
+ import GoogleCast
6
6
  /**
7
7
  * Please read the Capacitor iOS Plugin Development Guide
8
8
  * here: https://capacitorjs.com/docs/plugins/ios
@@ -15,9 +15,8 @@ protocol CallbackHandler {
15
15
  func rePresentPlayerRequested() // Request plugin to re-present
16
16
  }
17
17
 
18
-
19
18
  @objc(JwPlayerPlugin)
20
- public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
19
+ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin, GCKLoggerDelegate {
21
20
  public let identifier = "JwPlayerPlugin"
22
21
  public let jsName = "JwPlayer"
23
22
  public let pluginMethods: [CAPPluginMethod] = [
@@ -42,12 +41,13 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
42
41
  CAPPluginMethod(name: "currentPlaylist", returnType: CAPPluginReturnPromise),
43
42
  CAPPluginMethod(name: "resume", returnType: CAPPluginReturnPromise)
44
43
  ]
45
- private var viewController: CustomPlayerViewController? = nil
46
-
44
+ private var viewController: CustomPlayerViewController?
47
45
 
48
46
  override public func load() {
49
47
  // Configure the audio session when the plugin loads
50
48
  setupAudioSession()
49
+ print("[JWPlayer] Plugin loaded. Note: Chromecast is initialized in AppDelegate.")
50
+ initializeChromecast()
51
51
  super.load()
52
52
  }
53
53
 
@@ -62,7 +62,33 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
62
62
  print("[JWPlayer] Error setting up AVAudioSession: \(error.localizedDescription)")
63
63
  }
64
64
  }
65
-
65
+
66
+ private func initializeChromecast() {
67
+ print("[ChromecastDebug] Starting Chromecast initialization")
68
+ let kReceiverAppID = "CC1AD845" // Custom App ID for Chromecast
69
+ let discoveryCriteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
70
+ let options = GCKCastOptions(discoveryCriteria: discoveryCriteria)
71
+ options.physicalVolumeButtonsWillControlDeviceVolume = true
72
+ options.disableDiscoveryAutostart = false
73
+ print("[ChromecastDebug] Setting custom options for Chromecast")
74
+ do {
75
+ GCKCastContext.setSharedInstanceWith(options)
76
+ print("[ChromecastDebug] Chromecast context initialized with App ID: \(kReceiverAppID)")
77
+ } catch {
78
+ print("[ChromecastDebug] Error initializing Chromecast context: \(error.localizedDescription)")
79
+ }
80
+
81
+ let filter = GCKLoggerFilter.init()
82
+ filter.minimumLevel = .verbose
83
+ GCKLogger.sharedInstance().filter = filter
84
+ GCKLogger.sharedInstance().delegate = self
85
+ print("[ChromecastDebug] Logger initialized")
86
+ }
87
+
88
+ public func logMessage(_ message: String, at level: GCKLoggerLevel, fromFunction function: String, location: String) {
89
+ print(function + " - " + message)
90
+ }
91
+
66
92
  @objc func initialize(_ call: CAPPluginCall) {
67
93
  print("[JWPlayer] initialize called")
68
94
  guard let licenseKey = call.getString("licenseKey") else {
@@ -70,14 +96,14 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
70
96
  call.reject("licenseKey is required for initialize")
71
97
  return
72
98
  }
73
-
99
+
74
100
  print("[JWPlayer] Setting license key")
75
101
  JWPlayerKitLicense.setLicenseKey(licenseKey)
76
102
  print("[JWPlayer] License key set successfully")
77
-
103
+
78
104
  call.resolve()
79
105
  }
80
-
106
+
81
107
  @objc func play(_ call: CAPPluginCall) {
82
108
  print("[JWPlayer] play called with options: \(call.options)")
83
109
  guard let mediaUrlStr = call.getString("mediaUrl") else {
@@ -85,29 +111,31 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
85
111
  call.reject("mediaUrl is required for initialize")
86
112
  return
87
113
  }
88
-
114
+
89
115
  guard let mediaUrl = URL(string: mediaUrlStr) else {
90
116
  print("[JWPlayer] Error: mediaUrl is invalid URL: \(mediaUrlStr)")
91
117
  call.reject("mediaUrl is invalid URL")
92
118
  return
93
119
  }
94
-
120
+
95
121
  guard let mediaType = call.getString("mediaType") else {
96
122
  print("[JWPlayer] Error: mediaType is missing")
97
123
  call.reject("mediaType is required for initialize")
98
124
  return
99
125
  }
100
-
126
+
127
+ let autostart = call.getBool("autostart") ?? false
128
+
101
129
  if self.viewController != nil {
102
130
  print("[JWPlayer] Error: player is already active")
103
131
  call.reject("the player is already active")
104
132
  return
105
133
  }
106
-
134
+
107
135
  print("[JWPlayer] Media type: \(mediaType), URL: \(mediaUrl)")
108
-
136
+
109
137
  // Create configuration directly
110
- var config: JWPlayerConfiguration? = nil
138
+ var config: JWPlayerConfiguration?
111
139
  do {
112
140
  switch mediaType {
113
141
  case "video":
@@ -117,11 +145,13 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
117
145
  .build()
118
146
  config = try JWPlayerConfigurationBuilder()
119
147
  .playlist(items: [item])
148
+ .autostart(autostart)
120
149
  .build()
121
150
  case "playlist":
122
151
  print("[JWPlayer] Creating playlist configuration")
123
152
  config = try JWPlayerConfigurationBuilder()
124
153
  .playlist(url: mediaUrl)
154
+ .autostart(autostart)
125
155
  .build()
126
156
  default:
127
157
  print("[JWPlayer] Error: Invalid mediaType: \(mediaType)")
@@ -133,13 +163,13 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
133
163
  call.reject("Error creating player configuration: \(error.localizedDescription)")
134
164
  return
135
165
  }
136
-
166
+
137
167
  guard let finalConfig = config else {
138
168
  print("[JWPlayer] Error: Configuration is nil after creation")
139
169
  call.reject("Failed to create player configuration")
140
170
  return
141
171
  }
142
-
172
+
143
173
  DispatchQueue.main.async(execute: DispatchWorkItem {
144
174
  print("[JWPlayer] Creating player view controller with config")
145
175
  // Ensure any previous instance is fully dismissed first
@@ -147,127 +177,128 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
147
177
  print("[JWPlayer] Warning: Play called while a player VC might still exist. Forcing cleanup.")
148
178
  self.viewController = nil
149
179
  }
150
-
180
+
151
181
  let viewController = CustomPlayerViewController(config: finalConfig, callbackHandler: self)
152
- viewController.modalPresentationStyle = .fullScreen
182
+ viewController.modalPresentationStyle = .overCurrentContext
183
+ viewController.modalTransitionStyle = .crossDissolve
153
184
  self.viewController = viewController // Keep strong reference
154
185
  print("[JWPlayer] Resolving play call")
155
186
  call.resolve()
156
- print("[JWPlayer] Presenting view controller")
187
+ print("[JWPlayer] Presenting view controller over current context")
157
188
  self.bridge?.viewController?.present(viewController, animated: true)
158
189
  print("[JWPlayer] View controller presented")
159
190
  })
160
191
  }
161
-
192
+
162
193
  @objc func pause(_ call: CAPPluginCall) {
163
194
  DispatchQueue.main.async(execute: DispatchWorkItem {
164
195
  guard let viewController = self.viewController else {
165
196
  call.reject("No active player")
166
197
  return
167
198
  }
168
-
199
+
169
200
  viewController.player.pause()
170
201
  call.resolve()
171
202
  })
172
203
  }
173
-
204
+
174
205
  @objc func stop(_ call: CAPPluginCall) {
175
206
  DispatchQueue.main.async(execute: DispatchWorkItem {
176
207
  guard let viewController = self.viewController else {
177
208
  call.reject("No active player")
178
209
  return
179
210
  }
180
-
211
+
181
212
  viewController.player.stop()
182
213
  call.resolve()
183
214
  })
184
215
  }
185
-
216
+
186
217
  @objc func seekTo(_ call: CAPPluginCall) {
187
218
  guard let time = call.getDouble("time") else {
188
219
  call.reject("time parameter is required")
189
220
  return
190
221
  }
191
-
222
+
192
223
  DispatchQueue.main.async(execute: DispatchWorkItem {
193
224
  guard let viewController = self.viewController else {
194
225
  call.reject("No active player")
195
226
  return
196
227
  }
197
-
228
+
198
229
  viewController.player.seek(to: TimeInterval(time))
199
230
  call.resolve()
200
231
  })
201
232
  }
202
-
233
+
203
234
  @objc func setVolume(_ call: CAPPluginCall) {
204
235
  guard let volume = call.getDouble("volume") else {
205
236
  call.reject("volume parameter is required")
206
237
  return
207
238
  }
208
-
239
+
209
240
  DispatchQueue.main.async(execute: DispatchWorkItem {
210
241
  guard let viewController = self.viewController else {
211
242
  call.reject("No active player")
212
243
  return
213
244
  }
214
-
245
+
215
246
  viewController.player.volume = volume
216
247
  call.resolve()
217
248
  })
218
249
  }
219
-
250
+
220
251
  @objc func getPosition(_ call: CAPPluginCall) {
221
252
  DispatchQueue.main.async(execute: DispatchWorkItem {
222
253
  guard let viewController = self.viewController else {
223
254
  call.reject("No active player")
224
255
  return
225
256
  }
226
-
257
+
227
258
  call.resolve(["position": viewController.player.time.position])
228
259
  })
229
260
  }
230
-
261
+
231
262
  @objc func getState(_ call: CAPPluginCall) {
232
263
  DispatchQueue.main.async(execute: DispatchWorkItem {
233
264
  guard let viewController = self.viewController else {
234
265
  call.reject("No active player")
235
266
  return
236
267
  }
237
-
268
+
238
269
  call.resolve(["state": viewController.player.getState().rawValue])
239
270
  })
240
271
  }
241
-
272
+
242
273
  @objc func setSpeed(_ call: CAPPluginCall) {
243
274
  guard let speed = call.getDouble("speed") else {
244
275
  call.reject("speed parameter is required")
245
276
  return
246
277
  }
247
-
278
+
248
279
  DispatchQueue.main.async(execute: DispatchWorkItem {
249
280
  guard let viewController = self.viewController else {
250
281
  call.reject("No active player")
251
282
  return
252
283
  }
253
-
284
+
254
285
  viewController.player.playbackRate = speed
255
286
  call.resolve()
256
287
  })
257
288
  }
258
-
289
+
259
290
  @objc func setPlaylistIndex(_ call: CAPPluginCall) {
260
291
  guard let index = call.getInt("index") else {
261
292
  call.reject("index parameter is required")
262
293
  return
263
294
  }
264
-
295
+
265
296
  DispatchQueue.main.async(execute: DispatchWorkItem {
266
297
  guard let viewController = self.viewController else {
267
298
  call.reject("No active player")
268
299
  return
269
300
  }
270
-
301
+
271
302
  // Use the loadPlayerItemAt method which should be available
272
303
  do {
273
304
  viewController.player.loadPlayerItemAt(index: index)
@@ -277,43 +308,43 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
277
308
  }
278
309
  })
279
310
  }
280
-
311
+
281
312
  @objc func loadPlaylist(_ call: CAPPluginCall) {
282
313
  guard let playlistUrlStr = call.getString("playlistUrl") else {
283
314
  call.reject("playlistUrl parameter is required")
284
315
  return
285
316
  }
286
-
317
+
287
318
  guard let playlistUrl = URL(string: playlistUrlStr) else {
288
319
  call.reject("playlistUrl is invalid URL")
289
320
  return
290
321
  }
291
-
322
+
292
323
  DispatchQueue.main.async(execute: DispatchWorkItem {
293
324
  guard let viewController = self.viewController else {
294
325
  call.reject("No active player")
295
326
  return
296
327
  }
297
-
328
+
298
329
  viewController.player.loadPlaylist(url: playlistUrl)
299
330
  call.resolve()
300
331
  })
301
332
  }
302
-
333
+
303
334
  @objc func loadPlaylistWithItems(_ call: CAPPluginCall) {
304
335
  guard let playlist = call.getArray("playlist", [String: Any].self) else {
305
336
  call.reject("playlist parameter is required")
306
337
  return
307
338
  }
308
-
339
+
309
340
  DispatchQueue.main.async(execute: DispatchWorkItem {
310
341
  guard let viewController = self.viewController else {
311
342
  call.reject("No active player")
312
343
  return
313
344
  }
314
-
345
+
315
346
  var items: [JWPlayerItem] = []
316
-
347
+
317
348
  for item in playlist {
318
349
  if let fileStr = item["file"] as? String, let url = URL(string: fileStr) {
319
350
  do {
@@ -327,7 +358,7 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
327
358
  }
328
359
  }
329
360
  }
330
-
361
+
331
362
  if !items.isEmpty {
332
363
  viewController.player.loadPlaylist(items: items)
333
364
  call.resolve()
@@ -336,16 +367,16 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
336
367
  }
337
368
  })
338
369
  }
339
-
370
+
340
371
  @objc func getAudioTracks(_ call: CAPPluginCall) {
341
372
  DispatchQueue.main.async(execute: DispatchWorkItem {
342
373
  guard let viewController = self.viewController else {
343
374
  call.reject("No active player")
344
375
  return
345
376
  }
346
-
377
+
347
378
  let audioTracks = viewController.player.audioTracks
348
-
379
+
349
380
  if audioTracks != nil && audioTracks.count > 0 {
350
381
  var results: [[String: Any]] = []
351
382
  for track in audioTracks {
@@ -362,46 +393,46 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
362
393
  }
363
394
  })
364
395
  }
365
-
396
+
366
397
  @objc func getCurrentAudioTrack(_ call: CAPPluginCall) {
367
398
  DispatchQueue.main.async(execute: DispatchWorkItem {
368
399
  guard let viewController = self.viewController else {
369
400
  call.reject("No active player")
370
401
  return
371
402
  }
372
-
403
+
373
404
  call.resolve(["index": viewController.player.currentAudioTrack])
374
405
  })
375
406
  }
376
-
407
+
377
408
  @objc func setCurrentAudioTrack(_ call: CAPPluginCall) {
378
409
  guard let index = call.getInt("index") else {
379
410
  call.reject("index parameter is required")
380
411
  return
381
412
  }
382
-
413
+
383
414
  DispatchQueue.main.async(execute: DispatchWorkItem {
384
415
  guard let viewController = self.viewController else {
385
416
  call.reject("No active player")
386
417
  return
387
418
  }
388
-
419
+
389
420
  viewController.player.currentAudioTrack = index
390
421
  call.resolve()
391
422
  })
392
423
  }
393
-
424
+
394
425
  @objc func getCaptions(_ call: CAPPluginCall) {
395
426
  DispatchQueue.main.async(execute: DispatchWorkItem {
396
427
  guard let viewController = self.viewController else {
397
428
  call.reject("No active player")
398
429
  return
399
430
  }
400
-
431
+
401
432
  // JWPlayer iOS SDK doesn't have a direct way to get all caption tracks
402
433
  // But we can use the advanced settings to access them
403
434
  let captionsTrackId = viewController.player.currentCaptionsTrack
404
-
435
+
405
436
  // Create a basic response with just the current caption track
406
437
  let captionsArray: [[String: Any]] = [
407
438
  // Caption track 0 is usually "Off"
@@ -415,34 +446,34 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
415
446
  }
416
447
  return false
417
448
  }
418
-
449
+
419
450
  call.resolve(["captions": captionsArray])
420
451
  })
421
452
  }
422
-
453
+
423
454
  @objc func getCurrentCaptions(_ call: CAPPluginCall) {
424
455
  DispatchQueue.main.async(execute: DispatchWorkItem {
425
456
  guard let viewController = self.viewController else {
426
457
  call.reject("No active player")
427
458
  return
428
459
  }
429
-
460
+
430
461
  call.resolve(["index": viewController.player.currentCaptionsTrack])
431
462
  })
432
463
  }
433
-
464
+
434
465
  @objc func setCurrentCaptions(_ call: CAPPluginCall) {
435
466
  guard let index = call.getInt("index") else {
436
467
  call.reject("index parameter is required")
437
468
  return
438
469
  }
439
-
470
+
440
471
  DispatchQueue.main.async(execute: DispatchWorkItem {
441
472
  guard let viewController = self.viewController else {
442
473
  call.reject("No active player")
443
474
  return
444
475
  }
445
-
476
+
446
477
  do {
447
478
  try viewController.player.setCaptionTrack(index: index)
448
479
  call.resolve()
@@ -451,21 +482,21 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
451
482
  }
452
483
  })
453
484
  }
454
-
485
+
455
486
  @objc func currentPlaylist(_ call: CAPPluginCall) {
456
487
  DispatchQueue.main.async(execute: DispatchWorkItem {
457
488
  guard let viewController = self.viewController else {
458
489
  call.reject("No active player")
459
490
  return
460
491
  }
461
-
492
+
462
493
  // Just return an empty array since we can't safely access playlist information
463
494
  call.resolve([
464
495
  "playlist": []
465
496
  ])
466
497
  })
467
498
  }
468
-
499
+
469
500
  @objc func resume(_ call: CAPPluginCall) {
470
501
  print("[JWPlayer] resume called")
471
502
  DispatchQueue.main.async(execute: DispatchWorkItem {
@@ -474,7 +505,7 @@ public class JwPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
474
505
  call.reject("No active player to resume")
475
506
  return
476
507
  }
477
-
508
+
478
509
  print("[JWPlayer] Resuming playback")
479
510
  viewController.player.play()
480
511
  call.resolve()
@@ -489,7 +520,7 @@ extension JwPlayerPlugin: CallbackHandler {
489
520
  print("[JWPlayer] Event: \(eventName), Data: \(String(describing: data))")
490
521
  notifyListeners(eventName, data: data)
491
522
  }
492
-
523
+
493
524
  func onPlayerDismissed(isPiPDismissal: Bool) {
494
525
  print("[JWPlayer] Player dismissed callback. Was PiP dismissal: \(isPiPDismissal)")
495
526
  // Only nil out the reference if it wasn't a dismissal for starting PiP
@@ -503,7 +534,7 @@ extension JwPlayerPlugin: CallbackHandler {
503
534
  self.notifyListeners("pipStarted", data: nil)
504
535
  }
505
536
  }
506
-
537
+
507
538
  func rePresentPlayerRequested() {
508
539
  print("[JWPlayer] Re-present player requested")
509
540
  guard let vc = self.viewController else {
@@ -514,13 +545,13 @@ extension JwPlayerPlugin: CallbackHandler {
514
545
  print("[JWPlayer] Error: Cannot re-present, bridge view controller is nil.")
515
546
  return
516
547
  }
517
-
548
+
518
549
  // Ensure it's not already being presented or presenting something else
519
550
  if vc.isBeingPresented || vc.presentingViewController != nil || bridgeVC.presentedViewController != nil {
520
551
  print("[JWPlayer] Warning: Cannot re-present, presentation context busy.")
521
552
  return
522
553
  }
523
-
554
+
524
555
  DispatchQueue.main.async {
525
556
  print("[JWPlayer] Re-presenting player view controller")
526
557
  bridgeVC.present(vc, animated: true)
@@ -537,102 +568,100 @@ class CustomPlayerViewController: JWPlayerViewController, JWPlayerViewController
537
568
  "newSize": ["width": newSize.width, "height": newSize.height]
538
569
  ]
539
570
  callbackHandler?.notifyEventListener("playerSizeChange", data: sizeData)
540
-
571
+
541
572
  }
542
-
573
+
543
574
  func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, screenTappedAt position: CGPoint) {
544
575
  print("[JWPlayer] Screen tapped at: \(position)")
545
576
  let positionData: [String: Any] = ["x": position.x, "y": position.y]
546
577
  callbackHandler?.notifyEventListener("screenTapped", data: positionData)
547
578
  }
548
-
549
-
579
+
550
580
  private var callbackHandler: CallbackHandler?
551
581
  private var playerConfig: JWPlayerConfiguration?
552
582
  private var closeButton: UIButton! // Custom close button
553
-
583
+
554
584
  // Standard init is unavailable
555
585
  @available(*, unavailable)
556
586
  init() {
557
587
  fatalError("init() is unavailable, use init(config:callbackHandler:)")
558
588
  }
559
-
589
+
560
590
  // Designated initializer
561
591
  init(config: JWPlayerConfiguration, callbackHandler: CallbackHandler? = nil) {
562
592
  print("[JWPlayer] CustomPlayerViewController init with config")
563
593
  self.playerConfig = config
564
594
  self.callbackHandler = callbackHandler
565
595
  super.init(nibName: nil, bundle: nil)
566
-
596
+
567
597
  // Set UI delegate
568
598
  self.uiDelegate = self
569
599
  print("[JWPlayer] Set self as JWPlayerViewControllerUIDelegate")
570
-
600
+
571
601
  // Enable PiP
572
602
  self.allowsPictureInPicturePlayback = true
573
603
  print("[JWPlayer] allowsPictureInPicturePlayback set to true")
574
-
604
+
575
605
  setupFullscreenConfig()
576
606
  }
577
-
607
+
578
608
  required init?(coder: NSCoder) {
579
609
  print("[JWPlayer] CustomPlayerViewController init from coder - Not recommended")
580
610
  self.playerConfig = nil
581
611
  super.init(coder: coder)
582
-
612
+
583
613
  // Set UI delegate
584
614
  self.uiDelegate = self
585
615
  print("[JWPlayer] Set self as JWPlayerViewControllerUIDelegate (coder init)")
586
-
616
+
587
617
  // Enable PiP even if initialized from coder
588
618
  self.allowsPictureInPicturePlayback = true
589
619
  print("[JWPlayer] allowsPictureInPicturePlayback set to true (coder init)")
590
-
620
+
591
621
  setupFullscreenConfig()
592
622
  }
593
-
623
+
594
624
  private func setupFullscreenConfig() {
595
625
  print("[JWPlayer] Setting up fullscreen config")
596
626
  // Ensure player is always fullscreen
597
627
  modalPresentationStyle = .fullScreen
598
-
628
+
599
629
  // Configure the player view to fill the screen
600
630
  view.frame = UIScreen.main.bounds
601
631
  self.forceFullScreenOnLandscape = false
602
632
  self.forceLandscapeOnFullScreen = false
603
633
  print("[JWPlayer] Fullscreen config set")
604
634
  }
605
-
635
+
606
636
  override func viewDidLoad() {
607
637
  print("[JWPlayer] viewDidLoad")
608
638
  super.viewDidLoad()
609
-
639
+
610
640
  // Apply the configuration passed during initialization
611
641
  if let config = self.playerConfig {
612
642
  print("[JWPlayer] Configuring player from init config")
613
643
  player.configurePlayer(with: config)
614
644
  print("[JWPlayer] Player configured successfully from init config")
615
-
616
645
  } else {
617
646
  print("[JWPlayer] Error: Player configuration is missing in viewDidLoad")
618
647
  self.callbackHandler?.notifyEventListener("error", data: ["message": "Player configuration missing"])
619
648
  }
620
-
649
+
621
650
  // Add the custom close button
622
651
  setupCloseButton()
623
652
  }
624
-
653
+
625
654
  override func viewDidAppear(_ animated: Bool) {
626
655
  print("[JWPlayer] viewDidAppear")
627
656
  super.viewDidAppear(animated)
628
-
657
+
629
658
  // Start playback automatically
630
- // print("[JWPlayer] Starting playback")
631
- // // player.play()
632
- // print("[JWPlayer] Playback started")
659
+ // print("[JWPlayer] Starting playback")
660
+ // player.play()
661
+ // print("[JWPlayer] Playback started")
633
662
  print("[JWPlayer] Button exists \(view.viewWithTag(2136) !== nil)")
634
663
  }
635
-
664
+
636
665
  // Setup and add the custom close button
637
666
  private func setupCloseButton() {
638
667
  closeButton = UIButton(type: .custom)
@@ -643,28 +672,28 @@ class CustomPlayerViewController: JWPlayerViewController, JWPlayerViewController
643
672
  closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold)
644
673
  closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .allTouchEvents)
645
674
  closeButton.tag = 2136
646
-
675
+
647
676
  // Position the button in the top-left corner
648
677
  closeButton.translatesAutoresizingMaskIntoConstraints = false
649
678
  view.addSubview(closeButton)
650
-
679
+
651
680
  NSLayoutConstraint.activate([
652
681
  closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15),
653
682
  closeButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 15),
654
683
  closeButton.widthAnchor.constraint(equalToConstant: 30),
655
684
  closeButton.heightAnchor.constraint(equalToConstant: 30)
656
685
  ])
657
-
686
+
658
687
  // Make sure it's always on top
659
688
  closeButton.layer.zPosition = 1000
660
689
  // Initially visible, will be updated by delegate method
661
690
  closeButton.alpha = 1.0
662
691
  print("[JWPlayer] Custom close button added")
663
-
692
+
664
693
  self.setVisibility(.hidden, for: [.fullscreenButton])
665
694
  print("[JWPlayer] Fullscreen button hidden")
666
695
  }
667
-
696
+
668
697
  // Action for the custom close button
669
698
  @objc private func closeButtonTapped() {
670
699
  print("[JWPlayer] Custom close button tapped - dismissing player manually")
@@ -673,21 +702,21 @@ class CustomPlayerViewController: JWPlayerViewController, JWPlayerViewController
673
702
  self?.callbackHandler?.onPlayerDismissed(isPiPDismissal: false)
674
703
  }
675
704
  }
676
-
705
+
677
706
  // MARK: - JWPlayerViewControllerUIDelegate Method
678
-
707
+
679
708
  func playerViewController(_ controller: JWPlayerViewController, controlBarVisibilityChanged isVisible: Bool, frame: CGRect) {
680
709
  print("[JWPlayer] Control bar visibility changed: \(isVisible)")
681
710
  let targetAlpha: CGFloat = isVisible ? 1.0 : 0.0
682
-
711
+
683
712
  // Set alpha directly without animation
684
713
  // Ensure closeButton is not nil before accessing
685
714
  guard let button = self.closeButton else { return }
686
715
  button.alpha = targetAlpha
687
716
  }
688
-
717
+
689
718
  // MARK: - AVPictureInPictureControllerDelegate Methods
690
-
719
+
691
720
  override func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
692
721
  print("[JWPlayer] Delegate: PiP Will Start")
693
722
  if #available(iOS 14.2, *) {
@@ -696,54 +725,123 @@ class CustomPlayerViewController: JWPlayerViewController, JWPlayerViewController
696
725
  }
697
726
  super.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
698
727
  }
699
-
728
+
700
729
  override func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
701
730
  print("[JWPlayer] Delegate: PiP Did Start")
702
731
  super.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
703
732
  }
704
-
733
+
705
734
  override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
706
735
  print("[JWPlayer] Delegate: PiP Failed to Start: \(error.localizedDescription)")
707
736
  super.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
708
737
  }
709
-
738
+
710
739
  override func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
711
740
  print("[JWPlayer] Delegate: PiP Will Stop")
712
741
  super.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
713
742
  }
714
-
743
+
715
744
  override func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
716
745
  print("[JWPlayer] Delegate: PiP Did Stop")
717
746
  super.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
718
-
747
+
719
748
  // If PiP is stopped directly by the user without restoration, notify the plugin
720
749
  // print("[JWPlayer] PiP stopped, cleaning up plugin reference.")
721
750
  // self.callbackHandler?.onPlayerDismissed(isPiPDismissal: false)
722
751
  }
723
-
752
+
724
753
  override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
725
754
  // print("[JWPlayer] Delegate: PiP Restore UI Requested")
726
755
  // Ask the plugin to re-present this view controller instance
727
756
  // callbackHandler?.rePresentPlayerRequested()
728
-
757
+
729
758
  // Here I will REMOVE the button to close and readd it
730
759
  if let closeButton = self.closeButton {
731
760
  closeButton.removeFromSuperview()
732
761
  self.setupCloseButton()
733
762
  }
734
-
763
+
735
764
  // MARK: - IMPORTANT-
736
765
  // Make sure to call the super method when you have restored the UI, it is important to notify the system of this.
737
766
  super.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
738
767
  }
739
-
768
+
740
769
  override func viewWillDisappear(_ animated: Bool) {
741
770
  super.viewWillDisappear(animated)
742
-
771
+
743
772
  // Only trigger the dismissal callback if it's a manual dismissal
744
- if (self.isBeingDismissed || self.isMovingFromParent) {
773
+ if self.isBeingDismissed || self.isMovingFromParent {
745
774
  print("[JWPlayer] Manual dismissal detected in viewWillDisappear")
746
775
  callbackHandler?.onPlayerDismissed(isPiPDismissal: false)
747
776
  }
748
777
  }
778
+
779
+ // MARK: - JWCastDelegate
780
+ // Optionally, override the following methods to receive and respond to events when casting.
781
+ // Always call the superclass's method when overriding these methods.
782
+
783
+ // Called when a new casting device comes online.
784
+ override func castController(_ controller: JWCastController, devicesAvailable devices: [JWCastingDevice]) {
785
+ super.castController(controller, devicesAvailable: devices)
786
+ print("[JWCastDelegate]: \(devices.count) became available: \(devices)")
787
+ }
788
+
789
+ // Called when a successful connection to a casting device is made.
790
+ override func castController(_ controller: JWCastController, connectedTo device: JWCastingDevice) {
791
+ super.castController(controller, connectedTo: device)
792
+ print("[JWCastDelegate]: Connected to device: \(device.identifier)")
793
+ }
794
+
795
+ // Called when the casting device disconnects.
796
+ override func castController(_ controller: JWCastController, disconnectedWithError error: Error?) {
797
+ super.castController(controller, disconnectedWithError: error)
798
+
799
+ if let error {
800
+ print("[JWCastDelegate]: Casting disconnected from device with error: \"\(error.localizedDescription)\"")
801
+ } else {
802
+ print("[JWCastDelegate]: Casting disconnected from device successfully.")
803
+ }
804
+ }
805
+
806
+ // Called when the connected casting device is temporarily disconnected. Video resumes on the mobile device until connection resumes.
807
+ override func castController(_ controller: JWCastController, connectionSuspendedWithDevice device: JWCastingDevice) {
808
+ super.castController(controller, connectionSuspendedWithDevice: device)
809
+ print("[JWCastDelegate]: Connection suspended with device: \(device.identifier)")
810
+ }
811
+
812
+ // Called after connection is reestablished following a temporary disconnection. Video resumes on the casting device.
813
+ override func castController(_ controller: JWCastController, connectionRecoveredWithDevice device: JWCastingDevice) {
814
+ super.castController(controller, connectionRecoveredWithDevice: device)
815
+ print("[JWCastDelegate]: Connection recovered with device: \(device.identifier)")
816
+ }
817
+
818
+ // Called when an attempt to connect to a casting device is unsuccessful.
819
+ override func castController(_ controller: JWCastController, connectionFailedWithError error: Error) {
820
+ super.castController(controller, connectionFailedWithError: error)
821
+ print("[JWCastDelegate]: Connection failed with error: \(error.localizedDescription)")
822
+ }
823
+
824
+ // Called when casting session begins.
825
+ override func castController(_ controller: JWCastController, castingBeganWithDevice device: JWCastingDevice) {
826
+ super.castController(controller, castingBeganWithDevice: device)
827
+ print("[JWCastDelegate]: Casting began with device: \(device.identifier)")
828
+ }
829
+
830
+ // Called when an attempt to cast to a casting device is unsuccessful.
831
+ override func castController(_ controller: JWCastController, castingFailedWithError error: Error) {
832
+ super.castController(controller, castingFailedWithError: error)
833
+ print("[JWCastDelegate]: Casting failed with error: \(error.localizedDescription)")
834
+ }
835
+
836
+ // Called when a casting session ends.
837
+ override func castController(_ controller: JWCastController, castingEndedWithError error: Error?) {
838
+ super.castController(controller, castingEndedWithError: error)
839
+
840
+ if let error {
841
+ print("[JWCastDelegate]: Casting ended with error: \"\(error.localizedDescription)\"")
842
+ } else {
843
+ print("[JWCastDelegate]: Casting ended successfully.")
844
+ }
845
+ }
846
+
749
847
  }