mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-24 23:37:00 +00:00
fix(ios): notification controls flow (#4854)
* fix(ios): load artwork asynchronously to unblock notification controls * fix(ios): remove playback observer on player removal and guard artwork callback * fix: cleaning up player * refactor: `if` syntax * fix: add missing cleaner * fix: remove `rounded` from current time * chore: update pod versions * refactor(ios): use targeted update functions at each call site * refactor(ios): use async/await to load artwork metadata * fix(ios): update static now playing info after setting external metadata * fix: find new player before updating playback state * fix(ios): take over notification controls when registering an already-playing player * fix(ios): clear stale artwork and guard against item change in async artwork load * fix: guard notification controls updates against stale player item * fix: update playback duration periodically to handle streams with initially unknown duration
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -22,7 +22,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate {
|
||||
|
||||
func onRateChanged(rate: Float) {
|
||||
_eventEmitter?.onPlaybackRateChange(Double(rate))
|
||||
NowPlayingInfoCenterManager.shared.updateNowPlayingInfo()
|
||||
NowPlayingInfoCenterManager.shared.updatePlaybackState()
|
||||
updateAndEmitPlaybackState()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user