Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- Fixed an issue on iOS where fetching the artwork for the NowPlayingInfo could cause a crash due to unsafe thread usage.

## [11.4.1] - 26-07-22

### Changed
Expand Down
2 changes: 1 addition & 1 deletion ios/THEOplayerRCTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ public class THEOplayerRCTView: UIView {
}

// trigger different feature managers to reflect the current state
self.nowPlayingManager.updateNowPlaying()
self.nowPlayingManager.updateNowPlayingInfo()
self.remoteCommandsManager.updateRemoteCommands()
self.pipControlsManager.updatePipControls()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate
return false
}

view.nowPlayingManager.updateNowPlaying()
view.nowPlayingManager.updateNowPlayingInfo()
return view.backgroundAudioConfig.enabled
}
return false
Expand Down
122 changes: 88 additions & 34 deletions ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import MediaPlayer
class THEOplayerRCTNowPlayingManager {
// MARK: Members
private weak var player: THEOplayer?
private var nowPlayingInfo = [String : Any]()
private let nowPlayingQueue = DispatchQueue(label: "com.theoplayer.reactnative.nowplayinginfo")
private var nowPlayingInfoStorage = [String : Any]()
private var nowPlayingInfoGeneration: Int = 0

// MARK: player Listeners
private var durationChangeListener: EventListener?
Expand All @@ -25,12 +27,12 @@ class THEOplayerRCTNowPlayingManager {
// update elapsed time on close
if let player = self.player {
updateCurrentTime(player.currentTime)
self.processNowPlayingToInfoCenter()
self.processNowPlayingInfoToInfoCenter()
}

// clear nowPlayingInfo
self.nowPlayingInfo = [:]
self.clearNowPlayingOnInfoCenter()
self.clearNowPlayingInfoStorage()
self.clearNowPlayingInfoOnInfoCenter()

if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Destroy, nowPlayingInfo cleared on infoCenter.") }
}
Expand Down Expand Up @@ -61,16 +63,15 @@ class THEOplayerRCTNowPlayingManager {
}
}

