Updated audio attachments to allow scrubbing.

This commit is contained in:
Morgan Pretty 2022-01-13 14:56:22 +11:00
parent e6c90c5e18
commit 2018e94df8
4 changed files with 117 additions and 12 deletions

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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)
}
}