@jwplayer/jwplayer-react-native 1.1.3 → 1.3.0

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 (36) hide show
  1. package/README.md +114 -21
  2. package/RNJWPlayer.podspec +1 -1
  3. package/android/build.gradle +14 -1
  4. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +27 -0
  5. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +373 -204
  6. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +16 -0
  7. package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +13 -1
  8. package/badges/version.svg +1 -1
  9. package/docs/CONFIG-REFERENCE.md +747 -0
  10. package/docs/MIGRATION-GUIDE.md +617 -0
  11. package/docs/PLATFORM-DIFFERENCES.md +693 -0
  12. package/docs/props.md +15 -3
  13. package/index.d.ts +225 -216
  14. package/index.js +34 -0
  15. package/ios/RNJWPlayer/RNJWPlayerView.swift +365 -10
  16. package/ios/RNJWPlayer/RNJWPlayerViewController.swift +45 -16
  17. package/ios/RNJWPlayer/RNJWPlayerViewManager.m +2 -0
  18. package/ios/RNJWPlayer/RNJWPlayerViewManager.swift +13 -0
  19. package/package.json +2 -2
  20. package/types/advertising.d.ts +514 -0
  21. package/types/index.d.ts +21 -0
  22. package/types/legacy.d.ts +82 -0
  23. package/types/platform-specific.d.ts +641 -0
  24. package/types/playlist.d.ts +410 -0
  25. package/types/unified-config.d.ts +591 -0
  26. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  27. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  28. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  29. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  30. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  31. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  32. package/android/.gradle/8.9/gc.properties +0 -0
  33. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  34. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  35. package/android/.gradle/vcs-1/gc.properties +0 -0
  36. package/docs/types.md +0 -254
@@ -307,6 +307,344 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
307
307
  }
308
308
  }
309
309
 
