element-ios/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift

269 lines
9.5 KiB
Swift

//
// Copyright 2021-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
protocol VoiceMessageAudioPlayerDelegate: AnyObject {
func audioPlayerDidStartLoading(_ audioPlayer: VoiceMessageAudioPlayer)
func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer)
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer)
func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer)
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer)
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer)
func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError: Error)
}
enum VoiceMessageAudioPlayerError: Error {
case genericError
}
class VoiceMessageAudioPlayer: NSObject {
private var playerItem: AVPlayerItem?
private var audioPlayer: AVQueuePlayer?
private var statusObserver: NSKeyValueObservation?
private var playbackBufferEmptyObserver: NSKeyValueObservation?
private var rateObserver: NSKeyValueObservation?
private var playToEndObserver: NSObjectProtocol?
private var appBackgroundObserver: NSObjectProtocol?
private let delegateContainer = DelegateContainer()
private(set) var url: URL?
private(set) var displayName: String?
var isPlaying: Bool {
guard let audioPlayer = audioPlayer else {
return false
}
return audioPlayer.currentItem != nil && (audioPlayer.rate > 0)
}
var duration: TimeInterval {
return abs(CMTimeGetSeconds(self.audioPlayer?.currentItem?.duration ?? .zero))
}
var currentTime: TimeInterval {
let currentTime = abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero))
return currentTime.isFinite ? currentTime : .zero
}
var playerItems: [AVPlayerItem] {
guard let audioPlayer = audioPlayer else {
return []
}
return audioPlayer.items()
}
var currentUrl: URL? {
return (audioPlayer?.currentItem?.asset as? AVURLAsset)?.url
}
private(set) var isStopped = true
deinit {
removeObservers()
}
func loadContentFromURL(_ url: URL, displayName: String? = nil) {
if self.url == url {
return
}
self.url = url
self.displayName = displayName
removeObservers()
delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStartLoading(self)
}
playerItem = AVPlayerItem(url: url)
audioPlayer = AVQueuePlayer(playerItem: playerItem)
addObservers()
}
func addContentFromURL(_ url: URL) {
let playerItem = AVPlayerItem(url: url)
audioPlayer?.insert(playerItem, after: nil)
// audioPlayerDidFinishPlaying must be called on this last AVPlayerItem
NotificationCenter.default.removeObserver(playToEndObserver as Any)
playToEndObserver = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in
guard let self = self else { return }
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishPlaying(self)
}
}
}
func reloadContentIfNeeded() {
if let url, let audioPlayer, audioPlayer.currentItem == nil {
self.url = nil
loadContentFromURL(url)
}
}
func removeAllPlayerItems() {
audioPlayer?.removeAllItems()
}
func unloadContent() {
url = nil
audioPlayer?.replaceCurrentItem(with: nil)
}
func play() {
isStopped = false
reloadContentIfNeeded()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
MXLog.error("Could not redirect audio playback to speakers.")
}
audioPlayer?.play()
}
func pause() {
audioPlayer?.pause()
}
func stop() {
if isStopped {
return
}
isStopped = true
audioPlayer?.pause()
audioPlayer?.seek(to: .zero)
}
func seekToTime(_ time: TimeInterval, completionHandler: @escaping (Bool) -> Void = { _ in }) {
audioPlayer?.seek(to: CMTime(seconds: time, preferredTimescale: 60000), completionHandler: completionHandler)
}
func registerDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) {
delegateContainer.registerDelegate(delegate)
}
func deregisterDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) {
delegateContainer.deregisterDelegate(delegate)
}
// MARK: - Private
private func addObservers() {
guard let audioPlayer = audioPlayer, let playerItem = playerItem else {
return
}
statusObserver = playerItem.observe(\.status, options: [.old, .new]) { [weak self] item, change in
guard let self = self else { return }
switch playerItem.status {
case .failed:
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayer(self, didFailWithError: playerItem.error ?? VoiceMessageAudioPlayerError.genericError)
}
case .readyToPlay:
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishLoading(self)
}
default:
break
}
}
playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.old, .new]) { [weak self] item, change in
guard let self = self else { return }
if playerItem.isPlaybackBufferEmpty {
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStartLoading(self)
}
} else {
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishLoading(self)
}
}
}
rateObserver = audioPlayer.observe(\.rate, options: [.old, .new]) { [weak self] player, change in
guard let self = self else { return }
if audioPlayer.rate == 0.0 {
if self.isStopped {
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStopPlaying(self)
}
} else {
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidPausePlaying(self)
}
}
} else {
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStartPlaying(self)
}
}
}
playToEndObserver = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in
guard let self = self else { return }
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishPlaying(self)
}
}
appBackgroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
guard let self = self, !BuildSettings.allowBackgroundAudioMessagePlayback else { return }
self.pause()
self.delegateContainer.notifyDelegatesWithBlock { delegate in
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidPausePlaying(self)
}
}
}
private func removeObservers() {
statusObserver?.invalidate()
playbackBufferEmptyObserver?.invalidate()
rateObserver?.invalidate()
NotificationCenter.default.removeObserver(playToEndObserver as Any)
NotificationCenter.default.removeObserver(appBackgroundObserver as Any)
}
}
extension VoiceMessageAudioPlayerDelegate {
func audioPlayerDidStartLoading(_ audioPlayer: VoiceMessageAudioPlayer) { }
func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { }
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { }
func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { }
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { }
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { }
func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError: Error) { }
}