mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Fix playback and recording of voice messages (#1948)
This commit is contained in:
@@ -447,11 +447,11 @@ class AudioRecorderMock: AudioRecorderProtocol {
|
||||
var deleteRecordingCalled: Bool {
|
||||
return deleteRecordingCallsCount > 0
|
||||
}
|
||||
var deleteRecordingClosure: (() -> Void)?
|
||||
var deleteRecordingClosure: (() async -> Void)?
|
||||
|
||||
func deleteRecording() {
|
||||
func deleteRecording() async {
|
||||
deleteRecordingCallsCount += 1
|
||||
deleteRecordingClosure?()
|
||||
await deleteRecordingClosure?()
|
||||
}
|
||||
//MARK: - averagePowerForChannelNumber
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ struct ComposerToolbar: View {
|
||||
} stopRecording: {
|
||||
if let voiceMessageRecordingStartTime, Date.now.timeIntervalSince(voiceMessageRecordingStartTime) < voiceMessageMinimumRecordingDuration {
|
||||
context.send(viewAction: .cancelVoiceMessageRecording)
|
||||
withAnimation {
|
||||
withElementAnimation {
|
||||
showVoiceMessageRecordingTooltip = true
|
||||
}
|
||||
} else {
|
||||
@@ -273,7 +273,7 @@ struct ComposerToolbar: View {
|
||||
.allowsHitTesting(false)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + voiceMessageTooltipDuration) {
|
||||
withAnimation {
|
||||
withElementAnimation {
|
||||
showVoiceMessageRecordingTooltip = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,42 +42,38 @@ class AudioRecorder: NSObject, AudioRecorderProtocol, AVAudioRecorderDelegate {
|
||||
audioRecorder?.isRecording ?? false
|
||||
}
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "io.element.elementx.audio_recorder", qos: .userInitiated)
|
||||
private var stopped = false
|
||||
|
||||
func record(with recordID: AudioRecordingIdentifier) async -> Result<Void, AudioRecorderError> {
|
||||
stopped = false
|
||||
guard await requestRecordPermission() else {
|
||||
return .failure(.recordPermissionNotGranted)
|
||||
}
|
||||
|
||||
let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
||||
AVSampleRateKey: 48000,
|
||||
AVEncoderBitRateKey: 128_000,
|
||||
AVNumberOfChannelsKey: 1,
|
||||
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
let url = URL.temporaryDirectory.appendingPathComponent("voice-message-\(recordID.identifier).m4a")
|
||||
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
|
||||
audioRecorder?.delegate = self
|
||||
audioRecorder?.isMeteringEnabled = true
|
||||
audioRecorder?.record()
|
||||
let result = await startRecording(with: recordID)
|
||||
switch result {
|
||||
case .success:
|
||||
actionsSubject.send(.didStartRecording)
|
||||
} catch {
|
||||
MXLog.error("audio recording failed: \(error)")
|
||||
case .failure(let error):
|
||||
actionsSubject.send(.didFailWithError(error: error))
|
||||
}
|
||||
return .success(())
|
||||
return result
|
||||
}
|
||||
|
||||
func stopRecording() async {
|
||||
guard let audioRecorder, audioRecorder.isRecording else {
|
||||
return
|
||||
await withCheckedContinuation { continuation in
|
||||
stopRecording {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
audioRecorder.stop()
|
||||
}
|
||||
|
||||
func deleteRecording() {
|
||||
audioRecorder?.deleteRecording()
|
||||
func deleteRecording() async {
|
||||
await withCheckedContinuation { continuation in
|
||||
deleteRecording {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func peakPowerForChannelNumber(_ channelNumber: Int) -> Float {
|
||||
@@ -121,7 +117,69 @@ class AudioRecorder: NSObject, AudioRecorderProtocol, AVAudioRecorderDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func startRecording(with recordID: AudioRecordingIdentifier) async -> Result<Void, AudioRecorderError> {
|
||||
await withCheckedContinuation { continuation in
|
||||
startRecording(with: recordID) { result in
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startRecording(with recordID: AudioRecordingIdentifier, completion: @escaping (Result<Void, AudioRecorderError>) -> Void) {
|
||||
dispatchQueue.async { [weak self] in
|
||||
guard let self, !self.stopped else {
|
||||
completion(.failure(.recordingCancelled))
|
||||
return
|
||||
}
|
||||
let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
||||
AVSampleRateKey: 48000,
|
||||
AVEncoderBitRateKey: 128_000,
|
||||
AVNumberOfChannelsKey: 1,
|
||||
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
let url = URL.temporaryDirectory.appendingPathComponent("voice-message-\(recordID.identifier).m4a")
|
||||
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
|
||||
audioRecorder?.delegate = self
|
||||
audioRecorder?.isMeteringEnabled = true
|
||||
audioRecorder?.record()
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
MXLog.error("audio recording failed: \(error)")
|
||||
completion(.failure(.genericError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopRecording(completion: @escaping () -> Void) {
|
||||
dispatchQueue.async { [weak self] in
|
||||
defer {
|
||||
completion()
|
||||
}
|
||||
guard let self else { return }
|
||||
stopped = true
|
||||
guard let audioRecorder, audioRecorder.isRecording else {
|
||||
return
|
||||
}
|
||||
audioRecorder.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteRecording(completion: @escaping () -> Void) {
|
||||
dispatchQueue.async { [weak self] in
|
||||
defer {
|
||||
completion()
|
||||
}
|
||||
guard let self else { return }
|
||||
audioRecorder?.deleteRecording()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVAudioRecorderDelegate
|
||||
|
||||
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully success: Bool) {
|
||||
|
||||
@@ -33,6 +33,7 @@ extension AudioRecordingIdentifier {
|
||||
enum AudioRecorderError: Error {
|
||||
case genericError
|
||||
case recordPermissionNotGranted
|
||||
case recordingCancelled
|
||||
}
|
||||
|
||||
enum AudioRecorderAction {
|
||||
@@ -49,7 +50,7 @@ protocol AudioRecorderProtocol: AnyObject {
|
||||
|
||||
func record(with recordID: AudioRecordingIdentifier) async -> Result<Void, AudioRecorderError>
|
||||
func stopRecording() async
|
||||
func deleteRecording()
|
||||
func deleteRecording() async
|
||||
func averagePowerForChannelNumber(_ channelNumber: Int) -> Float
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ class MediaPlayerProvider: MediaPlayerProviderProtocol {
|
||||
return audioPlayerStates[audioPlayerStateID]
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func register(audioPlayerState: AudioPlayerState) {
|
||||
guard let audioPlayerStateID = audioPlayerStateID(for: audioPlayerState.id) else {
|
||||
MXLog.error("Failed to build a key to register this audioPlayerState: \(audioPlayerState)")
|
||||
@@ -57,7 +56,6 @@ class MediaPlayerProvider: MediaPlayerProviderProtocol {
|
||||
audioPlayerStates[audioPlayerStateID] = audioPlayerState
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func unregister(audioPlayerState: AudioPlayerState) {
|
||||
guard let audioPlayerStateID = audioPlayerStateID(for: audioPlayerState.id) else {
|
||||
MXLog.error("Failed to build a key to register this audioPlayerState: \(audioPlayerState)")
|
||||
@@ -66,12 +64,12 @@ class MediaPlayerProvider: MediaPlayerProviderProtocol {
|
||||
audioPlayerStates[audioPlayerStateID] = nil
|
||||
}
|
||||
|
||||
func detachAllStates(except exception: AudioPlayerState?) async {
|
||||
func detachAllStates(except exception: AudioPlayerState?) {
|
||||
for key in audioPlayerStates.keys {
|
||||
if let exception, key == audioPlayerStateID(for: exception.id) {
|
||||
continue
|
||||
}
|
||||
await audioPlayerStates[key]?.detachAudioPlayer()
|
||||
audioPlayerStates[key]?.detachAudioPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ enum MediaPlayerProviderError: Error {
|
||||
case unsupportedMediaType
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol MediaPlayerProviderProtocol {
|
||||
func player(for mediaSource: MediaSourceProxy) -> Result<MediaPlayerProtocol, MediaPlayerProviderError>
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
if audioPlayer.state == .playing {
|
||||
audioPlayer.pause()
|
||||
} else {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayer)
|
||||
audioPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,14 +68,14 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
|
||||
func cancelRecording() async {
|
||||
await audioRecorder.stopRecording()
|
||||
audioRecorder.deleteRecording()
|
||||
await audioRecorder.deleteRecording()
|
||||
recordingURL = nil
|
||||
previewAudioPlayerState = nil
|
||||
}
|
||||
|
||||
func deleteRecording() async {
|
||||
await stopPlayback()
|
||||
audioRecorder.deleteRecording()
|
||||
await audioRecorder.deleteRecording()
|
||||
previewAudioPlayerState = nil
|
||||
recordingURL = nil
|
||||
}
|
||||
@@ -189,7 +189,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
|
||||
// Build the preview audio player
|
||||
let mediaSource = MediaSourceProxy(url: url, mimeType: mp4accMimeType)
|
||||
guard case .success(let mediaPlayer) = mediaPlayerProvider.player(for: mediaSource), let audioPlayer = mediaPlayer as? AudioPlayerProtocol else {
|
||||
guard case .success(let mediaPlayer) = await mediaPlayerProvider.player(for: mediaSource), let audioPlayer = mediaPlayer as? AudioPlayerProtocol else {
|
||||
return .failure(.previewNotAvailable)
|
||||
}
|
||||
previewAudioPlayer = audioPlayer
|
||||
|
||||
Reference in New Issue
Block a user