310
+ private var pendingPlayerConfig: [String: Any]?
311
+ private var playerConfigTimeout: Timer?
312
+ private let maxPendingTime: TimeInterval = 5.0 // Maximum time to wait for PiP to close
313
+ private var isRecreatingPlayer: Bool = false // Prevents re-entrant calls during recreation
314
+
315
+ @objc func recreatePlayerWithConfig(_ config: [String: Any]) {
316
+ // Prevent re-entrant calls while player is being recreated
317
+ if isRecreatingPlayer {
318
+ print("Warning: Player recreation already in progress, queueing this config change")
319
+ // Override any pending config with the latest one
320
+ pendingPlayerConfig = config
321
+ return
322
+ }
323
+
324
+ // Cancel any existing pending configuration
325
+ if pendingPlayerConfig != nil {
326
+ print("Warning: Overriding pending content switch")
327
+ playerConfigTimeout?.invalidate()
328
+ pendingPlayerConfig = nil
329
+ }
330
+
331
+ // Validate config
332
+ guard !config.isEmpty else {
333
+ print("Error: Empty config provided to recreatePlayerWithConfig")
334
+ return
335
+ }
336
+
337
+ // 1. Handle PiP state (must exit PiP before any changes)
338
+ var isPipActive = false
339
+ var pipController: AVPictureInPictureController?
340
+
341
+ if let playerView = playerView {
342
+ pipController = playerView.pictureInPictureController
343
+ isPipActive = pipController?.isPictureInPictureActive ?? false
344
+ } else if let playerViewController = playerViewController {
345
+ pipController = playerViewController.playerView.pictureInPictureController
346
+ isPipActive = pipController?.isPictureInPictureActive ?? false
347
+ }
348
+
349
+ if isPipActive {
350
+ guard let pipController = pipController else {
351
+ print("Warning: PiP appears active but controller is nil, proceeding with direct switch")
352
+ proceedWithConfigChange(config: config)
353
+ return
354
+ }
355
+
356
+ pendingPlayerConfig = config
357
+
358
+ // Set a timeout to prevent infinite waiting
359
+ playerConfigTimeout = Timer.scheduledTimer(withTimeInterval: maxPendingTime, repeats: false) { [weak self] _ in
360
+ guard let self = self else { return }
361
+ print("Warning: PiP close timeout reached, forcing content switch")
362
+ if let pendingConfig = self.pendingPlayerConfig {
363
+ self.pendingPlayerConfig = nil
364
+ self.proceedWithConfigChange(config: pendingConfig)
365
+ }
366
+ }
367
+
368
+ // Attempt to stop PiP
369
+ pipController.stopPictureInPicture()
370
+ return
371
+ }
372
+
373
+ // 2. Handle fullscreen state (only exit if we need to recreate)
374
+ let isFullscreen = playerViewController?.isFullScreen ?? false
375
+
376
+ if isFullscreen && requiresPlayerRecreation(config) {
377
+ // Only exit fullscreen if we need to recreate the player
378
+ // For reconfiguration, fullscreen state will be preserved automatically
379
+ print("Fullscreen active and recreation needed - exiting fullscreen first")
380
+ pendingPlayerConfig = config
381
+
382
+ // Set a timeout to prevent infinite waiting
383
+ playerConfigTimeout = Timer.scheduledTimer(withTimeInterval: maxPendingTime, repeats: false) { [weak self] _ in
384
+ guard let self = self else { return }
385
+ print("Warning: Fullscreen exit timeout reached, forcing content switch")
386
+ if let pendingConfig = self.pendingPlayerConfig {
387
+ self.pendingPlayerConfig = nil
388
+ self.proceedWithConfigChange(config: pendingConfig)
389
+ }
390
+ }
391
+
392
+ // Exit fullscreen
393
+ playerViewController?.dismiss(animated: true) { [weak self] in
394
+ guard let self = self else { return }
395
+ self.playerConfigTimeout?.invalidate()
396
+ self.playerConfigTimeout = nil
397
+ if let pendingConfig = self.pendingPlayerConfig {
398
+ self.pendingPlayerConfig = nil
399
+ self.proceedWithConfigChange(config: pendingConfig)
400
+ }
401
+ }
402
+ return
403
+ }
404
+
405
+ // 3. No special state handling needed - proceed with config change
406
+ // If in fullscreen and just reconfiguring, state will be preserved
407
+ proceedWithConfigChange(config: config)
408
+ }
409
+
410
+ /// Determines the appropriate way to apply the config change
411
+ private func proceedWithConfigChange(config: [String: Any]) {
412
+ // Clear any pending timeout
413
+ playerConfigTimeout?.invalidate()
414
+ playerConfigTimeout = nil
415
+
416
+ // Ensure we're on the main thread
417
+ guard Thread.isMainThread else {
418
+ DispatchQueue.main.async { [weak self] in
419
+ self?.proceedWithConfigChange(config: config)
420
+ }
421
+ return
422
+ }
423
+
424
+ // Decide: recreate or reconfigure?
425
+ if requiresPlayerRecreation(config) {
426
+ print("Player recreation required - performing full recreation")
427
+ completePlayerRecreation(config: config)
428
+ } else {
429
+ print("Reconfiguring existing player without recreation (optimized)")
430
+ reconfigurePlayer(config: config)
431
+ }
432
+ }
433
+
434
+ /// Determines if a config change requires full player recreation.
435
+ /// Only returns true for changes that genuinely cannot be handled by reconfiguration.
436
+ private func requiresPlayerRecreation(_ config: [String: Any]) -> Bool {
437
+ // If no player exists, we need to create one
438
+ guard playerViewController != nil || playerView != nil else {
439
+ return true
440
+ }
441
+
442
+ // If no previous config, this is first-time setup
443
+ guard let currentConfig = currentConfig else {
444
+ return true
445
+ }
446
+
447
+ // Check for license changes (requires recreation)
448
+ let newLicense = config["license"] as? String
449
+ let oldLicense = currentConfig["license"] as? String
450
+
451
+ if newLicense != oldLicense {
452
+ if newLicense != nil && oldLicense != nil {
453
+ print("License changed from '\(oldLicense!)' to '\(newLicense!)' - recreation required")
454
+ return true
455
+ } else if newLicense == nil || oldLicense == nil {
456
+ print("License presence changed - recreation required")
457
+ return true
458
+ }
459
+ }
460
+
461
+ // Check for viewOnly mode changes (playerView vs playerViewController)
462
+ let newViewOnly = config["viewOnly"] as? Bool ?? false
463
+ let oldViewOnly = currentConfig["viewOnly"] as? Bool ?? false
464
+ if newViewOnly != oldViewOnly {
465
+ print("viewOnly mode changed - recreation required")
466
+ return true
467
+ }
468
+
469
+ // All other changes can be handled by reconfiguration (optimized path!)
470
+ print("iOS: Using reconfiguration path (optimized)")
471
+ return false
472
+ }
473
+
474
+ /// Reconfigures the existing player with new settings without recreation.
475
+ /// This is the preferred path for config updates as it preserves the player instance
476
+ /// and maintains state (fullscreen, etc.), following JWPlayer SDK's design intent.
477
+ private func reconfigurePlayer(config: [String: Any]) {
478
+ guard let playerViewController = playerViewController else {
479
+ // No player exists, need to create
480
+ print("No player exists - falling back to recreation")
481
+ completePlayerRecreation(config: config)
482
+ return
483
+ }
484
+
485
+ // Prevent re-entrant calls during reconfiguration (Issue #192 - Error 180001)
486
+ if isRecreatingPlayer {
487
+ print("Warning: Reconfiguration already in progress, queueing this config change")
488
+ pendingPlayerConfig = config
489
+ return
490
+ }
491
+
492
+ isRecreatingPlayer = true
493
+
494
+ // Preserve state
495
+ let wasFullscreen = playerViewController.isFullScreen
496
+ let currentState = playerViewController.player.getState()
497
+ let wasPlaying = currentState == .playing
498
+
499
+ // Stop playback before reconfiguration (prevents issues)
500
+ playerViewController.player.stop()
501
+
502
+ // Parse config early (before setting license) to check if it's valid
503
+ let forceLegacyConfig = config["forceLegacyConfig"] as? Bool ?? false
504
+ let playlistItemCallback = config["playlistItemCallbackEnabled"] as? Bool ?? false
505
+
506
+ // Set license FIRST (before parsing config fully)
507
+ let license = config["license"] as? String
508
+ self.setLicense(license: license)
509
+
510
+ // Handle audio session for background/PiP
511
+ if let bae = config["backgroundAudioEnabled"] as? Bool, let pe = config["pipEnabled"] as? Bool {
512
+ backgroundAudioEnabled = bae
513
+ pipEnabled = pe
514
+ }
515
+
516
+ if backgroundAudioEnabled || pipEnabled {
517
+ let category = config["category"] != nil ? config["category"] as? String : "playback"
518
+ let categoryOptions = config["categoryOptions"] as? [String]
519
+ let mode = config["mode"] as? String
520
+ self.initAudioSession(category: category, categoryOptions: categoryOptions, mode: mode)
521
+ } else {
522
+ self.deinitAudioSession()
523
+ }
524
+
525
+ // Handle DRM parameters
526
+ processSpcUrl = config["processSpcUrl"] as? String
527
+ fairplayCertUrl = config["certificateUrl"] as? String
528
+ contentUUID = config["contentUUID"] as? String
529
+
530
+ // Handle legacy DRM in playlist
531
+ if forceLegacyConfig {
532
+ if let playlist = config["playlist"] as? [AnyObject] {
533
+ let item = playlist.first
534
+ if let itemMap = item as? [String: Any] {
535
+ if itemMap["processSpcUrl"] != nil {
536
+ processSpcUrl = itemMap["processSpcUrl"] as? String
537
+ }
538
+ if itemMap["certificateUrl"] != nil {
539
+ fairplayCertUrl = itemMap["certificateUrl"] as? String
540
+ }
541
+ }
542
+ }
543
+ }
544
+
545
+ // Build new configuration
546
+ do {
547
+ let playerConfig: JWPlayerConfiguration
548
+
549
+ if forceLegacyConfig {
550
+ playerConfig = try getPlayerConfiguration(config: config)
551
+ } else {
552
+ guard let data = try? JSONSerialization.data(withJSONObject: config, options: .prettyPrinted),
553
+ let jwConfig = try? JWJSONParser.config(from: data) else {
554
+ print("Failed to parse config - falling back to recreation")
555
+ completePlayerRecreation(config: config)
556
+ return
557
+ }
558
+ playerConfig = jwConfig
559
+ }
560
+
561
+ // Update stored config
562
+ currentConfig = config
563
+
564
+ // Reconfigure existing player (this is the key optimization!)
565
+ playerViewController.player.configurePlayer(with: playerConfig)
566
+
567
+ // Setup playlist item callback if needed
568
+ if playlistItemCallback {
569
+ setupPlaylistItemCallback()
570
+ }
571
+
572
+ // Fullscreen state is automatically preserved by the view controller
573
+ // No need to manually restore it
574
+ print("Player reconfigured successfully (fullscreen: \(wasFullscreen))")
575
+
576
+ // Clear the reconfiguration flag after a delay to ensure SDK completes initialization
577
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
578
+ guard let self = self else { return }
579
+ self.isRecreatingPlayer = false
580
+
581
+ // If there's a queued config change, process it now
582
+ if let queuedConfig = self.pendingPlayerConfig {
583
+ print("Processing queued config change after reconfiguration")
584
+ self.pendingPlayerConfig = nil
585
+ self.recreatePlayerWithConfig(queuedConfig)
586
+ }
587
+ }
588
+
589
+ // Optionally restart playback if it was playing
590
+ // (Usually handled by autostart in config)
591
+
592
+ } catch {
593
+ print("Error during reconfiguration: \(error) - falling back to recreation")
594
+ isRecreatingPlayer = false // Clear flag before fallback
595
+ completePlayerRecreation(config: config)
596
+ }
597
+ }
598
+
599
+ /// Performs full player recreation (destroy + create).
600
+ /// This is the expensive path and should only be used when truly necessary.
601
+ private func completePlayerRecreation(config: [String: Any]) {
602
+ // Ensure we're on the main thread
603
+ guard Thread.isMainThread else {
604
+ DispatchQueue.main.async { [weak self] in
605
+ self?.completePlayerRecreation(config: config)
606
+ }
607
+ return
608
+ }
609
+
610
+ // Set flag to prevent re-entrant calls
611
+ isRecreatingPlayer = true
612
+
613
+ // 1. Stop current playback safely
614
+ if let playerView = playerView {
615
+ let state = playerView.player.getState()
616
+ if state == .playing || state == .buffering {
617
+ playerView.player.stop()
618
+ }
619
+ } else if let playerViewController = playerViewController {
620
+ let state = playerViewController.player.getState()
621
+ if state == .playing || state == .buffering {
622
+ playerViewController.player.stop()
623
+ }
624
+ }
625
+
626
+ // 2. Destroy current player
627
+ dismissPlayerViewController()
628
+ removePlayerView()
629
+
630
+ // 3. Create new player with new config
631
+ setNewConfig(config: config)
632
+
633
+ // 4. Clear the recreation flag after a delay to ensure setup completes
634
+ // The iOS SDK needs time to finish initialization
635
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
636
+ guard let self = self else { return }
637
+ self.isRecreatingPlayer = false
638
+
639
+ // If there's a queued config change, process it now
640
+ if let queuedConfig = self.pendingPlayerConfig {
641
+ print("Processing queued config change")
642
+ self.pendingPlayerConfig = nil
643
+ self.recreatePlayerWithConfig(queuedConfig)
644
+ }
645
+ }
646
+ }
647
+
310
648
  func setNewConfig(config: [String : Any]) {
311
649
  let forceLegacyConfig = config["forceLegacyConfig"] as? Bool?
312
650
  let playlistItemCallback = config["playlistItemCallbackEnabled"] as? Bool?
@@ -344,7 +682,6 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
344
682
  contentUUID = config["contentUUID"] as? String
345
683
 
346
684
  if forceLegacyConfig == true {
347
-
348
685
  // Dangerous: check playlist for processSpcUrl / fairplayCertUrl in playlist
349
686
  // Only checks first playlist item as multi-item DRM playlists are ill advised
350
687
  if let playlist = config["playlist"] as? [AnyObject] {
@@ -358,7 +695,6 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
358
695
  }
359
696
  }
360
697
  }
