303 lines
14 KiB
Swift
303 lines
14 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
|
|
import MediaPlayer
|
|
|
|
@objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate {
|
|
|
|
private enum Constants {
|
|
static let roomAvatarImageSize: CGSize = CGSize(width: 600, height: 600)
|
|
static let roomAvatarFontSize: CGFloat = 40.0
|
|
static let roomAvatarMimetype: String = "image/jpeg"
|
|
}
|
|
|
|
private var roomAvatarLoader: MXMediaLoader?
|
|
private let audioPlayers: NSMapTable<NSString, VoiceMessageAudioPlayer>
|
|
private let audioRecorders: NSHashTable<VoiceMessageAudioRecorder>
|
|
private let nowPlayingInfoDelegates: NSMapTable<VoiceMessageAudioPlayer, VoiceMessageNowPlayingInfoDelegate>
|
|
|
|
private var displayLink: CADisplayLink!
|
|
|
|
|
|
|
|
// Retain active audio players(playing or paused) so it doesn't stop playing on timeline cell reuse
|
|
// and we can pause/resume players on switching rooms.
|
|
private var activeAudioPlayers: Set<VoiceMessageAudioPlayer>
|
|
|
|
// Keep reference to currently playing player for remote control.
|
|
private var currentlyPlayingAudioPlayer: VoiceMessageAudioPlayer?
|
|
|
|
@objc public static let sharedProvider = VoiceMessageMediaServiceProvider()
|
|
|
|
private var roomAvatar: UIImage?
|
|
@objc public var currentRoomSummary: MXRoomSummary? {
|
|
didSet {
|
|
// set avatar placeholder for now
|
|
roomAvatar = AvatarGenerator.generateAvatar(forMatrixItem: currentRoomSummary?.roomId,
|
|
withDisplayName: currentRoomSummary?.displayName,
|
|
size: Constants.roomAvatarImageSize.width,
|
|
andFontSize: Constants.roomAvatarFontSize)
|
|
|
|
guard let avatarUrl = currentRoomSummary?.avatar else {
|
|
return
|
|
}
|
|
|
|
if let cachePath = MXMediaManager.thumbnailCachePath(forMatrixContentURI: avatarUrl,
|
|
andType: Constants.roomAvatarMimetype,
|
|
inFolder: currentRoomSummary?.roomId,
|
|
toFitViewSize: Constants.roomAvatarImageSize,
|
|
with: MXThumbnailingMethodCrop),
|
|
FileManager.default.fileExists(atPath: cachePath) {
|
|
// found in the cache, load it
|
|
roomAvatar = MXMediaManager.loadThroughCache(withFilePath: cachePath)
|
|
} else {
|
|
// cancel previous loader first
|
|
roomAvatarLoader?.cancel()
|
|
roomAvatarLoader = nil
|
|
|
|
guard let mediaManager = currentRoomSummary?.mxSession.mediaManager else {
|
|
return
|
|
}
|
|
|
|
// not found in the cache, download it
|
|
roomAvatarLoader = mediaManager.downloadThumbnail(fromMatrixContentURI: avatarUrl,
|
|
withType: Constants.roomAvatarMimetype,
|
|
inFolder: currentRoomSummary?.roomId,
|
|
toFitViewSize: Constants.roomAvatarImageSize,
|
|
with: MXThumbnailingMethodCrop,
|
|
success: { filePath in
|
|
if let filePath = filePath {
|
|
self.roomAvatar = MXMediaManager.loadThroughCache(withFilePath: filePath)
|
|
}
|
|
self.roomAvatarLoader = nil
|
|
}, failure: { error in
|
|
self.roomAvatarLoader = nil
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private override init() {
|
|
audioPlayers = NSMapTable<NSString, VoiceMessageAudioPlayer>(valueOptions: .weakMemory)
|
|
audioRecorders = NSHashTable<VoiceMessageAudioRecorder>(options: .weakMemory)
|
|
nowPlayingInfoDelegates = NSMapTable<VoiceMessageAudioPlayer, VoiceMessageNowPlayingInfoDelegate>(keyOptions: .weakMemory, valueOptions: .weakMemory)
|
|
activeAudioPlayers = Set<VoiceMessageAudioPlayer>()
|
|
super.init()
|
|
|
|
displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector)
|
|
displayLink.isPaused = true
|
|
displayLink.add(to: .current, forMode: .common)
|
|
}
|
|
|
|
@objc func audioPlayerForIdentifier(_ identifier: String) -> VoiceMessageAudioPlayer {
|
|
if let audioPlayer = audioPlayers.object(forKey: identifier as NSString) {
|
|
return audioPlayer
|
|
}
|
|
|
|
let audioPlayer = VoiceMessageAudioPlayer()
|
|
audioPlayer.registerDelegate(self)
|
|
audioPlayers.setObject(audioPlayer, forKey: identifier as NSString)
|
|
return audioPlayer
|
|
}
|
|
|
|
@objc func audioRecorder() -> VoiceMessageAudioRecorder {
|
|
let audioRecorder = VoiceMessageAudioRecorder()
|
|
audioRecorder.registerDelegate(self)
|
|
audioRecorders.add(audioRecorder)
|
|
return audioRecorder
|
|
}
|
|
|
|
@objc func pauseAllServices() {
|
|
pauseAllServicesExcept(nil)
|
|
}
|
|
|
|
func registerNowPlayingInfoDelegate(_ delegate: VoiceMessageNowPlayingInfoDelegate, forPlayer player: VoiceMessageAudioPlayer) {
|
|
nowPlayingInfoDelegates.setObject(delegate, forKey: player)
|
|
}
|
|
|
|
func deregisterNowPlayingInfoDelegate(forPlayer player: VoiceMessageAudioPlayer) {
|
|
nowPlayingInfoDelegates.removeObject(forKey: player)
|
|
}
|
|
|
|
// MARK: - VoiceMessageAudioPlayerDelegate
|
|
|
|
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
|
currentlyPlayingAudioPlayer = audioPlayer
|
|
activeAudioPlayers.insert(audioPlayer)
|
|
|
|
let shouldSetupRemoteCommandCenter = nowPlayingInfoDelegates.object(forKey: audioPlayer)?.shouldSetupRemoteCommandCenter(audioPlayer: audioPlayer) ?? true
|
|
if shouldSetupRemoteCommandCenter {
|
|
setUpRemoteCommandCenter()
|
|
} else {
|
|
// clean up the remote command center
|
|
tearDownRemoteCommandCenter()
|
|
}
|
|
pauseAllServicesExcept(audioPlayer)
|
|
}
|
|
|
|
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
|
if currentlyPlayingAudioPlayer == audioPlayer {
|
|
// If we have a NowPlayingInfoDelegate for this player
|
|
let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer)
|
|
|
|
// ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it)
|
|
if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true {
|
|
currentlyPlayingAudioPlayer = nil
|
|
tearDownRemoteCommandCenter()
|
|
}
|
|
}
|
|
activeAudioPlayers.remove(audioPlayer)
|
|
}
|
|
|
|
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
|
if currentlyPlayingAudioPlayer == audioPlayer {
|
|
// If we have a NowPlayingInfoDelegate for this player
|
|
let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer)
|
|
|
|
// ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it)
|
|
if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true {
|
|
currentlyPlayingAudioPlayer = nil
|
|
tearDownRemoteCommandCenter()
|
|
}
|
|
}
|
|
activeAudioPlayers.remove(audioPlayer)
|
|
}
|
|
|
|
// MARK: - VoiceMessageAudioRecorderDelegate
|
|
|
|
func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) {
|
|
pauseAllServicesExcept(audioRecorder)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func pauseAllServicesExcept(_ service: AnyObject?) {
|
|
for audioRecorder in audioRecorders.allObjects {
|
|
if audioRecorder === service {
|
|
continue
|
|
}
|
|
|
|
// We should release the audio session only if we want to pause all services
|
|
let shouldReleaseAudioSession = (service == nil)
|
|
audioRecorder.stopRecording(releaseAudioSession: shouldReleaseAudioSession)
|
|
}
|
|
|
|
guard let audioPlayersEnumerator = audioPlayers.objectEnumerator() else {
|
|
return
|
|
}
|
|
|
|
for case let audioPlayer as VoiceMessageAudioPlayer in audioPlayersEnumerator {
|
|
if audioPlayer === service {
|
|
continue
|
|
}
|
|
|
|
audioPlayer.pause()
|
|
}
|
|
}
|
|
|
|
@objc private func handleDisplayLinkTick() {
|
|
updateNowPlayingInfoCenter()
|
|
}
|
|
|
|
private func setUpRemoteCommandCenter() {
|
|
guard BuildSettings.allowBackgroundAudioMessagePlayback else {
|
|
return
|
|
}
|
|
|
|
displayLink.isPaused = false
|
|
|
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
|
|
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
|
|
commandCenter.playCommand.isEnabled = true
|
|
commandCenter.playCommand.removeTarget(nil)
|
|
commandCenter.playCommand.addTarget { [weak self] event in
|
|
guard let audioPlayer = self?.currentlyPlayingAudioPlayer else {
|
|
return MPRemoteCommandHandlerStatus.commandFailed
|
|
}
|
|
|
|
audioPlayer.play()
|
|
|
|
return MPRemoteCommandHandlerStatus.success
|
|
}
|
|
|
|
commandCenter.pauseCommand.isEnabled = true
|
|
commandCenter.pauseCommand.removeTarget(nil)
|
|
commandCenter.pauseCommand.addTarget { [weak self] event in
|
|
guard let audioPlayer = self?.currentlyPlayingAudioPlayer else {
|
|
return MPRemoteCommandHandlerStatus.commandFailed
|
|
}
|
|
|
|
audioPlayer.pause()
|
|
|
|
return MPRemoteCommandHandlerStatus.success
|
|
}
|
|
|
|
commandCenter.skipForwardCommand.isEnabled = true
|
|
commandCenter.skipForwardCommand.removeTarget(nil)
|
|
commandCenter.skipForwardCommand.addTarget { [weak self] event in
|
|
guard let audioPlayer = self?.currentlyPlayingAudioPlayer, let skipEvent = event as? MPSkipIntervalCommandEvent else {
|
|
return MPRemoteCommandHandlerStatus.commandFailed
|
|
}
|
|
|
|
audioPlayer.seekToTime(audioPlayer.currentTime + skipEvent.interval)
|
|
|
|
return MPRemoteCommandHandlerStatus.success
|
|
}
|
|
|
|
commandCenter.skipBackwardCommand.isEnabled = true
|
|
commandCenter.skipBackwardCommand.removeTarget(nil)
|
|
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
|
|
guard let audioPlayer = self?.currentlyPlayingAudioPlayer, let skipEvent = event as? MPSkipIntervalCommandEvent else {
|
|
return MPRemoteCommandHandlerStatus.commandFailed
|
|
}
|
|
|
|
audioPlayer.seekToTime(audioPlayer.currentTime - skipEvent.interval)
|
|
|
|
return MPRemoteCommandHandlerStatus.success
|
|
}
|
|
}
|
|
|
|
private func tearDownRemoteCommandCenter() {
|
|
displayLink.isPaused = true
|
|
|
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
|
|
|
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
|
nowPlayingInfoCenter.nowPlayingInfo = nil
|
|
nowPlayingInfoCenter.playbackState = .stopped
|
|
|
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
commandCenter.playCommand.isEnabled = false
|
|
commandCenter.playCommand.removeTarget(nil)
|
|
commandCenter.pauseCommand.isEnabled = false
|
|
commandCenter.pauseCommand.removeTarget(nil)
|
|
commandCenter.skipForwardCommand.isEnabled = false
|
|
commandCenter.skipForwardCommand.removeTarget(nil)
|
|
commandCenter.skipBackwardCommand.isEnabled = false
|
|
commandCenter.skipBackwardCommand.removeTarget(nil)
|
|
}
|
|
|
|
private func updateNowPlayingInfoCenter() {
|
|
guard let audioPlayer = currentlyPlayingAudioPlayer else {
|
|
return
|
|
}
|
|
|
|
// Checks if we have a delegate for this player, or if we should update the NowPlayingInfoCenter ourselves
|
|
if let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) {
|
|
nowPlayingInfoDelegate.updateNowPlayingInfoCenter(forPlayer: audioPlayer)
|
|
} else {
|
|
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
|
nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder,
|
|
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
|
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
|
|
}
|
|
}
|
|
}
|