func updateNowPlaying() {
// Reset any existing playing info
self.nowPlayingInfo = [:]
func updateNowPlayingInfo() {
// Reset any existing playing info; this invalidates any artwork fetch still in flight
let generation = self.clearNowPlayingInfoStorage()

// Gather new playing info
if let player = self.player,
let sourceDescription = player.source,
let metadata = sourceDescription.metadata {
let artWorkUrlString = self.getArtWorkUrlStringFromSourceDescription(sourceDescription)
self.nowPlayingInfo = [String : Any]()
self.updateTitle(metadata.title)
self.updateArtist(metadata.metadataKeys?["artist"] as? String)
self.updateAlbum(metadata.metadataKeys?["album"] as? String)
Expand All @@ -81,16 +82,58 @@ class THEOplayerRCTNowPlayingManager {
self.updateServiceIdentifier(metadata.metadataKeys?["nowPlayingServiceIdentifier"] as? String)
self.updateContentIdentifier(metadata.metadataKeys?["nowPlayingContentIdentifier"] as? String)
self.updateCurrentTime(player.currentTime)
self.updateArtWork(artWorkUrlString) { [weak self] in
self?.processNowPlayingToInfoCenter()
self.updateArtWork(artWorkUrlString, generation: generation) { [weak self] in
self?.processNowPlayingInfoToInfoCenter()
}
} else {
self.clearNowPlayingOnInfoCenter()
self.clearNowPlayingInfoOnInfoCenter()
}
}

private func processNowPlayingToInfoCenter() {
let nowPlayingInfo = self.nowPlayingInfo
// MARK: - thread safe nowPlayingInfo access
// nowPlayingInfoStorage is touched both from the main thread (player event listeners,
// updateNowPlayingInfo, destroy) and from the URLSession delegate queue (artwork fetch
// completion). All access is serialized through nowPlayingQueue to avoid corrupting
// the dictionary storage.
//
// nowPlayingInfoGeneration identifies the source the storage is currently describing.
// It is bumped on every clear, so an artwork fetch that completes after a source change
// (or after the info was cleared) can detect that it is stale and skip both its write
// and the publication to the info center.
private func setNowPlayingInfoStorage(_ key: String, _ value: Any?) {
self.nowPlayingQueue.sync { self.nowPlayingInfoStorage[key] = value }
}

// Writes only when the generation is still current; returns false when stale.
private func setNowPlayingInfoStorage(_ key: String, _ value: Any?, ifGeneration generation: Int) -> Bool {
self.nowPlayingQueue.sync {
guard generation == self.nowPlayingInfoGeneration else {
return false
}
self.nowPlayingInfoStorage[key] = value
return true
}
}

private func isCurrentNowPlayingInfoGeneration(_ generation: Int) -> Bool {
self.nowPlayingQueue.sync { generation == self.nowPlayingInfoGeneration }
}

private func getNowPlayingInfoStorage() -> [String : Any] {
self.nowPlayingQueue.sync { self.nowPlayingInfoStorage }
}

@discardableResult
private func clearNowPlayingInfoStorage() -> Int {
self.nowPlayingQueue.sync {
self.nowPlayingInfoStorage = [:]
self.nowPlayingInfoGeneration += 1
return self.nowPlayingInfoGeneration
}
}

private func processNowPlayingInfoToInfoCenter() {
let nowPlayingInfo = self.getNowPlayingInfoStorage()
if !nowPlayingInfo.isEmpty {
Task { @MainActor in
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
Expand All @@ -101,11 +144,11 @@ class THEOplayerRCTNowPlayingManager {
}
}
} else {
self.clearNowPlayingOnInfoCenter()
self.clearNowPlayingInfoOnInfoCenter()
}
}

private func clearNowPlayingOnInfoCenter() {
private func clearNowPlayingInfoOnInfoCenter() {
Task { @MainActor in
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] clearing nowPlayingInfo (to nil) on infoCenter.") }
Expand All @@ -130,62 +173,73 @@ class THEOplayerRCTNowPlayingManager {

private func updateTitle(_ metadataTitle: String?) {
if let title = metadataTitle {
self.nowPlayingInfo[MPMediaItemPropertyTitle] = title
self.setNowPlayingInfoStorage(MPMediaItemPropertyTitle, title)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] title [\(title)] stored in nowPlayingInfo.") }
}
}

private func updateArtist(_ metadataArtist: String?) {
if let artist = metadataArtist {
self.nowPlayingInfo[MPMediaItemPropertyArtist] = artist
self.setNowPlayingInfoStorage(MPMediaItemPropertyArtist, artist)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] artist [\(artist)] stored in nowPlayingInfo.") }
}
}

private func updateAlbum(_ metadataAlbum: String?) {
if let album = metadataAlbum {
self.nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
self.setNowPlayingInfoStorage(MPMediaItemPropertyAlbumTitle, album)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] album [\(album)] stored in nowPlayingInfo.") }
}
}

private func updateSubtitle(_ metadataSubtitle: String?) {
if let subtitle = metadataSubtitle {
self.nowPlayingInfo[MPMediaItemPropertyArtist] = subtitle
self.setNowPlayingInfoStorage(MPMediaItemPropertyArtist, subtitle)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] subtitle [\(subtitle)] stored in nowPlayingInfo.") }
}
}
Comment thread
wvanhaevre marked this conversation as resolved.
Outdated

private func updateServiceIdentifier(_ serviceId: String?) {
if let serviceId = serviceId {
self.nowPlayingInfo[MPNowPlayingInfoPropertyServiceIdentifier] = serviceId
self.setNowPlayingInfoStorage(MPNowPlayingInfoPropertyServiceIdentifier, serviceId)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] serviceId [\(serviceId)] stored in nowPlayingInfo.") }
}
}

private func updateContentIdentifier(_ contentId: String?) {
if let contentId = contentId {
self.nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = contentId
self.setNowPlayingInfoStorage(MPNowPlayingInfoPropertyExternalContentIdentifier, contentId)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] contentId [\(contentId)] stored in nowPlayingInfo.") }
}
}

private func updateMediaType() {
self.nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = NSNumber(value: 2)
self.setNowPlayingInfoStorage(MPNowPlayingInfoPropertyMediaType, NSNumber(value: 2))
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] mediaType [hardcoded 2, for video] stored in nowPlayingInfo.") }
}

