Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voice message additions #4661

Merged
merged 6 commits into from
Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Changes to be released in next version
* Settings: The notifications toggle no longer detects the system's "Deliver Quietly" configuration as disabled (#2368).
* Settings: Adds a link to open the Settings app to quickly configure app notifications.
* VoIP: Text & icon changes on call tiles (#4642).
* Voice messages: Stop recording and go into locked mode when the application becomes inactive (#4656)
* Voice messages: Allow voice message playback control from the iOS lock screen and control center (#4655)

🐛 Bugfix
*
Expand Down
1 change: 1 addition & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1691,3 +1691,4 @@ Tap the + to start adding people.";
"voice_message_release_to_send" = "Hold to record, release to send";
"voice_message_remaining_recording_time" = "%@s left";
"voice_message_stop_locked_mode_recording" = "Tap on your recording to stop or listen";
"voice_message_lock_screen_placeholder" = "Voice message";
4 changes: 4 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4898,6 +4898,10 @@ internal enum VectorL10n {
internal static var voice: String {
return VectorL10n.tr("Vector", "voice")
}
/// Voice message
internal static var voiceMessageLockScreenPlaceholder: String {
return VectorL10n.tr("Vector", "voice_message_lock_screen_placeholder")
}
/// Hold to record, release to send
internal static var voiceMessageReleaseToSend: String {
return VectorL10n.tr("Vector", "voice_message_release_to_send")
Expand Down
2 changes: 2 additions & 0 deletions Riot/Modules/Room/RoomViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,8 @@ - (void)displayRoom:(MXKRoomDataSource *)dataSource
}

[self refreshRoomInputToolbar];

[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
}

- (void)onRoomDataSourceReady
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: .themeServiceDidChangeTheme, object: nil)
updateTheme()

NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)

updateUI()
}

Expand Down Expand Up @@ -312,6 +314,10 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
_voiceMessageToolbarView.update(theme: themeService.theme)
}

@objc private func applicationWillResignActive() {
finishRecording()
}

@objc private func handleDisplayLinkTick() {
updateUI()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,84 @@
//

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>

// 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 {
// 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)

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 {
Expand Down Expand Up @@ -57,12 +121,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()
}
}

Expand Down Expand Up @@ -96,4 +169,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: Constants.roomAvatarImageSize) { [weak self] size in
return self?.roomAvatar ?? UIImage()
}

let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: audioPlayer.displayName ?? VectorL10n.voiceMessageLockScreenPlaceholder,
MPMediaItemPropertyArtist: currentRoomSummary?.displayname as Any,
MPMediaItemPropertyArtwork: artwork,
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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)
Expand Down