361
- } else {
362
698
  }
363
699
 
364
700
  do {
@@ -1171,26 +1507,30 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
1171
1507
 
1172
1508
  func appIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
1173
1509
  guard let fairplayCertUrlString = fairplayCertUrl, let finalUrl = URL(string: fairplayCertUrlString) else {
1510
+ handler(nil)
1174
1511
  return
1175
1512
  }
1176
1513
 
1177
1514
  let request = URLRequest(url: finalUrl)
1178
1515
  let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
1179
1516
  if let error = error {
1180
- print("DRM cert request error - \(error.localizedDescription)")
1517
+ print("Error fetching FairPlay certificate: \(error.localizedDescription)")
1518
+ handler(nil)
1519
+ return
1181
1520
  }
1521
+
1182
1522
  handler(data)
1183
1523
  }
1184
1524
  task.resume()
1185
1525
  }
1186
1526
 
1187
1527
  func contentKeyWithSPCData(_ spcData: Data, completionHandler handler: @escaping (Data?, Date?, String?) -> Void) {
1188
- if processSpcUrl == nil {
1528
+ guard let processSpcUrlString = processSpcUrl else {
1529
+ handler(nil, nil, nil)
1189
1530
  return
1190
1531
  }
1191
1532
 
1192
- guard let processSpcUrl = URL(string: processSpcUrl) else {
1193
- print("Invalid processSpcUrl")
1533
+ guard let processSpcUrl = URL(string: processSpcUrlString) else {
1194
1534
  handler(nil, nil, nil)
1195
1535
  return
1196
1536
  }
@@ -1201,13 +1541,22 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
1201
1541
  ckcRequest.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
1202
1542
 
1203
1543
  URLSession.shared.dataTask(with: ckcRequest) { (data, response, error) in
1204
- if let httpResponse = response as? HTTPURLResponse, (error != nil || httpResponse.statusCode != 200) {
1205
- print("DRM ckc request error - %@", error?.localizedDescription ?? "Unknown error")
1544
+ if let error = error {
1545
+ print("Error fetching FairPlay license: \(error.localizedDescription)")
1546
+ handler(nil, nil, nil)
1547
+ return
1548
+ }
1549
+
1550
+ if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
1206
1551
  handler(nil, nil, nil)
1207
1552
  return
1208
1553
  }
1209
1554
 
1210
- handler(data, nil, "application/octet-stream")
1555
+ if let data = data {
1556
+ handler(data, nil, "application/octet-stream")
1557
+ } else {
1558
+ handler(nil, nil, nil)
1559
+ }
1211
1560
  }.resume()
1212
1561
  }
1213
1562
 
@@ -1230,7 +1579,13 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
1230
1579
  }
1231
1580
 
1232
1581
  func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1233
-
1582
+ // Handle any pending content switch
1583
+ if let config = pendingPlayerConfig {
1584
+ pendingPlayerConfig = nil
1585
+ DispatchQueue.main.async { [weak self] in
1586
+ self?.proceedWithConfigChange(config: config)
1587
+ }
1588
+ }
1234
1589
  }
