From 2018e94df81010035726c1dd7845ebb52e88ab97 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Jan 2022 14:56:22 +1100 Subject: [PATCH] Updated audio attachments to allow scrubbing. --- .../AttachmentPrepViewController.swift | 45 ++++++++++++++++++- .../MediaMessageView.swift | 40 ++++++++++++++++- .../OWSVideoPlayer.swift | 6 +-- .../VideoPlayerView.swift | 38 +++++++++++++--- 4 files changed, 117 insertions(+), 12 deletions(-) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index 998d7c783..0b352f6af 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -15,7 +15,7 @@ protocol AttachmentPrepViewControllerDelegate: AnyObject { // MARK: - -public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate { +public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate, MediaMessageViewAudioDelegate { // We sometimes shrink the attachment view so that it remains somewhat visible // when the keyboard is presented. public enum AttachmentViewScale { @@ -74,6 +74,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD private lazy var mediaMessageView: MediaMessageView = { let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) view.translatesAutoresizingMaskIntoConstraints = false + view.audioDelegate = self view.isHidden = (imageEditorView != nil) return view @@ -168,12 +169,15 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD mediaMessageView.videoPlayButton.isHidden = true mediaMessageView.addSubview(playerView) - // we don't want the progress bar to zoom during "pinch-to-zoom" + // We don't want the progress bar to zoom during "pinch-to-zoom" // but we do want it to shrink with the media content when the user // pops the keyboard. contentContainerView.addSubview(progressBar) contentContainerView.addSubview(playVideoButton) } + else if attachment.isAudio, mediaMessageView.audioPlayer != nil { + contentContainerView.addSubview(progressBar) + } setupLayout() } @@ -268,6 +272,13 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize), ]) } + else if attachment.isAudio, mediaMessageView.audioPlayer != nil { + NSLayoutConstraint.activate([ + progressBar.topAnchor.constraint(equalTo: view.topAnchor), + progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor), + progressBar.heightAnchor.constraint(equalToConstant: 44) + ]) + } } // MARK: - Navigation Bar @@ -326,6 +337,11 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } public func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) { + if attachment.isAudio { + mediaMessageView.pauseAudio() + return + } + guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return @@ -335,25 +351,50 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) { + if attachment.isAudio { + mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time)) + progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds) + return + } + guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return } videoPlayer.seek(to: time) + progressBar.updateState() } public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) { + if attachment.isAudio { + mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time)) + progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds) + + if mediaMessageView.wasPlayingAudio { + mediaMessageView.playAudio() + } + return + } + guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return } videoPlayer.seek(to: time) + progressBar.updateState() + if (shouldResumePlayback) { videoPlayer.play() } } + + // MARK: - MediaMessageViewAudioDelegate + + public func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) { + progressBar.manuallySetValue(progressSeconds, durationSeconds: durationSeconds) + } // MARK: - Helpers diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index d5c9c2d64..50cec8afa 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -8,6 +8,10 @@ import YYImage import NVActivityIndicatorView import SessionUIKit +public protocol MediaMessageViewAudioDelegate: AnyObject { + func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) +} + public class MediaMessageView: UIView, OWSAudioPlayerDelegate { public enum Mode: UInt { case large @@ -26,8 +30,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) }() + public var wasPlayingAudio: Bool = false public var audioProgressSeconds: CGFloat = 0 public var audioDurationSeconds: CGFloat = 0 + public weak var audioDelegate: MediaMessageViewAudioDelegate? public var playbackState = AudioPlaybackState.stopped { didSet { @@ -354,6 +360,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { imageView.isHidden = false audioPlayPauseButton.isHidden = (audioPlayer == nil) setAudioIconToPlay() + setAudioProgress(0, duration: (audioPlayer?.duration ?? 0)) fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? .withRenderingMode(.alwaysTemplate) @@ -561,6 +568,32 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } .retainUntilComplete() } + + // MARK: - Functions + + public func playAudio() { + audioPlayer?.play() + ensureButtonState() + } + + public func pauseAudio() { + wasPlayingAudio = (audioPlayer?.isPlaying == true) + + // If the 'audioPlayer' has a duration of 0 then we probably haven't played previously which + // will result in the audioPlayer having a 'duration' of 0 breaking the progressBar. We play + // the audio to get it to properly load the file right before pausing it so the data is + // loaded correctly + if audioPlayer?.duration == 0 { + audioPlayer?.play() + } + + audioPlayer?.pause() + ensureButtonState() + } + + public func setAudioTime(currentTime: TimeInterval) { + audioPlayer?.setCurrentTime(currentTime) + } // MARK: - Event Handlers @@ -594,8 +627,13 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) { + // Note: When the OWSAudioPlayer stops it sets the duration to 0 (which we want to ignore so + // the UI doesn't look buggy) + let finalDuration: CGFloat = (duration > 0 ? duration : audioDurationSeconds) audioProgressSeconds = progress - audioDurationSeconds = duration + audioDurationSeconds = finalDuration + + audioDelegate?.progressChanged(progress, durationSeconds: finalDuration) } private func setAudioIconToPlay() { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift b/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift index 3fa8828e6..581b861cc 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift @@ -58,7 +58,7 @@ public class OWSVideoPlayer: NSObject { if item.currentTime() == item.duration { // Rewind for repeated plays, but only if it previously played to end. - avPlayer.seek(to: CMTime.zero) + avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero) } avPlayer.play() @@ -67,13 +67,13 @@ public class OWSVideoPlayer: NSObject { @objc public func stop() { avPlayer.pause() - avPlayer.seek(to: CMTime.zero) + avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero) audioSession.endAudioActivity(self.audioActivity) } @objc(seekToTime:) public func seek(to time: CMTime) { - avPlayer.seek(to: time) + avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) } // MARK: private diff --git a/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift b/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift index ff4e5637b..f9edd1de7 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift @@ -88,12 +88,16 @@ public class PlayerProgressBar: UIView { let duration: CMTime = item.asset.duration slider.maximumValue = Float(CMTimeGetSeconds(duration)) - // OPTIMIZE We need a high frequency observer for smooth slider updates, - // but could use a much less frequent observer for label updates - progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] (_) in - self?.updateState() - }) as AnyObject updateState() + + // OPTIMIZE We need a high frequency observer for smooth slider updates while playing, + // but could use a much less frequent observer for label updates + progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] _ in + // If it is playing update the time + if self?.player?.rate != 0 && self?.player?.error == nil { + self?.updateState() + } + }) as AnyObject } } @@ -182,7 +186,7 @@ public class PlayerProgressBar: UIView { // MARK: Render cycle - private func updateState() { + public func updateState() { guard let player = player else { owsFailDebug("player isn't set.") return @@ -219,4 +223,26 @@ public class PlayerProgressBar: UIView { let seconds: Double = Double(slider.value) return CMTime(seconds: seconds, preferredTimescale: kPreferredTimeScale) } + + // MARK: - Functions + + public func manuallySetValue(_ positionSeconds: CGFloat, durationSeconds: CGFloat) { + let remainingSeconds = (durationSeconds - positionSeconds) + + slider.minimumValue = 0 + slider.maximumValue = Float(durationSeconds) + + positionLabel.text = formatter.string(from: positionSeconds) + + guard let remainingString = formatter.string(from: remainingSeconds) else { + owsFailDebug("unable to format time remaining") + remainingLabel.text = "0:00" + return + } + + // show remaining time as negative + remainingLabel.text = "-\(remainingString)" + + slider.setValue(Float(positionSeconds), animated: false) + } }