Updated audio attachments to allow scrubbing.
This commit is contained in:
parent
e6c90c5e18
commit
2018e94df8
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue