269 lines
9.5 KiB
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) { }
|
|
}
|