diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c1807acc..3144721f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1565,7 +1565,7 @@ PODS: - React-logger (= 0.77.3) - React-perflogger (= 0.77.3) - React-utils (= 0.77.3) - - ReactNativeVideo (7.0.0-beta.1): + - ReactNativeVideo (7.0.0-beta.6): - DoubleConversion - glog - hermes-engine @@ -1587,7 +1587,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ReactNativeVideoDrm (7.0.0-beta.1): + - ReactNativeVideoDrm (7.0.0-beta.6): - DoubleConversion - glog - hermes-engine @@ -1904,8 +1904,8 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 31015410a4a53b9fd0a908ad4d6e3e2b9a25086a ReactCodegen: 53316394e985ded1babc7f143c90c77d2bb1b43c ReactCommon: bf4612cba0fa356b529385029f470d5529dddde4 - ReactNativeVideo: 10dd0a47f8228b41565a3efb13df5b323633b590 - ReactNativeVideoDrm: 07b826ab66fd0a00ab3dd3bef2dd2c026e919a9a + ReactNativeVideo: de7056e831a46412e81fbf730ef739ec4c7378fe + ReactNativeVideoDrm: a83670242cec729b4fb67e7c804ab3300e848b86 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 92f3bb322c40a86b7233b815854730442e01b8c4 diff --git a/packages/react-native-video/ios/core/NowPlayingInfoCenterManager.swift b/packages/react-native-video/ios/core/NowPlayingInfoCenterManager.swift index b783f987..6824df36 100644 --- a/packages/react-native-video/ios/core/NowPlayingInfoCenterManager.swift +++ b/packages/react-native-video/ios/core/NowPlayingInfoCenterManager.swift @@ -17,7 +17,6 @@ class NowPlayingInfoCenterManager { private var skipForwardTarget: Any? private var skipBackwardTarget: Any? private var playbackPositionTarget: Any? - private var seekTarget: Any? private var togglePlayPauseTarget: Any? private let remoteCommandCenter = MPRemoteCommandCenter.shared() @@ -47,7 +46,7 @@ class NowPlayingInfoCenterManager { return } - if receivingRemoteControlEvents == false { + if !receivingRemoteControlEvents { receivingRemoteControlEvents = true } @@ -58,7 +57,8 @@ class NowPlayingInfoCenterManager { observers[player.hashValue] = observePlayers(player: player) players.add(player) - if currentPlayer == nil { + // Also take over if the new player is already playing — KVO won't fire since rate hasn't changed + if currentPlayer == nil || player.rate != 0 { setCurrentPlayer(player: player) } } @@ -75,13 +75,21 @@ class NowPlayingInfoCenterManager { observers.removeValue(forKey: player.hashValue) players.remove(player) - if currentPlayer == player { - currentPlayer = nil - updateNowPlayingInfo() - } - if players.allObjects.isEmpty { cleanup() + return + } + + if currentPlayer == player { + if let playbackObserver { + player.removeTimeObserver(playbackObserver) + self.playbackObserver = nil + } + currentPlayer = nil + findNewCurrentPlayer() + if currentPlayer == nil { + updatePlaybackState() + } } } @@ -91,7 +99,9 @@ class NowPlayingInfoCenterManager { if let playbackObserver { currentPlayer?.removeTimeObserver(playbackObserver) + self.playbackObserver = nil } + currentPlayer = nil invalidateCommandTargets() @@ -106,6 +116,7 @@ class NowPlayingInfoCenterManager { if let playbackObserver { currentPlayer?.removeTimeObserver(playbackObserver) + self.playbackObserver = nil } currentPlayer = player @@ -116,7 +127,7 @@ class NowPlayingInfoCenterManager { forInterval: CMTime(value: 1, timescale: 4), queue: .main, using: { [weak self] _ in - self?.updateNowPlayingInfo() + self?.updatePlaybackState() } ) } @@ -218,13 +229,17 @@ class NowPlayingInfoCenterManager { } public func updateNowPlayingInfo() { - guard let player = currentPlayer else { - invalidateCommandTargets() - MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] - return - } + updateStaticInfo() + updatePlaybackState() + } - guard let currentItem = player.currentItem else { + func updateStaticInfo(ifCurrentItem playerItem: AVPlayerItem) { + guard currentPlayer?.currentItem === playerItem else { return } + updateStaticInfo() + } + + func updateStaticInfo() { + guard let player = currentPlayer, let currentItem = player.currentItem else { return } @@ -233,51 +248,63 @@ class NowPlayingInfoCenterManager { // When the metadata has the tag "iTunSMPB" or "iTunNORM" then the metadata is not converted correctly and comes [nil, nil, ...] // This leads to a crash of the app let metadata: [AVMetadataItem] = { - let common = processMetadataItems(currentItem.asset.commonMetadata) let external = processMetadataItems(currentItem.externalMetadata) - return Array(common.merging(external) { _, new in new }.values) }() - let titleItem = - AVMetadataItem.metadataItems( - from: metadata, - filteredByIdentifier: .commonIdentifierTitle - ).first?.stringValue ?? "" + let title = AVMetadataItem.metadataItems( + from: metadata, + filteredByIdentifier: .commonIdentifierTitle + ).first?.stringValue ?? "" - let artistItem = - AVMetadataItem.metadataItems( - from: metadata, - filteredByIdentifier: .commonIdentifierArtist - ).first?.stringValue ?? "" + let artist = AVMetadataItem.metadataItems( + from: metadata, + filteredByIdentifier: .commonIdentifierArtist + ).first?.stringValue ?? "" - // I have some issue with this - setting artworkItem when it not set dont return nil but also is crashing application - // this is very hacky workaround for it - let imgData = AVMetadataItem.metadataItems( + var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + info[MPMediaItemPropertyTitle] = title + info[MPMediaItemPropertyArtist] = artist + info[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds + info[MPNowPlayingInfoPropertyIsLiveStream] = CMTIME_IS_INDEFINITE(currentItem.asset.duration) + info[MPMediaItemPropertyArtwork] = nil // Clear artwork from previous item; will be loaded asynchronously below + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + + // Load artwork asynchronously so notification controls appear immediately. + guard let artworkMetadataItem = AVMetadataItem.metadataItems( from: metadata, filteredByIdentifier: .commonIdentifierArtwork - ).first?.dataValue - let image = imgData.flatMap { UIImage(data: $0) } ?? UIImage() - let artworkItem = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + ).first else { return } - let newNowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: titleItem, - MPMediaItemPropertyArtist: artistItem, - MPMediaItemPropertyArtwork: artworkItem, - MPMediaItemPropertyPlaybackDuration: currentItem.duration.seconds, - MPNowPlayingInfoPropertyElapsedPlaybackTime: currentItem.currentTime() - .seconds.rounded(), - MPNowPlayingInfoPropertyPlaybackRate: player.rate, - MPNowPlayingInfoPropertyIsLiveStream: CMTIME_IS_INDEFINITE( - currentItem.asset.duration - ), - ] - let currentNowPlayingInfo = - MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + Task { [weak self, weak player, weak currentItem] in + guard let data = try? await artworkMetadataItem.load(.dataValue), + let image = UIImage(data: data) else { return } + let artworkItem = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + await MainActor.run { + guard let self, self.currentPlayer === player, + self.currentPlayer?.currentItem === currentItem else { return } + var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + info[MPMediaItemPropertyArtwork] = artworkItem + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + } + } - MPNowPlayingInfoCenter.default().nowPlayingInfo = - currentNowPlayingInfo.merging(newNowPlayingInfo) { _, new in new } + func updatePlaybackState() { + guard let player = currentPlayer else { + invalidateCommandTargets() + MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] + return + } + + guard let currentItem = player.currentItem else { return } + + var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds + info[MPNowPlayingInfoPropertyPlaybackRate] = player.rate + info[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds + MPNowPlayingInfoCenter.default().nowPlayingInfo = info } private func findNewCurrentPlayer() { diff --git a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift index 624e351a..c042a38b 100644 --- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift +++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift @@ -22,7 +22,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate { func onRateChanged(rate: Float) { _eventEmitter?.onPlaybackRateChange(Double(rate)) - NowPlayingInfoCenterManager.shared.updateNowPlayingInfo() + NowPlayingInfoCenterManager.shared.updatePlaybackState() updateAndEmitPlaybackState() } diff --git a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift index bc3d9f74..690bbfea 100644 --- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift +++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift @@ -408,6 +408,7 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec { } if !items.isEmpty { playerItem.externalMetadata = items + NowPlayingInfoCenterManager.shared.updateStaticInfo(ifCurrentItem: playerItem) } } @@ -421,7 +422,7 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec { DispatchQueue.main.async { guard let playerItem else { return } playerItem.externalMetadata = playerItem.externalMetadata + [.make(identifier: .commonIdentifierArtwork, value: data as NSData)] - NowPlayingInfoCenterManager.shared.updateNowPlayingInfo() + NowPlayingInfoCenterManager.shared.updateStaticInfo(ifCurrentItem: playerItem) } } } else if let imageUri {