@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.
- package/README.md +114 -21
- package/RNJWPlayer.podspec +1 -1
- package/android/build.gradle +14 -1
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +27 -0
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +373 -204
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +16 -0
- package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +13 -1
- package/badges/version.svg +1 -1
- package/docs/CONFIG-REFERENCE.md +747 -0
- package/docs/MIGRATION-GUIDE.md +617 -0
- package/docs/PLATFORM-DIFFERENCES.md +693 -0
- package/docs/props.md +15 -3
- package/index.d.ts +225 -216
- package/index.js +34 -0
- package/ios/RNJWPlayer/RNJWPlayerView.swift +365 -10
- package/ios/RNJWPlayer/RNJWPlayerViewController.swift +45 -16
- package/ios/RNJWPlayer/RNJWPlayerViewManager.m +2 -0
- package/ios/RNJWPlayer/RNJWPlayerViewManager.swift +13 -0
- package/package.json +2 -2
- package/types/advertising.d.ts +514 -0
- package/types/index.d.ts +21 -0
- package/types/legacy.d.ts +82 -0
- package/types/platform-specific.d.ts +641 -0
- package/types/playlist.d.ts +410 -0
- package/types/unified-config.d.ts +591 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
- 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("
|
|
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
|
-
|
|
1528
|
+
guard let processSpcUrlString = processSpcUrl else {
|
|
1529
|
+
handler(nil, nil, nil)
|
|
1189
1530
|
return
|
|
1190
1531
|
}
|
|
1191
1532
|
|
|
1192
|
-
guard let processSpcUrl = URL(string:
|
|
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
|
|
1205
|
-
print("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
223
|
+
guard let processSpcUrlString = parentView?.processSpcUrl else {
|
|
224
|
+
handler(nil, nil, nil)
|
|
220
225
|
return
|
|
221
226
|
}
|
|
222
227
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
}
|
|
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.
|
|
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",
|