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
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
141 changes: 96 additions & 45 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,36 +63,77 @@ 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.updateSubtitleOrArtist(subtitle: metadata.metadataKeys?["subtitle"] as? String,
artist: metadata.metadataKeys?["artist"] as? String)
self.updateAlbum(metadata.metadataKeys?["album"] as? String)
self.updateSubtitle(metadata.metadataKeys?["subtitle"] as? String)
self.updateDuration(player.duration)
self.updateMediaType() // video
self.updatePlaybackRate(player.playbackRate)
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,70 @@ 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
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] artist [\(artist)] stored in nowPlayingInfo.") }
// MPMediaItemPropertyArtist holds the secondary line shown below the title. An audio
// stream typically describes it through an artist, while a video stream has no artist but
// can provide a subtitle (e.g. title "My Series" with subtitle "ep.8: My Episode Title").
// Only one of both can be displayed, so the subtitle takes precedence when both are set.
private func updateSubtitleOrArtist(subtitle metadataSubtitle: String?, artist metadataArtist: String?) {
if let subtitleOrArtist = metadataSubtitle ?? metadataArtist {
self.setNowPlayingInfoStorage(MPMediaItemPropertyArtist, subtitleOrArtist)
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] subtitle/artist [\(subtitleOrArtist)] 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
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] subtitle [\(subtitle)] stored in nowPlayingInfo.") }
}
}

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 +248,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 +281,7 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player,
let duration = wplayer.duration {
welf.updateDuration(duration)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

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

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

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

Expand All @@ -275,14 +326,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