1235
1590
 
1236
1591
  func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
@@ -202,40 +202,57 @@ class RNJWPlayerViewController : JWPlayerViewController, JWPlayerViewControllerF
202
202
 
203
203
  func appIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
204
204
  guard let fairplayCertUrlString = parentView?.fairplayCertUrl, let fairplayCertUrl = URL(string: fairplayCertUrlString) else {
205
+ handler(nil)
205
206
  return
206
207
  }
207
208
 
208
209
  let request = URLRequest(url: fairplayCertUrl)
209
210
  let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
210
211
  if let error = error {
211
- print("DRM cert request error - \(error.localizedDescription)")
212
+ print("Error fetching FairPlay certificate: \(error.localizedDescription)")
213
+ handler(nil)
214
+ return
212
215
  }
216
+
213
217
  handler(data)
214
218
  }
215
219
  task.resume()
216
220
  }
217
221
 
218
222
  func contentKeyWithSPCData(_ spcData: Data, completionHandler handler: @escaping (Data?, Date?, String?) -> Void) {
219
- if parentView?.processSpcUrl == nil {
223
+ guard let processSpcUrlString = parentView?.processSpcUrl else {
224
+ handler(nil, nil, nil)
220
225
  return
221
226
  }
222
227
 
223
- if let processSpcUrl = parentView?.processSpcUrl {
224
- let ckcRequest = NSMutableURLRequest(url: NSURL(string: processSpcUrl)! as URL)
225
- ckcRequest.httpMethod = "POST"
226
- ckcRequest.httpBody = spcData
227
- ckcRequest.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
228
-
229
- URLSession.shared.dataTask(with: ckcRequest as URLRequest) { (data, response, error) in
230
- if let httpResponse = response as? HTTPURLResponse, (error != nil || httpResponse.statusCode != 200) {
231
- NSLog("DRM ckc request error - %@", error.debugDescription)
232
- handler(nil, nil, nil)
233
- return
234
- }
228
+ guard let processSpcUrl = URL(string: processSpcUrlString) else {
229
+ handler(nil, nil, nil)
230
+ return
231
+ }
235
232
 
233
+ let ckcRequest = NSMutableURLRequest(url: processSpcUrl)
234
+ ckcRequest.httpMethod = "POST"
235
+ ckcRequest.httpBody = spcData
236
+ ckcRequest.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
237
+
238
+ URLSession.shared.dataTask(with: ckcRequest as URLRequest) { (data, response, error) in
239
+ if let error = error {
240
+ print("Error fetching FairPlay license: \(error.localizedDescription)")
241
+ handler(nil, nil, nil)
242
+ return
243
+ }
244
+
245
+ if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
246
+ handler(nil, nil, nil)
247
+ return
248
+ }
249
+
250
+ if let data = data {
236
251
  handler(data, nil, "application/octet-stream")
237
- }.resume()
238
- }
252
+ } else {
253
+ handler(nil, nil, nil)
254
+ }
255
+ }.resume()
239
256
  }
