#4655 - Allowing voice message playback to be controlled from the lock screen or the control center.

This commit is contained in:
Stefan Ceriu
2021-08-04 16:51:22 +03:00
committed by Stefan Ceriu
parent 60d316b18f
commit bd255d370c
4 changed files with 128 additions and 5 deletions
@@ -45,6 +45,7 @@ class VoiceMessageAudioPlayer: NSObject {
private let delegateContainer = DelegateContainer()
private(set) var url: URL?
private(set) var displayName: String?
var isPlaying: Bool {
guard let audioPlayer = audioPlayer else {
@@ -68,12 +69,13 @@ class VoiceMessageAudioPlayer: NSObject {
removeObservers()
}
func loadContentFromURL(_ url: URL) {
func loadContentFromURL(_ url: URL, displayName: String? = nil) {
if self.url == url {
return
}
self.url = url
self.displayName = displayName
removeObservers()
@@ -15,20 +15,44 @@
//
import Foundation
import MediaPlayer
@objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate {
private enum Constants {
static let roomAvatarImageSize: CGFloat = 100.0
static let roomAvatarFontSize: CGFloat = 40.0
}
private let audioPlayers: NSMapTable<NSString, VoiceMessageAudioPlayer>
private let audioRecorders: NSHashTable<VoiceMessageAudioRecorder>
// Retain currently playing audio player so it doesn't stop playing on timeline cell reusage
private var displayLink: CADisplayLink!
// Retain currently playing audio player so it doesn't stop playing on timeline cell reuse
private var currentlyPlayingAudioPlayer: VoiceMessageAudioPlayer?
@objc public static let sharedProvider = VoiceMessageMediaServiceProvider()
private var roomAvatar: UIImage?
@objc public var currentRoomSummary: MXRoomSummary? {
didSet {
roomAvatar = AvatarGenerator.generateAvatar(forMatrixItem: currentRoomSummary?.roomId,
withDisplayName: currentRoomSummary?.displayname,
size: Constants.roomAvatarImageSize,
andFontSize: Constants.roomAvatarFontSize)
}
}
private override init() {
audioPlayers = NSMapTable<NSString, VoiceMessageAudioPlayer>(valueOptions: .weakMemory)
audioRecorders = NSHashTable<VoiceMessageAudioRecorder>(options: .weakMemory)
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 {
@@ -57,12 +81,21 @@ import Foundation
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
currentlyPlayingAudioPlayer = audioPlayer
setUpRemoteCommandCenter()
stopAllServicesExcept(audioPlayer)
}
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
if currentlyPlayingAudioPlayer == audioPlayer {
currentlyPlayingAudioPlayer = nil
tearDownRemoteCommandCenter()
}
}
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
if currentlyPlayingAudioPlayer == audioPlayer {
currentlyPlayingAudioPlayer = nil
tearDownRemoteCommandCenter()
}
}
@@ -96,4 +129,90 @@ import Foundation
audioPlayer.unloadContent()
}
}
@objc private func handleDisplayLinkTick() {
updateNowPlayingInfoCenter()
}
private func setUpRemoteCommandCenter() {
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
}
private func updateNowPlayingInfoCenter() {
guard let audioPlayer = currentlyPlayingAudioPlayer else {
return
}
let artwork = MPMediaItemArtwork(boundsSize: .init(width: Constants.roomAvatarImageSize, height: Constants.roomAvatarImageSize)) { [weak self] size in
return self?.roomAvatar ?? UIImage()
}
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: audioPlayer.displayName ?? "Voice message",
MPMediaItemPropertyArtist: currentRoomSummary?.displayname as Any,
MPMediaItemPropertyArtwork: artwork,
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
}
}
@@ -56,8 +56,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
let playbackView: VoiceMessagePlaybackView
init(mediaServiceProvider: VoiceMessageMediaServiceProvider,
cacheManager: VoiceMessageAttachmentCacheManager) {
init(mediaServiceProvider: VoiceMessageMediaServiceProvider, cacheManager: VoiceMessageAttachmentCacheManager) {
self.mediaServiceProvider = mediaServiceProvider
self.cacheManager = cacheManager
@@ -93,7 +92,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
audioPlayer.play()
}
} else if let url = urlToLoad {
audioPlayer.loadContentFromURL(url)
audioPlayer.loadContentFromURL(url, displayName: attachment?.originalFileName)
audioPlayer.play()
}
}
@@ -153,6 +152,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0)
}
}
details.loading = self.loading
playbackView.configureWithDetails(details)