Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
81 changes: 49 additions & 32 deletions ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ 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]()

// MARK: player Listeners
private var durationChangeListener: EventListener?
Expand All @@ -25,12 +26,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 +62,15 @@ class THEOplayerRCTNowPlayingManager {
}
}

func updateNowPlaying() {
func updateNowPlayingInfo() {
// Reset any existing playing info
self.nowPlayingInfo = [:]
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 @@ -82,15 +82,32 @@ class THEOplayerRCTNowPlayingManager {
self.updateContentIdentifier(metadata.metadataKeys?["nowPlayingContentIdentifier"] as? String)
self.updateCurrentTime(player.currentTime)
self.updateArtWork(artWorkUrlString) { [weak self] in
self?.processNowPlayingToInfoCenter()
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.
private func setNowPlayingInfoStorage(_ key: String, _ value: Any?) {
self.nowPlayingQueue.sync { self.nowPlayingInfoStorage[key] = value }
}

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

private func clearNowPlayingInfoStorage() {
self.nowPlayingQueue.sync { self.nowPlayingInfoStorage = [:] }
}

private func processNowPlayingInfoToInfoCenter() {
let nowPlayingInfo = self.getNowPlayingInfoStorage()
if !nowPlayingInfo.isEmpty {
Task { @MainActor in
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
Expand All @@ -101,11 +118,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,48 +147,48 @@ 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.") }
}

Expand All @@ -181,9 +198,9 @@ class THEOplayerRCTNowPlayingManager {
let dataTask = URLSession.shared.dataTask(with: artUrl) { [weak self] (data, _, _) in
if let displayIconData = data,
let displayIcon = UIImage(data: displayIconData) {
self?.nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: displayIcon.size) { size in
self?.setNowPlayingInfoStorage(MPMediaItemPropertyArtwork, MPMediaItemArtwork(boundsSize: displayIcon.size) { size in
return displayIcon
}
})
if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Artwork stored in nowPlayingInfo.") }
} else {
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.
Expand All @@ -197,22 +214,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 +247,7 @@ class THEOplayerRCTNowPlayingManager {
let wplayer = player,
let duration = wplayer.duration {
welf.updateDuration(duration)
welf.processNowPlayingToInfoCenter()
welf.processNowPlayingInfoToInfoCenter()
}
}

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

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

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

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