240
257
 
241
258
  // MARK: - AV Picture In Picture Delegate
@@ -249,26 +266,38 @@ class RNJWPlayerViewController : JWPlayerViewController, JWPlayerViewControllerF
249
266
 
250
267
  override func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
251
268
  super.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
269
+ // Forward to parent view to handle pending config changes
270
+ parentView?.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
252
271
  }
253
272
 
254
273
  override func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
255
274
  super.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
275
+ // Forward to parent view for logging/tracking
276
+ parentView?.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
256
277
  }
257
278
 
258
279
  override func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
259
280
  super.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
281
+ // Forward to parent view for logging/tracking
282
+ parentView?.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
260
283
  }
261
284
 
262
285
  override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
263
286
  super.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
287
+ // Forward to parent view for error handling
288
+ parentView?.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
264
289
  }
265
290
 
266
291
  override func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
267
292
  super.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
293
+ // Forward to parent view for logging/tracking
294
+ parentView?.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
268
295
  }
269
296
 
270
297
  override func pictureInPictureController(_ pictureInPictureController:AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler:@escaping (Bool) -> Void) {
271
298
  super.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
299
+ // Forward to parent view
300
+ parentView?.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
272
301
  }
273
302
 
274
303
  // MARK: - JWPlayer State Delegate
