mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Allow to record a voice message (#1926)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:050b2abdd29e0dea7b8aa3db65eb34491334675de3bffe8695d2b0b6cce51c15
|
||||
size 244042
|
||||
@@ -47,7 +47,7 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
override func setUp() async throws {
|
||||
audioPlayerActionsSubject = .init()
|
||||
audioPlayerSeekCallsSubject = .init()
|
||||
audioPlayerState = AudioPlayerState(duration: 10.0)
|
||||
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 10.0)
|
||||
audioPlayerMock = buildAudioPlayerMock()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,10 @@ class AudioRecorderStateTests: XCTestCase {
|
||||
|
||||
private func buildAudioRecorderMock() -> AudioRecorderMock {
|
||||
let audioRecorderMock = AudioRecorderMock()
|
||||
audioRecorderMock.isRecording = false
|
||||
audioRecorderMock.underlyingActions = audioRecorderActions
|
||||
audioRecorderMock.currentTime = 0.0
|
||||
audioRecorderMock.averagePowerForChannelNumberReturnValue = 0
|
||||
return audioRecorderMock
|
||||
}
|
||||
|
||||
@@ -44,14 +46,13 @@ class AudioRecorderStateTests: XCTestCase {
|
||||
|
||||
func testAttach() async throws {
|
||||
audioRecorderState.attachAudioRecorder(audioRecorderMock)
|
||||
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
|
||||
}
|
||||
|
||||
func testDetach() async throws {
|
||||
audioRecorderState.attachAudioRecorder(audioRecorderMock)
|
||||
|
||||
audioRecorderState.detachAudioRecorder()
|
||||
audioRecorderMock.isRecording = true
|
||||
await audioRecorderState.detachAudioRecorder()
|
||||
XCTAssert(audioRecorderMock.stopRecordingCalled)
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
class MediaPlayerProviderTests: XCTestCase {
|
||||
private var mediaPlayerProvider: MediaPlayerProvider!
|
||||
|
||||
private let oggMimeType = "audio/ogg"
|
||||
private let someURL = URL("/some/url")
|
||||
private let someOtherURL = URL("/some/other/url")
|
||||
|
||||
override func setUp() async throws {
|
||||
mediaPlayerProvider = MediaPlayerProvider()
|
||||
}
|
||||
|
||||
func testPlayerForWrongMediaType() async throws {
|
||||
let mediaSourceWithoutMimeType = MediaSourceProxy(url: someURL, mimeType: nil)
|
||||
switch mediaPlayerProvider.player(for: mediaSourceWithoutMimeType) {
|
||||
case .failure(.unsupportedMediaType):
|
||||
// Ok
|
||||
break
|
||||
default:
|
||||
XCTFail("An error is expected")
|
||||
}
|
||||
|
||||
let mediaSourceVideo = MediaSourceProxy(url: someURL, mimeType: "video/mp4")
|
||||
switch mediaPlayerProvider.player(for: mediaSourceWithoutMimeType) {
|
||||
case .failure(.unsupportedMediaType):
|
||||
// Ok
|
||||
break
|
||||
default:
|
||||
XCTFail("An error is expected")
|
||||
}
|
||||
}
|
||||
|
||||
func testPlayerFor() async throws {
|
||||
let mediaSource = MediaSourceProxy(url: someURL, mimeType: oggMimeType)
|
||||
guard case .success(let playerA) = mediaPlayerProvider.player(for: mediaSource) else {
|
||||
XCTFail("A valid player is expected")
|
||||
return
|
||||
}
|
||||
|
||||
// calling it again with another mediasource must returns the same player
|
||||
let otherMediaSource = MediaSourceProxy(url: someOtherURL, mimeType: oggMimeType)
|
||||
guard case .success(let playerB) = mediaPlayerProvider.player(for: otherMediaSource) else {
|
||||
XCTFail("A valid player is expected")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssert(playerA === playerB)
|
||||
}
|
||||
|
||||
func testPlayerStates() async throws {
|
||||
let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.random)
|
||||
// By default, there should be no player state
|
||||
XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
|
||||
let audioPlayerState = await AudioPlayerState(id: audioPlayerStateId, duration: 10.0)
|
||||
await mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
|
||||
XCTAssertEqual(audioPlayerState, mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
|
||||
await mediaPlayerProvider.unregister(audioPlayerState: audioPlayerState)
|
||||
XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
}
|
||||
|
||||
func testDetachAllStates() async throws {
|
||||
let audioPlayer = AudioPlayerMock()
|
||||
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
|
||||
|
||||
let audioPlayerStates = await Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 0), count: 10)
|
||||
for audioPlayerState in audioPlayerStates {
|
||||
await mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
|
||||
await audioPlayerState.attachAudioPlayer(audioPlayer)
|
||||
let isAttached = await audioPlayerState.isAttached
|
||||
XCTAssertTrue(isAttached)
|
||||
}
|
||||
|
||||
await mediaPlayerProvider.detachAllStates(except: nil)
|
||||
for audioPlayerState in audioPlayerStates {
|
||||
let isAttached = await audioPlayerState.isAttached
|
||||
XCTAssertFalse(isAttached)
|
||||
}
|
||||
}
|
||||
|
||||
func testDetachAllStatesWithException() async throws {
|
||||
let audioPlayer = AudioPlayerMock()
|
||||
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
|
||||
|
||||
let audioPlayerStates = await Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 0), count: 10)
|
||||
for audioPlayerState in audioPlayerStates {
|
||||
await mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
|
||||
await audioPlayerState.attachAudioPlayer(audioPlayer)
|
||||
let isAttached = await audioPlayerState.isAttached
|
||||
XCTAssertTrue(isAttached)
|
||||
}
|
||||
|
||||
let exception = audioPlayerStates[1]
|
||||
await mediaPlayerProvider.detachAllStates(except: exception)
|
||||
for audioPlayerState in audioPlayerStates {
|
||||
let isAttached = await audioPlayerState.isAttached
|
||||
if audioPlayerState == exception {
|
||||
XCTAssertTrue(isAttached)
|
||||
} else {
|
||||
XCTAssertFalse(isAttached)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ class PillContextTests: XCTestCase {
|
||||
proxyMock.members = subject.asCurrentValuePublisher()
|
||||
let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: proxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -59,6 +60,7 @@ class PillContextTests: XCTestCase {
|
||||
proxyMock.members = subject.asCurrentValuePublisher()
|
||||
let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: proxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -77,6 +79,7 @@ class PillContextTests: XCTestCase {
|
||||
mockController.roomProxy = proxyMock
|
||||
let mock = RoomScreenViewModel(timelineController: mockController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: proxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
|
||||
@@ -51,6 +51,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -84,6 +85,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -115,6 +117,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -143,6 +146,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -171,6 +175,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "")),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -195,6 +200,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -233,6 +239,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -272,6 +279,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -303,6 +311,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -322,6 +331,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -340,6 +350,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -359,6 +370,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxyMock,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -495,6 +507,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
roomProxy: roomProxy,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
|
||||
@@ -56,7 +56,10 @@ class VoiceMessageCacheTests: XCTestCase {
|
||||
|
||||
// If the file is present in the cache, its URL must be returned
|
||||
let temporaryFileURL = try createTemporaryFile(named: testFilename, withExtension: mpeg4aacFileExtension)
|
||||
let cachedURL = try voiceMessageCache.cache(mediaSource: mediaSource, using: temporaryFileURL, move: true)
|
||||
guard case .success(let cachedURL) = voiceMessageCache.cache(mediaSource: mediaSource, using: temporaryFileURL, move: true) else {
|
||||
XCTFail("A success is expected")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(cachedURL, voiceMessageCache.fileURL(for: mediaSource))
|
||||
}
|
||||
@@ -64,22 +67,20 @@ class VoiceMessageCacheTests: XCTestCase {
|
||||
func testCacheInvalidFileExtension() async throws {
|
||||
// An error should be raised if the file extension is not "m4a"
|
||||
let mpegFileURL = try createTemporaryFile(named: testFilename, withExtension: "mpg")
|
||||
do {
|
||||
_ = try voiceMessageCache.cache(mediaSource: mediaSource, using: mpegFileURL, move: true)
|
||||
guard case .failure(let error) = voiceMessageCache.cache(mediaSource: mediaSource, using: mpegFileURL, move: true) else {
|
||||
XCTFail("An error is expected")
|
||||
} catch {
|
||||
switch error as? VoiceMessageCacheError {
|
||||
case .invalidFileExtension:
|
||||
break
|
||||
default:
|
||||
XCTFail("A VoiceMessageCacheError.invalidFileExtension is expected")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(error, .invalidFileExtension)
|
||||
}
|
||||
|
||||
func testCacheCopy() async throws {
|
||||
let fileURL = try createTemporaryFile(named: testFilename, withExtension: mpeg4aacFileExtension)
|
||||
let cacheURL = try voiceMessageCache.cache(mediaSource: mediaSource, using: fileURL, move: false)
|
||||
guard case .success(let cacheURL) = voiceMessageCache.cache(mediaSource: mediaSource, using: fileURL, move: false) else {
|
||||
XCTFail("A success is expected")
|
||||
return
|
||||
}
|
||||
|
||||
// The source file must remain in its original location
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path()))
|
||||
@@ -89,7 +90,10 @@ class VoiceMessageCacheTests: XCTestCase {
|
||||
|
||||
func testCacheMove() async throws {
|
||||
let fileURL = try createTemporaryFile(named: testFilename, withExtension: mpeg4aacFileExtension)
|
||||
let cacheURL = try voiceMessageCache.cache(mediaSource: mediaSource, using: fileURL, move: true)
|
||||
guard case .success(let cacheURL) = voiceMessageCache.cache(mediaSource: mediaSource, using: fileURL, move: true) else {
|
||||
XCTFail("A success is expected")
|
||||
return
|
||||
}
|
||||
|
||||
// The file must have been moved
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: fileURL.path()))
|
||||
|
||||
@@ -93,7 +93,7 @@ class VoiceMessageMediaManagerTests: XCTestCase {
|
||||
let mediaSource = MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType)
|
||||
mediaProvider.loadFileFromSourceReturnValue = MediaFileHandleProxy.unmanaged(url: loadedFile)
|
||||
let audioConverter = AudioConverterMock()
|
||||
voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = cachedConvertedFileURL
|
||||
voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL)
|
||||
voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider,
|
||||
voiceMessageCache: voiceMessageCache,
|
||||
audioConverter: audioConverter,
|
||||
@@ -124,7 +124,7 @@ class VoiceMessageMediaManagerTests: XCTestCase {
|
||||
}
|
||||
voiceMessageCache.cacheMediaSourceUsingMoveClosure = { _, _, _ in
|
||||
cachedURL = cachedConvertedFileURL
|
||||
return cachedConvertedFileURL
|
||||
return .success(cachedConvertedFileURL)
|
||||
}
|
||||
|
||||
let audioConverter = AudioConverterMock()
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class VoiceMessageRecorderTests: XCTestCase {
|
||||
private var voiceMessageRecorder: VoiceMessageRecorder!
|
||||
|
||||
private var audioRecorder: AudioRecorderMock!
|
||||
private var mediaPlayerProvider: MediaPlayerProviderMock!
|
||||
private var audioConverter: AudioConverterMock!
|
||||
private var voiceMessageCache: VoiceMessageCacheMock!
|
||||
|
||||
private var audioPlayer: AudioPlayerMock!
|
||||
private var audioPlayerActionsSubject: PassthroughSubject<AudioPlayerAction, Never> = .init()
|
||||
private var audioPlayerActions: AnyPublisher<AudioPlayerAction, Never> {
|
||||
audioPlayerActionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let recordingURL = URL("/some/url")
|
||||
|
||||
override func setUp() async throws {
|
||||
audioRecorder = AudioRecorderMock()
|
||||
audioRecorder.recordWithReturnValue = .success(())
|
||||
audioRecorder.underlyingCurrentTime = 0
|
||||
audioRecorder.averagePowerForChannelNumberReturnValue = 0
|
||||
audioPlayer = AudioPlayerMock()
|
||||
audioPlayer.actions = audioPlayerActions
|
||||
|
||||
mediaPlayerProvider = MediaPlayerProviderMock()
|
||||
mediaPlayerProvider.playerForClosure = { _ in
|
||||
.success(self.audioPlayer)
|
||||
}
|
||||
audioConverter = AudioConverterMock()
|
||||
voiceMessageCache = VoiceMessageCacheMock()
|
||||
|
||||
voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: audioRecorder,
|
||||
mediaPlayerProvider: mediaPlayerProvider,
|
||||
audioConverter: audioConverter,
|
||||
voiceMessageCache: voiceMessageCache)
|
||||
}
|
||||
|
||||
func testStartRecording() async throws {
|
||||
audioRecorder.url = recordingURL
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
XCTAssert(audioRecorder.recordWithCalled)
|
||||
XCTAssertEqual(voiceMessageRecorder.recordingURL, audioRecorder.url)
|
||||
}
|
||||
|
||||
func testStopRecording() async throws {
|
||||
audioRecorder.isRecording = true
|
||||
audioRecorder.currentTime = 14.0
|
||||
audioRecorder.url = recordingURL
|
||||
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
// Internal audio recorder must have been stopped
|
||||
XCTAssert(audioRecorder.stopRecordingCalled)
|
||||
|
||||
// A preview player state must be available
|
||||
let previewPlayerState = voiceMessageRecorder.previewAudioPlayerState
|
||||
XCTAssertNotNil(previewPlayerState)
|
||||
XCTAssertEqual(previewPlayerState?.duration, audioRecorder.currentTime)
|
||||
}
|
||||
|
||||
func testCancelRecording() async throws {
|
||||
audioRecorder.isRecording = true
|
||||
|
||||
await voiceMessageRecorder.cancelRecording()
|
||||
|
||||
// The recording audio file must have been deleted
|
||||
XCTAssert(audioRecorder.deleteRecordingCalled)
|
||||
}
|
||||
|
||||
func testDeleteRecording() async throws {
|
||||
await voiceMessageRecorder.deleteRecording()
|
||||
XCTAssert(audioRecorder.deleteRecordingCalled)
|
||||
}
|
||||
|
||||
func testStartPlayback() async throws {
|
||||
audioRecorder.url = recordingURL
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
// if the player url doesn't match the recording url
|
||||
guard case .success = await voiceMessageRecorder.startPlayback() else {
|
||||
XCTFail("Playback should start")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssert(audioPlayer.loadMediaSourceUsingAutoplayCalled)
|
||||
XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.url, recordingURL)
|
||||
XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.mediaSource.mimeType, "audio/m4a")
|
||||
XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.mediaSource.url, recordingURL)
|
||||
XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.autoplay, true)
|
||||
XCTAssertFalse(audioPlayer.playCalled)
|
||||
}
|
||||
|
||||
func testResumePlayback() async throws {
|
||||
audioRecorder.url = recordingURL
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
// if the player url matches the recording url
|
||||
audioPlayer.url = recordingURL
|
||||
guard case .success = await voiceMessageRecorder.startPlayback() else {
|
||||
XCTFail("Playback should start")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(audioPlayer.loadMediaSourceUsingAutoplayCalled)
|
||||
XCTAssert(audioPlayer.playCalled)
|
||||
}
|
||||
|
||||
func testPausePlayback() async throws {
|
||||
audioRecorder.url = recordingURL
|
||||
switch await voiceMessageRecorder.startRecording() {
|
||||
case .failure(let error):
|
||||
XCTFail("Recording should start. \(error)")
|
||||
case .success:
|
||||
break
|
||||
}
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
voiceMessageRecorder.pausePlayback()
|
||||
XCTAssert(audioPlayer.pauseCalled)
|
||||
}
|
||||
|
||||
func testStopPlayback() async throws {
|
||||
audioRecorder.url = recordingURL
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
await voiceMessageRecorder.stopPlayback()
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, false)
|
||||
XCTAssert(audioPlayer.stopCalled)
|
||||
}
|
||||
|
||||
func testSeekPlayback() async throws {
|
||||
audioRecorder.url = recordingURL
|
||||
// Calling stop will generate the preview player state needed to have an audio player
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
voiceMessageRecorder.previewAudioPlayerState?.attachAudioPlayer(audioPlayer)
|
||||
|
||||
await voiceMessageRecorder.seekPlayback(to: 0.4)
|
||||
XCTAssert(audioPlayer.seekToCalled)
|
||||
XCTAssertEqual(audioPlayer.seekToReceivedProgress, 0.4)
|
||||
}
|
||||
|
||||
func testBuildRecordedWaveform() async throws {
|
||||
guard let audioFileUrl = Bundle(for: Self.self).url(forResource: "test_audio", withExtension: "mp3") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
audioRecorder.url = audioFileUrl
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
guard case .success(let data) = await voiceMessageRecorder.buildRecordingWaveform() else {
|
||||
XCTFail("A waveform is expected")
|
||||
return
|
||||
}
|
||||
XCTAssert(!data.isEmpty)
|
||||
}
|
||||
|
||||
func testSendVoiceMessage() async throws {
|
||||
guard let audioFileUrl = Bundle(for: Self.self).url(forResource: "test_voice_message", withExtension: "m4a") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
audioRecorder.currentTime = 42
|
||||
audioRecorder.url = audioFileUrl
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
|
||||
let roomProxy = RoomProxyMock()
|
||||
let audioConverter = AudioConverterMock()
|
||||
var convertedFileUrl: URL?
|
||||
var convertedFileSize: UInt64?
|
||||
|
||||
audioConverter.convertToOpusOggSourceURLDestinationURLClosure = { source, destination in
|
||||
convertedFileUrl = destination
|
||||
try? FileManager.default.removeItem(at: destination)
|
||||
let internalConverter = AudioConverter()
|
||||
try internalConverter.convertToOpusOgg(sourceURL: source, destinationURL: destination)
|
||||
convertedFileSize = try? UInt64(FileManager.default.sizeForItem(at: destination))
|
||||
// the source URL must be the recorded file
|
||||
XCTAssertEqual(source, audioFileUrl)
|
||||
// check the converted file extension
|
||||
XCTAssertEqual(destination.pathExtension, "ogg")
|
||||
}
|
||||
|
||||
roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = { url, audioInfo, waveform, _, _ in
|
||||
XCTAssertEqual(url, convertedFileUrl)
|
||||
XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime)
|
||||
XCTAssertEqual(audioInfo.size, convertedFileSize)
|
||||
XCTAssertEqual(audioInfo.mimetype, "audio/ogg")
|
||||
XCTAssertFalse(waveform.isEmpty)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
guard case .success = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
|
||||
XCTFail("A success is expected")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled)
|
||||
XCTAssert(roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled)
|
||||
|
||||
// the converted file must have been deleted
|
||||
if let convertedFileUrl {
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: convertedFileUrl.path()))
|
||||
} else {
|
||||
XCTFail("converted file URL is missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26fdd567b146251130beb6f662ec227ad48d734561c759b072a0e37d8f804789
|
||||
size 96929
|
||||
oid sha256:4847895e8d01132c5fbef0cd2a5b3ac8c394ae197a15b37b7d9fff53cfa0bab9
|
||||
size 95668
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1a14094723fbe08e8ebf64f6a7f567e57e8835533fcb3c2f03f806acded2af54
|
||||
size 65817
|
||||
oid sha256:efc7fa1ab541e5dd8bb1757ed782065e7c7e84d7354fe90702f1091134a24467
|
||||
size 63847
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee69c3ef27115aeee58aacd362b3d382480b47bf1e53ffd1ce72880319d11e67
|
||||
size 60199
|
||||
oid sha256:af1d1d6fcf418f97cc9aae336b349b3630a1e2995c26ab8565421c97a823935a
|
||||
size 59818
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76d5359eca0ee480fc6197652baf9b282d342cfd1e84893afb4a469c572f24a9
|
||||
size 57410
|
||||
oid sha256:73435a7b6a9a06ee58de540c559cbe5a2492bb9c9617260b303957d54dcf195c
|
||||
size 56863
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8dfd0301b8fd911e44b1f5f1caf9b74f575eb57e74d39a7765bde7966e5872d0
|
||||
size 63481
|
||||
Reference in New Issue
Block a user