private func updateArtWork(_ urlString: String?, completion: (() -> Void)?) {
private func updateArtWork(_ urlString: String?, generation: Int, completion: (() -> Void)?) {
if let artUrlString = urlString,
let artUrl = URL(string: artUrlString) {
let dataTask = URLSession.shared.dataTask(with: artUrl) { [weak self] (data, _, _) in
guard let welf = self else {
return
}
if let displayIconData = data,
let displayIcon = UIImage(data: displayIconData) {
self?.nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: displayIcon.size) { size in
let artWork = MPMediaItemArtwork(boundsSize: displayIcon.size) { size in
return displayIcon
}
guard welf.setNowPlayingInfoStorage(MPMediaItemPropertyArtwork, artWork, ifGeneration: generation) else {
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Artwork arrived for an outdated source, discarding it.") }
return
}
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Artwork stored in nowPlayingInfo.") }
} else {
guard welf.isCurrentNowPlayingInfoGeneration(generation) else {
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Artwork failed for an outdated source, discarding it.") }
return
}
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Failed to store artwork in nowPlayingInfo.") }
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
completion?()
Expand All @@ -197,22 +251,22 @@ class THEOplayerRCTNowPlayingManager {
}

private func updatePlaybackRate(_ playerPlaybackRate: Double) {
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: playerPlaybackRate)
self.setNowPlayingInfoStorage(MPNowPlayingInfoPropertyPlaybackRate, NSNumber(value: playerPlaybackRate))
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] playbackrate [\(playerPlaybackRate)] stored in nowPlayingInfo.") }
}

private func updateCurrentTime(_ currentTime: Double) {
self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: currentTime)
self.setNowPlayingInfoStorage(MPNowPlayingInfoPropertyElapsedPlaybackTime, NSNumber(value: currentTime))
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] currentTime [\(currentTime)] stored in nowPlayingInfo.") }
}

private func updateDuration(_ duration: Double?) {
if let duration = duration {
let isLiveStream = duration.isInfinite
self.nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = isLiveStream
self.setNowPlayingInfoStorage(MPNowPlayingInfoPropertyIsLiveStream, isLiveStream)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] isLiveStream [\(isLiveStream)] stored in nowPlayingInfo.") }
if !isLiveStream {
self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
self.setNowPlayingInfoStorage(MPMediaItemPropertyPlaybackDuration, NSNumber(value: duration))
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] duration [\(duration)] stored in nowPlayingInfo.") }
}
}
Expand All @@ -230,7 +284,7 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player,
let duration = wplayer.duration {
welf.updateDuration(duration)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

Expand All @@ -241,7 +295,7 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player {
welf.updatePlaybackRate(wplayer.playbackRate)
welf.updateCurrentTime(wplayer.currentTime)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

Expand All @@ -252,7 +306,7 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player {
welf.updatePlaybackRate(0)
welf.updateCurrentTime(wplayer.currentTime)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

Expand All @@ -264,7 +318,7 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player {
welf.updatePlaybackRate(wplayer.playbackRate)
welf.updateCurrentTime(wplayer.currentTime)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

Expand All @@ -275,14 +329,14 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player {
welf.updatePlaybackRate(wplayer.playbackRate)
welf.updateCurrentTime(wplayer.currentTime)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

// SOURCE_CHANGE
self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE) { [weak self] event in
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] SOURCE_CHANGE \(event.source == nil ? "to nil" : "")") }
self?.updateNowPlaying()
self?.updateNowPlayingInfo()
}
}

Expand Down
Loading