Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1644,7 +1644,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNJWPlayer (1.4.0):
- RNJWPlayer (1.4.1):
- google-cast-sdk (= 4.8.3)
- GoogleAds-IMA-iOS-SDK (= 3.22.1)
- JWPlayerKit (= 4.25.2)
Expand Down Expand Up @@ -2034,7 +2034,7 @@ SPEC CHECKSUMS:
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: 8b1080a6db0be82dbca18550d6212b885bfab6b2
RNJWPlayer: 290598f5151baecd9f26b7b2f5ae0ab5b5e3caab
RNJWPlayer: d3b16dea43b05ea9d660a6b2c3e18b11ccb82529
RNScreens: 790123c4a28783d80a342ce42e8c7381bed62db1
RNVectorIcons: bd818296a51dc2bb8c3bd97a3ca399df1afe216d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Expand Down
14 changes: 8 additions & 6 deletions RNJWPlayer.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@ Pod::Spec.new do |s|
'OTHER_LDFLAGS': '-ObjC',
}

swift_flags = ['$(inherited)']

if defined?($RNJWPlayerUseGoogleCast)
Pod::UI.puts "RNJWPlayer: enable Google Cast"
s.dependency 'google-cast-sdk', '4.8.3'
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '$(inherited) -D USE_GOOGLE_CAST'
}
swift_flags << '-D USE_GOOGLE_CAST'
end
if defined?($RNJWPlayerUseGoogleIMA)
Pod::UI.puts "RNJWPlayer: enable IMA SDK"
s.dependency 'GoogleAds-IMA-iOS-SDK', '3.22.1'
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '$(inherited) -D USE_GOOGLE_IMA'
}
swift_flags << '-D USE_GOOGLE_IMA'
end

s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => swift_flags.join(' ')
}

end
142 changes: 111 additions & 31 deletions ios/RNJWPlayer/RNJWPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
var playerFailed = false
var castController: JWCastController!
var isCasting: Bool = false

/// Checks actual GCK cast session state, independent of our delegate callbacks.
/// The SDK's built-in cast UI can start casting without going through our castController,
/// so we check GCKCastContext directly for the most reliable result.
var isActivelyCasting: Bool {
#if USE_GOOGLE_CAST
return GCKCastContext.sharedInstance().castState == .connected
#else
return false
#endif
}
var availableDevices: [AnyObject]!
var onBeforeNextPlaylistItemCompletion: ((JWPlayerItem?) -> ())?
var pendingConfigAfterPlaylistItemCallback: [String: Any]?
Expand Down Expand Up @@ -324,6 +335,7 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
private var isRecreatingPlayer: Bool = false // Prevents re-entrant calls during recreation

@objc func recreatePlayerWithConfig(_ config: [String: Any]) {

// Prevent re-entrant calls while player is being recreated
if isRecreatingPlayer {
print("Warning: Player recreation already in progress, queueing this config change")
Expand Down Expand Up @@ -501,29 +513,26 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
}

isRecreatingPlayer = true

// Preserve state
let wasFullscreen = playerViewController.isFullScreen
let currentState = playerViewController.player.getState()
let wasPlaying = currentState == .playing

// Stop playback before reconfiguration (prevents issues)
playerViewController.player.stop()

// Parse config early (before setting license) to check if it's valid
// Parse config early
let forceLegacyConfig = config["forceLegacyConfig"] as? Bool ?? false
let playlistItemCallback = config["playlistItemCallbackEnabled"] as? Bool ?? false

// Set license FIRST (before parsing config fully)
let license = config["license"] as? String
self.setLicense(license: license)

// Handle audio session for background/PiP
if let bae = config["backgroundAudioEnabled"] as? Bool, let pe = config["pipEnabled"] as? Bool {
backgroundAudioEnabled = bae
pipEnabled = pe
}

if backgroundAudioEnabled || pipEnabled {
let category = config["category"] != nil ? config["category"] as? String : "playback"
let categoryOptions = config["categoryOptions"] as? [String]
Expand All @@ -532,12 +541,12 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
} else {
self.deinitAudioSession()
}
// Handle DRM parameters

// Handle DRM parameters (update bridge vars for local playback DRM callbacks)
processSpcUrl = config["processSpcUrl"] as? String
fairplayCertUrl = config["certificateUrl"] as? String
contentUUID = config["contentUUID"] as? String

// Handle legacy DRM in playlist
if forceLegacyConfig {
if let playlist = config["playlist"] as? [AnyObject] {
Expand All @@ -552,11 +561,59 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
}
}
}