@@ -136,6 +136,8 @@ RCT_EXTERN_METHOD(reset)
136
136
 
137
137
  RCT_EXTERN_METHOD(loadPlaylist: (nonnull NSNumber *)reactTag: (nonnull NSArray *)playlist)
138
138
 
139
+ RCT_EXTERN_METHOD(recreatePlayerWithConfig: (nonnull NSNumber *)reactTag: (nonnull NSDictionary *)config)
140
+
139
141
  RCT_EXTERN_METHOD(loadPlaylistWithUrl: (nonnull NSNumber *)reactTag: (nonnull NSString *)playlist)
140
142
 
141
143
  RCT_EXTERN_METHOD(setFullscreen: (nonnull NSNumber *)reactTag: (BOOL)fullscreen)
@@ -552,6 +552,19 @@ class RNJWPlayerViewManager: RCTViewManager {
552
552
  }
553
553
  }
554
554
 
555
+ @objc func recreatePlayerWithConfig(_ reactTag: NSNumber, _ config: NSDictionary) {
556
+ DispatchQueue.main.async {
557
+ guard let view = self.bridge?.uiManager.view(
558
+ forReactTag: reactTag
559
+ ) as? RNJWPlayerView else {
560
+ print("Invalid view returned from registry, expecting RNJWPlayerView")
561
+ return
562
+ }
563
+
564
+ view.recreatePlayerWithConfig(config as? [String: Any] ?? [:])
565
+ }
566
+ }
567
+
555
568
  @objc func loadPlaylistWithUrl(_ reactTag: NSNumber, _ playlistString: String) {
556
569
  DispatchQueue.main.async {
557
570
  guard let view = self.getPlayerView(reactTag: reactTag) else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwplayer/jwplayer-react-native",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
4
4
  "description": "React-native Android/iOS plugin for JWPlayer SDK (https://www.jwplayer.com/)",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/jwplayer/jwplayer-react-native"
17
+ "url": "git+https://github.com/jwplayer/jwplayer-react-native.git"
18
18
  },
19
19
  "keywords": [
20
20
  "react",