mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-26 19:34:25 +02:00
746391ae23
* Cancel automatically a Voice Broadcast from a room after an app crash * We limit for the moment the uncompleted voice broadcast cleaning to the breadcrumbs rooms list By considering potential performance issues, we decided to limit the uncompleted VB cleaning in the recents list service: - we run the process only once when the application is resumed - we run the process only on the breadcrumbs rooms list This prevent us from checking several times in parallel the same room (because a room may be listed in several recents sections) This prevent us from checking several times the same room (each room should be checked only once after the app resume) If a room is not checked from the recents list, a sanity check is still done in RoomViewController (to clean the room when the user opens it)
320 lines
14 KiB
Swift
320 lines
14 KiB
Swift
//
|
|
// Copyright 2022 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 Foundation
|
|
|
|
/// VoiceBroadcastService handles voice broadcast.
|
|
/// Note: Cannot use a protocol because of Objective-C compatibility
|
|
@objcMembers
|
|
public class VoiceBroadcastService: NSObject {
|
|
|
|
// MARK: - Properties
|
|
|
|
public let room: MXRoom
|
|
public private(set) var voiceBroadcastId: String?
|
|
public private(set) var state: VoiceBroadcastInfoState
|
|
// Mechanism to process one call of sendVoiceBroadcastInfo() at a time
|
|
private let asyncTaskQueue: MXAsyncTaskQueue
|
|
|
|
// MARK: - Setup
|
|
|
|
public init(room: MXRoom, state: VoiceBroadcastInfoState) {
|
|
self.room = room
|
|
self.state = state
|
|
self.asyncTaskQueue = MXAsyncTaskQueue(label: "VoiceBroadcastServiceQueueEventSerialQueue-" + MXTools.generateSecret())
|
|
}
|
|
|
|
// MARK: - Constants
|
|
|
|
// MARK: - Public
|
|
|
|
// MARK: Voice broadcast info
|
|
|
|
/// Start a voice broadcast.
|
|
/// - Parameters:
|
|
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
|
|
func startVoiceBroadcast(completion: @escaping (MXResponse<String?>) -> Void) {
|
|
sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.started) { [weak self] response in
|
|
guard let self = self else { return }
|
|
|
|
switch response {
|
|
case .success((let eventIdResponse)):
|
|
self.voiceBroadcastId = eventIdResponse
|
|
completion(.success(eventIdResponse))
|
|
case .failure(let error):
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pause a voice broadcast.
|
|
/// - Parameters:
|
|
/// - lastChunkSequence: The last sent chunk number.
|
|
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
|
|
func pauseVoiceBroadcast(lastChunkSequence: Int, completion: @escaping (MXResponse<String?>) -> Void) {
|
|
sendVoiceBroadcastInfo(lastChunkSequence: lastChunkSequence, state: VoiceBroadcastInfoState.paused, completion: completion)
|
|
}
|
|
|
|
/// resume a voice broadcast.
|
|
/// - Parameters:
|
|
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
|
|
func resumeVoiceBroadcast(completion: @escaping (MXResponse<String?>) -> Void) {
|
|
sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.resumed, completion: completion)
|
|
}
|
|
|
|
/// stop a voice broadcast info.
|
|
/// - Parameters:
|
|
/// - lastChunkSequence: The last sent chunk number.
|
|
/// - voiceBroadcastId: The VoiceBroadcast identifier to stop. Use it only to force stop a specific VoiceBroadcast.
|
|
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
|
|
func stopVoiceBroadcast(lastChunkSequence: Int,
|
|
voiceBroadcastId: String? = nil,
|
|
completion: @escaping (MXResponse<String?>) -> Void) {
|
|
if let voiceBroadcastId = voiceBroadcastId {
|
|
self.voiceBroadcastId = voiceBroadcastId
|
|
}
|
|
sendVoiceBroadcastInfo(lastChunkSequence: lastChunkSequence, state: VoiceBroadcastInfoState.stopped, completion: completion)
|
|
}
|
|
|
|
func getState() -> String {
|
|
return self.state.rawValue
|
|
}
|
|
|
|
// MARK: Voice broadcast chunk
|
|
|
|
/// Send a bunch of a voice broadcast.
|
|
///
|
|
/// While sending, a fake event will be echoed in the messages list.
|
|
/// Once complete, this local echo will be replaced by the event saved by the homeserver.
|
|
///
|
|
/// - Parameters:
|
|
/// - audioFileLocalURL: the local filesystem path of the audio file to send.
|
|
/// - mimeType: (optional) the mime type of the file. Defaults to `audio/ogg`
|
|
/// - duration: the length of the voice message in milliseconds
|
|
/// - samples: an array of floating point values normalized to [0, 1], boxed within NSNumbers
|
|
/// - sequence: value of the chunk sequence.
|
|
/// - success: A block object called when the operation succeeds. It returns the event id of the event generated on the homeserver
|
|
/// - failure: A block object called when the operation fails.
|
|
func sendChunkOfVoiceBroadcast(audioFileLocalURL: URL,
|
|
mimeType: String?,
|
|
duration: UInt,
|
|
sequence: UInt,
|
|
success: @escaping ((String?) -> Void),
|
|
failure: @escaping ((Error?) -> Void)) {
|
|
guard let voiceBroadcastId = self.voiceBroadcastId else {
|
|
return failure(VoiceBroadcastServiceError.notStarted)
|
|
}
|
|
|
|
self.room.sendChunkOfVoiceBroadcast(localURL: audioFileLocalURL,
|
|
voiceBroadcastId: voiceBroadcastId,
|
|
mimeType: mimeType,
|
|
duration: duration,
|
|
sequence: sequence,
|
|
success: success,
|
|
failure: failure)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func allowedStates(from state: VoiceBroadcastInfoState) -> [VoiceBroadcastInfoState] {
|
|
switch state {
|
|
case .started:
|
|
return [.paused, .stopped]
|
|
case .paused:
|
|
return [.resumed, .stopped]
|
|
case .resumed:
|
|
return [.paused, .stopped]
|
|
case .stopped:
|
|
return [.started, .stopped]
|
|
}
|
|
}
|
|
|
|
private func sendVoiceBroadcastInfo(lastChunkSequence: Int = 0,
|
|
state: VoiceBroadcastInfoState,
|
|
completion: @escaping (MXResponse<String?>) -> Void) {
|
|
guard let userId = self.room.mxSession.myUserId else {
|
|
completion(.failure(VoiceBroadcastServiceError.missingUserId))
|
|
return
|
|
}
|
|
|
|
asyncTaskQueue.async { (taskCompleted) in
|
|
guard self.allowedStates(from: self.state).contains(state) else {
|
|
MXLog.warning("[VoiceBroadcastService] sendVoiceBroadcastInfo: unexpected state change \(self.state) -> \(state)")
|
|
completion(.failure(VoiceBroadcastServiceError.unexpectedState))
|
|
taskCompleted()
|
|
return
|
|
}
|
|
|
|
let stateKey = userId
|
|
|
|
let voiceBroadcastInfo = VoiceBroadcastInfo()
|
|
|
|
voiceBroadcastInfo.deviceId = self.room.mxSession.myDeviceId
|
|
|
|
voiceBroadcastInfo.state = state.rawValue
|
|
|
|
voiceBroadcastInfo.lastChunkSequence = lastChunkSequence
|
|
|
|
if state != VoiceBroadcastInfoState.started {
|
|
guard let voiceBroadcastId = self.voiceBroadcastId else {
|
|
completion(.failure(VoiceBroadcastServiceError.notStarted))
|
|
taskCompleted()
|
|
return
|
|
}
|
|
|
|
voiceBroadcastInfo.voiceBroadcastId = voiceBroadcastId
|
|
} else {
|
|
voiceBroadcastInfo.chunkLength = BuildSettings.voiceBroadcastChunkLength
|
|
}
|
|
|
|
guard let stateEventContent = voiceBroadcastInfo.jsonDictionary() as? [String: Any] else {
|
|
completion(.failure(VoiceBroadcastServiceError.unknown))
|
|
taskCompleted()
|
|
return
|
|
}
|
|
|
|
self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType),
|
|
content: stateEventContent, stateKey: stateKey) { [weak self] response in
|
|
guard let self = self else { return }
|
|
|
|
switch response {
|
|
case .success(let object):
|
|
self.state = state
|
|
completion(.success(object))
|
|
case .failure(let error):
|
|
completion(.failure(error))
|
|
}
|
|
taskCompleted()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Objective-C interface
|
|
extension VoiceBroadcastService {
|
|
|
|
/// Start a voice broadcast.
|
|
/// - Parameters:
|
|
/// - success: A closure called when the operation is complete.
|
|
/// - failure: A closure called when the operation fails.
|
|
@objc public func startVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) {
|
|
self.startVoiceBroadcast { response in
|
|
switch response {
|
|
case .success(let object):
|
|
success(object)
|
|
case .failure(let error):
|
|
failure(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pause a voice broadcast.
|
|
/// - Parameters:
|
|
/// - lastChunkSequence: The last sent chunk number.
|
|
/// - success: A closure called when the operation is complete.
|
|
/// - failure: A closure called when the operation fails.
|
|
@objc public func pauseVoiceBroadcast(lastChunkSequence: Int,
|
|
success: @escaping (String?) -> Void,
|
|
failure: @escaping (Error) -> Void) {
|
|
self.pauseVoiceBroadcast(lastChunkSequence: lastChunkSequence) { response in
|
|
switch response {
|
|
case .success(let object):
|
|
success(object)
|
|
case .failure(let error):
|
|
failure(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resume a voice broadcast.
|
|
/// - Parameters:
|
|
/// - success: A closure called when the operation is complete.
|
|
/// - failure: A closure called when the operation fails.
|
|
@objc public func resumeVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) {
|
|
self.resumeVoiceBroadcast { response in
|
|
switch response {
|
|
case .success(let object):
|
|
success(object)
|
|
case .failure(let error):
|
|
failure(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stop a voice broadcast.
|
|
/// - Parameters:
|
|
/// - lastChunkSequence: The last sent chunk number.
|
|
/// - success: A closure called when the operation is complete.
|
|
/// - failure: A closure called when the operation fails.
|
|
@objc public func stopVoiceBroadcast(lastChunkSequence: Int,
|
|
success: @escaping (String?) -> Void,
|
|
failure: @escaping (Error) -> Void) {
|
|
self.stopVoiceBroadcast(lastChunkSequence: lastChunkSequence) { response in
|
|
switch response {
|
|
case .success(let object):
|
|
success(object)
|
|
case .failure(let error):
|
|
failure(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Internal room additions
|
|
extension MXRoom {
|
|
|
|
/// Send a voice broadcast to the room.
|
|
/// - Parameters:
|
|
/// - localURL: the local filesystem path of the file to send.
|
|
/// - voiceBroadcastId: The event id of the started voice broadcast info state event
|
|
/// - mimeType: (optional) the mime type of the file. Defaults to `audio/ogg`.
|
|
/// - duration: the length of the voice message in milliseconds
|
|
/// - samples: an array of floating point values normalized to [0, 1]
|
|
/// - threadId: the id of the thread to send the message. nil by default.
|
|
/// - sequence: value of the chunk sequence.
|
|
/// - success: A closure called when the operation is complete.
|
|
/// - failure: A closure called when the operation fails.
|
|
/// - Returns: a `MXHTTPOperation` instance.
|
|
@nonobjc @discardableResult func sendChunkOfVoiceBroadcast(localURL: URL,
|
|
voiceBroadcastId: String,
|
|
mimeType: String?,
|
|
duration: UInt,
|
|
threadId: String? = nil,
|
|
sequence: UInt,
|
|
success: @escaping ((String?) -> Void),
|
|
failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? {
|
|
guard let relatesTo = MXEventContentRelatesTo(relationType: MXEventRelationTypeReference,
|
|
eventId: voiceBroadcastId).jsonDictionary() as? [String: Any] else {
|
|
failure(VoiceBroadcastServiceError.unknown)
|
|
return nil
|
|
}
|
|
|
|
let sequenceValue = [VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence: sequence]
|
|
|
|
return __sendVoiceMessage(localURL,
|
|
additionalContentParams: [kMXEventRelationRelatesToKey: relatesTo,
|
|
VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: sequenceValue],
|
|
mimeType: mimeType,
|
|
duration: duration,
|
|
samples: nil,
|
|
threadId: threadId,
|
|
localEcho: nil,
|
|
success: success,
|
|
failure: failure,
|
|
keepActualFilename: false)
|
|
}
|
|
}
|