// When actively casting, use loadPlaylist instead of configurePlayer.
// configurePlayer reinitializes the player which tears down the SDK's internal
// CastProvider, breaking the cast session. loadPlaylist preserves the cast session
// and the SDK automatically routes new content to the CastProvider which sends it
// to the receiver (including DRM sources and userInfo via customData).
if isActivelyCasting {
print("Casting active - using loadPlaylist to preserve cast session")

var playlistArray = [JWPlayerItem]()
if let playlist = config["playlist"] as? [[String: Any]] {
for item in playlist {
if let playerItem = try? getPlayerItem(item: item) {
playlistArray.append(playerItem)
}
}
}

guard !playlistArray.isEmpty else {
print("Error: No valid playlist items found in config during cast")
isRecreatingPlayer = false
return
}

currentConfig = config
playerViewController.player.loadPlaylist(items: playlistArray)

if playlistItemCallback {
setupPlaylistItemCallback()
}

print("Playlist loaded during cast session (items: \(playlistArray.count))")

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
self.isRecreatingPlayer = false

if let queuedConfig = self.pendingPlayerConfig {
print("Processing queued config change after cast loadPlaylist")
self.pendingPlayerConfig = nil
self.recreatePlayerWithConfig(queuedConfig)
}
}
return
}

// Non-casting path: stop playback and reconfigure the player
playerViewController.player.stop()

// Build new configuration
do {
let playerConfig: JWPlayerConfiguration

if forceLegacyConfig {
playerConfig = try getPlayerConfiguration(config: config)
} else {
Expand All @@ -568,38 +625,35 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
}
playerConfig = jwConfig
}

// Update stored config
currentConfig = config

// Reconfigure existing player (this is the key optimization!)
playerViewController.player.configurePlayer(with: playerConfig)

// Setup playlist item callback if needed
if playlistItemCallback {
setupPlaylistItemCallback()
}

// Fullscreen state is automatically preserved by the view controller
// No need to manually restore it
print("Player reconfigured successfully (fullscreen: \(wasFullscreen))")

// Clear the reconfiguration flag after a delay to ensure SDK completes initialization
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
self.isRecreatingPlayer = false

// If there's a queued config change, process it now
if let queuedConfig = self.pendingPlayerConfig {
print("Processing queued config change after reconfiguration")
self.pendingPlayerConfig = nil
self.recreatePlayerWithConfig(queuedConfig)
}
}

// Optionally restart playback if it was playing
// (Usually handled by autostart in config)


} catch {
print("Error during reconfiguration: \(error) - falling back to recreation")
isRecreatingPlayer = false // Clear flag before fallback
Expand Down Expand Up @@ -634,19 +688,37 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
}
}

// Track whether we need to re-establish casting after recreation
let wasCasting = isActivelyCasting

// 2. Destroy current player
dismissPlayerViewController()
removePlayerView()

// 3. Create new player with new config
setNewConfig(config: config)

// 4. Clear the recreation flag after a delay to ensure setup completes
// The iOS SDK needs time to finish initialization
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
self.isRecreatingPlayer = false


#if USE_GOOGLE_CAST
// If we were casting, the old castController's player reference is stale.
// Re-create the castController with the new player so casting can resume.
// Note: full recreation (license/viewOnly change) will interrupt the cast
// session. The user may need to re-initiate casting.
if wasCasting {
self.castController = nil
if let player = (self.playerView?.player ?? self.playerViewController?.player) as? JWPlayer {
self.castController = JWCastController(player: player)
self.castController.delegate = self
print("Cast controller recreated after full player recreation")
}
}
#endif

// If there's a queued config change, process it now
if let queuedConfig = self.pendingPlayerConfig {
print("Processing queued config change")
Expand Down Expand Up @@ -2176,12 +2248,17 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
extension RNJWPlayerView: JWCastDelegate {
// pragma Mark - Casting methods
func setUpCastController() {
if (playerView != nil) && playerView.player as! Bool && (castController == nil) {
castController = JWCastController(player:playerView.player)
castController.delegate = self
}
guard castController == nil else {
self.scanForDevices()
return
}

if let player = (playerView?.player ?? playerViewController?.player) as? JWPlayer {
castController = JWCastController(player: player)
castController.delegate = self
}

self.scanForDevices()
self.scanForDevices()
}

func scanForDevices() {
Expand Down Expand Up @@ -2255,10 +2332,12 @@ extension RNJWPlayerView: JWCastDelegate {
// MARK: - JWPlayer Cast Delegate

func castController(_ controller: JWCastController, castingBeganWithDevice device: JWCastingDevice) {
isCasting = true
self.onCasting?([:])
}

func castController(_ controller:JWCastController, castingEndedWithError error: Error?) {
isCasting = false
self.onCastingEnded?(["error": error as Any])
}

Expand Down Expand Up @@ -2316,6 +2395,7 @@ extension RNJWPlayerView: JWCastDelegate {
}

func castController(_ controller: JWCastController, disconnectedWithError error: (Error)?) {
isCasting = false
self.onDisconnectedFromCastingDevice?(["error": error as Any])
}
}
Expand Down