Add scrubbing

This commit is contained in:
David Langley
2021-09-22 09:36:20 +01:00
parent a178f587c2
commit 84ad1ec372
6 changed files with 147 additions and 10 deletions
@@ -121,8 +121,9 @@ class VoiceMessageAudioPlayer: NSObject {
audioPlayer?.seek(to: .zero)
}
func seekToTime(_ time: TimeInterval) {
audioPlayer?.seek(to: CMTime(seconds: time, preferredTimescale: 60000))
func seekToTime(_ time: TimeInterval, completionHandler:@escaping (Bool) -> Void = { _ in }) {
isStopped = false
audioPlayer?.seek(to: CMTime(seconds: time, preferredTimescale: 60000), completionHandler: completionHandler)
}
func registerDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) {
@@ -48,6 +48,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
private var audioSamples: [Float] = []
private var isInLockedMode: Bool = false
private var notifiedRemainingTime = false
private var recordDuration: TimeInterval?
private static let elapsedTimeFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
@@ -156,6 +157,28 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
}
}
func voiceMessageToolbarViewRequestedFormattedTimestamp(for progress: CGFloat) -> String? {
guard let duration = recordDuration else {
return nil
}
return VoiceMessageController.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: duration * progress))
}
func voiceMessageToolbarViewDidRequestSeek(to progress: CGFloat) {
guard let audioPlayer = audioPlayer,
let duration = recordDuration else {
return
}
if audioPlayer.url == nil {
audioPlayer.loadContentFromURL(temporaryFileURL)
}
audioPlayer.seekToTime(duration * progress) { [weak self] _ in
self?.updateUI()
}
}
func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) {
audioPlayer?.stop()
audioRecorder?.stopRecording()
@@ -213,6 +236,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
private func finishRecording() {
let recordDuration = audioRecorder?.currentTime
self.recordDuration = recordDuration
audioRecorder?.stopRecording()
guard isInLockedMode else {
@@ -406,10 +430,12 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
var details = VoiceMessageToolbarViewDetails()
details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle))
details.elapsedTime = VoiceMessageController.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration)))
// Show the current time if the player is paused or playing but not when stopped.
details.elapsedTime = VoiceMessageController.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (!audioPlayer.isStopped ? audioPlayer.currentTime : audioPlayer.duration)))
details.audioSamples = audioSamples
details.isPlaying = audioPlayer.isPlaying
details.progress = (audioPlayer.isPlaying ? (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) : 0.0)
// Set progress if the player is paused or playing but not when stopped.
details.progress = (!audioPlayer.isStopped ? (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) : 0.0)
_voiceMessageToolbarView.configureWithDetails(details)
}
@@ -97,6 +97,29 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
}
}
func voiceMessagePlaybackViewRequestedFormattedTimestamp(for progress: CGFloat) -> String? {
return VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration * progress))
}
func voiceMessagePlaybackViewDidRequestSeek(to progress: CGFloat) {
guard let audioPlayer = audioPlayer else {
return
}
if state == .stopped {
state = .paused
}
if audioPlayer.url == nil,
let url = urlToLoad {
audioPlayer.loadContentFromURL(url, displayName: attachment?.originalFileName)
}
audioPlayer.seekToTime(self.duration * progress) { [weak self] _ in
self?.updateUI()
}
}
func voiceMessagePlaybackViewDidChangeWidth() {
loadAttachmentData()
}
@@ -16,9 +16,13 @@
import Foundation
import Reusable
import UIKit
import MatrixSDK
protocol VoiceMessagePlaybackViewDelegate: AnyObject {
func voiceMessagePlaybackViewDidRequestPlaybackToggle()
func voiceMessagePlaybackViewRequestedFormattedTimestamp(for progress: CGFloat) -> String?
func voiceMessagePlaybackViewDidRequestSeek(to progress: CGFloat)
func voiceMessagePlaybackViewDidChangeWidth()
}
@@ -40,12 +44,19 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
private var _waveformView: VoiceMessageWaveformView!
private var currentTheme: Theme?
private var scrubProgress: CGFloat? {
didSet {
MXLog.debug("change progress \(scrubProgress)")
}
}
@IBOutlet private var backgroundView: UIView!
@IBOutlet private var recordingIcon: UIView!
@IBOutlet private var playButton: UIButton!
@IBOutlet private var elapsedTimeLabel: UILabel!
@IBOutlet private var waveformContainerView: UIView!
@IBOutlet private var panGestureRecognizer: UIPanGestureRecognizer!
weak var delegate: VoiceMessagePlaybackViewDelegate?
@@ -94,19 +105,17 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
}
if details.loading {
elapsedTimeLabel.text = "--:--"
_waveformView.progress = 0
_waveformView.samples = []
_waveformView.alpha = 0.3
} else {
elapsedTimeLabel.text = details.currentTime
_waveformView.progress = details.progress
_waveformView.samples = details.samples
_waveformView.alpha = 1.0
}
self.details = details
updateElapsedTime()
guard let theme = currentTheme else {
return
}
@@ -120,6 +129,20 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
elapsedTimeLabel.textColor = theme.colors.tertiaryContent
}
private func updateElapsedTime() {
guard let details = details,
!details.loading else {
elapsedTimeLabel.text = "--:--"
return
}
if let scrubProgress = scrubProgress,
let scrubTime = delegate?.voiceMessagePlaybackViewRequestedFormattedTimestamp(for: scrubProgress) {
elapsedTimeLabel.text = scrubTime
} else {
elapsedTimeLabel.text = details.currentTime
}
}
func getRequiredNumberOfSamples() -> Int {
_waveformView.setNeedsLayout()
_waveformView.layoutIfNeeded()
@@ -138,4 +161,31 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
@IBAction private func onPlayButtonTap() {
delegate?.voiceMessagePlaybackViewDidRequestPlaybackToggle()
}
@IBAction private func tap(gestureRecognizer: UITapGestureRecognizer) {
let x = gestureRecognizer.location(in: waveformContainerView).x.clamped(to: 0...waveformContainerView.bounds.width)
let progress = x / waveformContainerView.bounds.width
let seekPoint = progress == 1 ? 0 : progress
delegate?.voiceMessagePlaybackViewDidRequestSeek(to: seekPoint)
}
@IBAction private func pan(gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .possible, .cancelled, .failed:
scrubProgress = nil
case .began, .changed:
let x = gestureRecognizer.location(in: waveformContainerView).x.clamped(to: 0...waveformContainerView.bounds.width)
scrubProgress = x / waveformContainerView.bounds.width
case .ended:
let seekPoint = scrubProgress == 1 ? 0 : scrubProgress ?? 0
MXLog.debug("progress end seekPoint --->\(seekPoint)")
delegate?.voiceMessagePlaybackViewDidRequestSeek(to: seekPoint)
scrubProgress = nil
@unknown default:
break
}
updateElapsedTime()
}
}
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -50,6 +50,11 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7Fl-yZ-dZB">
<rect key="frame" x="94" y="7" width="317" height="30"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<connections>
<outletCollection property="gestureRecognizers" destination="ZQp-nu-e9K" appends="YES" id="98b-lQ-pW3"/>
<outletCollection property="gestureRecognizers" destination="WD8-Zc-Vke" appends="YES" id="6pf-Ca-Npj"/>
</connections>
</view>
</subviews>
<constraints>
@@ -75,15 +80,26 @@
<connections>
<outlet property="backgroundView" destination="LPc-i8-8UC" id="mfD-md-nTj"/>
<outlet property="elapsedTimeLabel" destination="eAi-HM-Wvj" id="z70-aJ-O90"/>
<outlet property="panGestureRecognizer" destination="ZQp-nu-e9K" id="h67-Te-8U3"/>
<outlet property="playButton" destination="GL1-b8-dZK" id="5u7-CG-d99"/>
<outlet property="recordingIcon" destination="REB-gl-h0h" id="uL1-nI-bhF"/>
<outlet property="waveformContainerView" destination="7Fl-yZ-dZB" id="f9u-wS-jvG"/>
</connections>
<point key="canvasLocation" x="-1742.753623188406" y="-299.33035714285711"/>
</view>
<panGestureRecognizer minimumNumberOfTouches="1" id="ZQp-nu-e9K">
<connections>
<action selector="panWithGestureRecognizer:" destination="cGR-49-HWB" id="McO-Pq-HFs"/>
</connections>
</panGestureRecognizer>
<tapGestureRecognizer id="WD8-Zc-Vke">
<connections>
<action selector="tapWithGestureRecognizer:" destination="cGR-49-HWB" id="6qM-N2-zmN"/>
</connections>
</tapGestureRecognizer>
</objects>
<resources>
<image name="voice_message_play_button" width="12.5" height="15"/>
<image name="voice_message_play_button" width="15.5" height="15"/>
<image name="voice_message_record_icon" width="10" height="10"/>
</resources>
</document>
@@ -23,6 +23,8 @@ protocol VoiceMessageToolbarViewDelegate: AnyObject {
func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewRequestedFormattedTimestamp(for progress: CGFloat) -> String?
func voiceMessageToolbarViewDidRequestSeek(to progress: CGFloat)
func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView)
}
@@ -93,6 +95,7 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture
private var lockChevronToRecordButtonDistance: CGFloat = 0.0
private var lockChevronToLockButtonDistance: CGFloat = 0.0
private var panDirection: UISwipeGestureRecognizer.Direction?
private var tapGesture: UITapGestureRecognizer!
private var details: VoiceMessageToolbarViewDetails?
@@ -126,6 +129,8 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleWaveformTap))
playbackView.waveformView.addGestureRecognizer(tapGesture)
tapGesture.delegate = self
self.tapGesture = tapGesture
updateUIWithDetails(VoiceMessageToolbarViewDetails(), animated: false)
}
@@ -183,12 +188,28 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture
return true
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.tapGesture, self.lastUIState != .lockedModeRecord {
return false
}
return true
}
// MARK: - VoiceMessagePlaybackViewDelegate
func voiceMessagePlaybackViewDidRequestPlaybackToggle() {
delegate?.voiceMessageToolbarViewDidRequestPlaybackToggle(self)
}
func voiceMessagePlaybackViewRequestedFormattedTimestamp(for progress: CGFloat) -> String? {
return delegate?.voiceMessageToolbarViewRequestedFormattedTimestamp(for: progress)
}
func voiceMessagePlaybackViewDidRequestSeek(to progress: CGFloat) {
delegate?.voiceMessageToolbarViewDidRequestSeek(to: progress)
}
func voiceMessagePlaybackViewDidChangeWidth() {
}