mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-31 23:47:01 +00:00
[WIP] Conference calls
This commit is contained in:
parent
d7ca478f24
commit
abdfc238f8
@ -49,6 +49,9 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
private var callContextState: OngoingCallContextState?
|
||||
private var ongoingContext: OngoingCallContext?
|
||||
private var ongoingContextStateDisposable: Disposable?
|
||||
private var ongoingContextIsFailedDisposable: Disposable?
|
||||
private var ongoingContextIsDroppedDisposable: Disposable?
|
||||
private var didDropCall = false
|
||||
private var sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
private var requestedVideoAspect: Float?
|
||||
private var reception: Int32?
|
||||
@ -136,6 +139,14 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
private var localVideoEndpointId: String?
|
||||
private var remoteVideoEndpointId: String?
|
||||
|
||||
private var conferenceSignalingDataDisposable: Disposable?
|
||||
private var conferenceIsConnected: Bool = false
|
||||
private var notifyConferenceIsConnectedTimer: Foundation.Timer?
|
||||
|
||||
private var remoteConferenceIsConnectedTimestamp: Double?
|
||||
private let remoteConferenceIsConnected = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private var remoteConferenceIsConnectedTimer: Foundation.Timer?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
audioSession: ManagedAudioSession,
|
||||
@ -296,7 +307,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] {
|
||||
self.sharedAudioDevice = nil
|
||||
} else {
|
||||
self.sharedAudioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: context.sharedContext.immediateExperimentalUISettings.experimentalCallMute)
|
||||
self.sharedAudioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false)
|
||||
}
|
||||
|
||||
self.audioSessionActiveDisposable = (self.audioSessionActive.get()
|
||||
@ -315,6 +326,18 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in
|
||||
}
|
||||
}
|
||||
|
||||
if self.isExpectedToBeConference {
|
||||
self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.processConferenceSignalingData(dataList: dataList)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -330,6 +353,12 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.screencastAudioDataDisposable.dispose()
|
||||
self.screencastStateDisposable.dispose()
|
||||
self.conferenceCallDisposable?.dispose()
|
||||
self.ongoingContextStateDisposable?.dispose()
|
||||
self.ongoingContextIsFailedDisposable?.dispose()
|
||||
self.ongoingContextIsDroppedDisposable?.dispose()
|
||||
self.notifyConferenceIsConnectedTimer?.invalidate()
|
||||
self.conferenceSignalingDataDisposable?.dispose()
|
||||
self.remoteConferenceIsConnectedTimer?.invalidate()
|
||||
|
||||
if let dropCallKitCallTimer = self.dropCallKitCallTimer {
|
||||
dropCallKitCallTimer.invalidate()
|
||||
@ -559,16 +588,14 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
switch sessionState.state {
|
||||
case .requesting:
|
||||
if let _ = audioSessionControl {
|
||||
if self.isExpectedToBeConference {
|
||||
} else {
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
}
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
}
|
||||
case let .active(id, key, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall):
|
||||
if let conferenceCall, self.conferenceCallDisposable == nil {
|
||||
presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel)
|
||||
|
||||
self.conferenceCallDisposable = (self.context.engine.calls.getCurrentGroupCall(callId: conferenceCall.id, accessHash: conferenceCall.accessHash)
|
||||
|> delay(sessionState.isOutgoing ? 0.0 : 2.0, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self, let result else {
|
||||
return
|
||||
@ -593,11 +620,14 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
invite: nil,
|
||||
joinAsPeerId: nil,
|
||||
isStream: false,
|
||||
encryptionKey: key
|
||||
encryptionKey: key,
|
||||
conferenceFromCallId: id,
|
||||
isConference: true,
|
||||
sharedAudioDevice: self.sharedAudioDevice
|
||||
)
|
||||
self.conferenceCall = conferenceCall
|
||||
|
||||
conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
|
||||
conferenceCall.setIsMuted(action: .muted(isPushToTalkActive: !self.isMutedValue))
|
||||
|
||||
let accountPeerId = conferenceCall.account.peerId
|
||||
let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = conferenceCall.members
|
||||
@ -624,24 +654,27 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
return lhs == rhs
|
||||
})
|
||||
|
||||
let remoteIsConnectedAggregated = combineLatest(queue: .mainQueue(),
|
||||
self.remoteConferenceIsConnected.get(),
|
||||
conferenceCall.hasActiveIncomingData
|
||||
)
|
||||
|> map { remoteConferenceIsConnected, hasActiveIncomingData -> Bool in
|
||||
return remoteConferenceIsConnected || hasActiveIncomingData
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
var startTimestamp: Double?
|
||||
self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(),
|
||||
conferenceCall.state,
|
||||
videoEndpoints
|
||||
videoEndpoints,
|
||||
conferenceCall.signalBars,
|
||||
conferenceCall.isFailed,
|
||||
remoteIsConnectedAggregated
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints in
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints, signalBars, isFailed, remoteIsConnectedAggregated in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let mappedState: PresentationCallState.State
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
mappedState = .connecting(nil)
|
||||
case .connected:
|
||||
let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent()
|
||||
startTimestamp = timestamp
|
||||
mappedState = .active(timestamp, nil, keyVisualHash)
|
||||
}
|
||||
|
||||
var mappedLocalVideoState: PresentationCallState.VideoState = .inactive
|
||||
var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive
|
||||
@ -664,13 +697,65 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
conferenceCall.setRequestedVideoList(items: requestedVideo)
|
||||
}
|
||||
|
||||
self.statePromise.set(PresentationCallState(
|
||||
state: mappedState,
|
||||
videoState: mappedLocalVideoState,
|
||||
remoteVideoState: mappedRemoteVideoState,
|
||||
remoteAudioState: .active,
|
||||
remoteBatteryLevel: .normal
|
||||
))
|
||||
var isConnected = false
|
||||
let mappedState: PresentationCallState.State
|
||||
if isFailed {
|
||||
mappedState = .terminating(.error(.disconnected))
|
||||
} else {
|
||||
switch callState.networkState {
|
||||
case .connecting:
|
||||
mappedState = .connecting(keyVisualHash)
|
||||
case .connected:
|
||||
isConnected = true
|
||||
if remoteIsConnectedAggregated {
|
||||
let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent()
|
||||
startTimestamp = timestamp
|
||||
mappedState = .active(timestamp, signalBars, keyVisualHash)
|
||||
} else {
|
||||
mappedState = .connecting(keyVisualHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateConferenceIsConnected(isConnected: isConnected)
|
||||
|
||||
if !self.didDropCall && !self.droppedCall {
|
||||
let presentationState = PresentationCallState(
|
||||
state: mappedState,
|
||||
videoState: mappedLocalVideoState,
|
||||
remoteVideoState: mappedRemoteVideoState,
|
||||
remoteAudioState: .active,
|
||||
remoteBatteryLevel: .normal
|
||||
)
|
||||
self.statePromise.set(presentationState)
|
||||
self.updateTone(presentationState, callContextState: nil, previous: nil)
|
||||
}
|
||||
})
|
||||
|
||||
self.ongoingContextIsFailedDisposable = (conferenceCall.isFailed
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.didDropCall {
|
||||
self.didDropCall = true
|
||||
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
|
||||
}
|
||||
})
|
||||
|
||||
self.ongoingContextIsDroppedDisposable = (conferenceCall.canBeRemoved
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.didDropCall {
|
||||
self.didDropCall = true
|
||||
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
|
||||
}
|
||||
})
|
||||
|
||||
var audioLevelId: UInt32?
|
||||
@ -707,12 +792,13 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.createConferenceIfPossible()
|
||||
}
|
||||
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
|
||||
if self.isExpectedToBeConference {
|
||||
if sessionState.isOutgoing {
|
||||
self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date())
|
||||
}
|
||||
} else {
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
||||
let logName = "\(id.id)_\(id.accessHash)"
|
||||
|
||||
@ -776,10 +862,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
}
|
||||
case let .terminated(_, _, options):
|
||||
if self.isExpectedToBeConference {
|
||||
} else {
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
}
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
if wasActive {
|
||||
let debugLogValue = Promise<String?>()
|
||||
self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue)
|
||||
@ -933,6 +1016,88 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateConferenceIsConnected(isConnected: Bool) {
|
||||
if self.conferenceIsConnected != isConnected {
|
||||
self.conferenceIsConnected = isConnected
|
||||
self.sendConferenceIsConnectedState()
|
||||
}
|
||||
|
||||
if self.notifyConferenceIsConnectedTimer == nil {
|
||||
self.notifyConferenceIsConnectedTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendConferenceIsConnectedState()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func sendConferenceIsConnectedState() {
|
||||
self.sendConferenceSignalingMessage(dict: ["_$": "s", "c": self.conferenceIsConnected])
|
||||
}
|
||||
|
||||
private func processConferenceSignalingData(dataList: [Data]) {
|
||||
for data in dataList {
|
||||
if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
self.processConferenceSignalingMessage(dict: dict)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processConferenceSignalingMessage(dict: [String: Any]) {
|
||||
if let type = dict["_$"] as? String {
|
||||
switch type {
|
||||
case "s":
|
||||
let isConnected = dict["c"] as? Bool ?? false
|
||||
self.remoteConferenceIsConnected.set(isConnected)
|
||||
|
||||
if isConnected {
|
||||
self.remoteConferenceIsConnectedTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
|
||||
if self.remoteConferenceIsConnectedTimer == nil && isConnected {
|
||||
self.remoteConferenceIsConnectedTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
if let remoteConferenceIsConnectedTimestamp = self.remoteConferenceIsConnectedTimestamp {
|
||||
if remoteConferenceIsConnectedTimestamp + 4.0 < timestamp {
|
||||
self.remoteConferenceIsConnected.set(false)
|
||||
}
|
||||
|
||||
if remoteConferenceIsConnectedTimestamp + 10.0 < timestamp {
|
||||
if !self.didDropCall {
|
||||
self.didDropCall = true
|
||||
|
||||
let presentationState = PresentationCallState(
|
||||
state: .terminating(.error(.disconnected)),
|
||||
videoState: .inactive,
|
||||
remoteVideoState: .inactive,
|
||||
remoteAudioState: .active,
|
||||
remoteBatteryLevel: .normal
|
||||
)
|
||||
self.statePromise.set(presentationState)
|
||||
self.updateTone(presentationState, callContextState: nil, previous: nil)
|
||||
|
||||
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendConferenceSignalingMessage(dict: [String: Any]) {
|
||||
if let data = try? JSONSerialization.data(withJSONObject: dict) {
|
||||
self.context.account.callSessionManager.sendSignalingData(internalId: self.internalId, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIsAudioSessionActive(_ value: Bool) {
|
||||
if self.isAudioSessionActive != value {
|
||||
self.isAudioSessionActive = value
|
||||
@ -1010,7 +1175,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.isMutedValue = value
|
||||
self.isMutedPromise.set(self.isMutedValue)
|
||||
self.ongoingContext?.setIsMuted(self.isMutedValue)
|
||||
self.conferenceCall?.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
|
||||
self.conferenceCall?.setIsMuted(action: .muted(isPushToTalkActive: !self.isMutedValue))
|
||||
}
|
||||
|
||||
public func requestVideo() {
|
||||
|
@ -703,7 +703,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
invite: nil,
|
||||
joinAsPeerId: nil,
|
||||
isStream: false,
|
||||
encryptionKey: nil
|
||||
encryptionKey: nil,
|
||||
conferenceFromCallId: nil,
|
||||
isConference: false,
|
||||
sharedAudioDevice: nil
|
||||
)
|
||||
call.schedule(timestamp: timestamp)
|
||||
|
||||
@ -743,7 +746,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
invite: nil,
|
||||
joinAsPeerId: nil,
|
||||
isStream: false,
|
||||
encryptionKey: nil
|
||||
encryptionKey: nil,
|
||||
conferenceFromCallId: nil,
|
||||
isConference: false,
|
||||
sharedAudioDevice: nil
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
@ -924,7 +930,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
invite: invite,
|
||||
joinAsPeerId: joinAsPeerId,
|
||||
isStream: initialCall.isStream ?? false,
|
||||
encryptionKey: nil
|
||||
encryptionKey: nil,
|
||||
conferenceFromCallId: nil,
|
||||
isConference: false,
|
||||
sharedAudioDevice: nil
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
|
@ -321,10 +321,10 @@ private extension CurrentImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
func stop(account: Account, reportCallId: CallId?) {
|
||||
switch self {
|
||||
case let .call(callContext):
|
||||
callContext.stop()
|
||||
callContext.stop(account: account, reportCallId: reportCallId)
|
||||
case .mediaStream, .externalMediaStream:
|
||||
break
|
||||
}
|
||||
@ -712,6 +712,30 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
private var myAudioLevelDisposable = MetaDisposable()
|
||||
|
||||
private var hasActiveIncomingDataValue: Bool = false {
|
||||
didSet {
|
||||
if self.hasActiveIncomingDataValue != oldValue {
|
||||
self.hasActiveIncomingDataPromise.set(self.hasActiveIncomingDataValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
private let hasActiveIncomingDataPromise = ValuePromise<Bool>(false)
|
||||
var hasActiveIncomingData: Signal<Bool, NoError> {
|
||||
return self.hasActiveIncomingDataPromise.get()
|
||||
}
|
||||
private var hasActiveIncomingDataDisposable: Disposable?
|
||||
private var hasActiveIncomingDataTimer: Foundation.Timer?
|
||||
|
||||
private let isFailedPromise = ValuePromise<Bool>(false)
|
||||
var isFailed: Signal<Bool, NoError> {
|
||||
return self.isFailedPromise.get()
|
||||
}
|
||||
|
||||
private let signalBarsPromise = Promise<Int32>(0)
|
||||
var signalBars: Signal<Int32, NoError> {
|
||||
return self.signalBarsPromise.get()
|
||||
}
|
||||
|
||||
private var audioSessionControl: ManagedAudioSessionControl?
|
||||
private var audioSessionDisposable: Disposable?
|
||||
private let audioSessionShouldBeActive = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
@ -842,6 +866,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
public let isStream: Bool
|
||||
private let encryptionKey: Data?
|
||||
private let sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
|
||||
private let conferenceFromCallId: CallId?
|
||||
private let isConference: Bool
|
||||
|
||||
public var onMutedSpeechActivityDetected: ((Bool) -> Void)?
|
||||
|
||||
@ -857,7 +885,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
invite: String?,
|
||||
joinAsPeerId: EnginePeer.Id?,
|
||||
isStream: Bool,
|
||||
encryptionKey: Data?
|
||||
encryptionKey: Data?,
|
||||
conferenceFromCallId: CallId?,
|
||||
isConference: Bool,
|
||||
sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
) {
|
||||
self.account = accountContext.account
|
||||
self.accountContext = accountContext
|
||||
@ -883,105 +914,110 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.hasVideo = false
|
||||
self.hasScreencast = false
|
||||
self.isStream = isStream
|
||||
self.conferenceFromCallId = conferenceFromCallId
|
||||
self.isConference = isConference
|
||||
self.encryptionKey = encryptionKey
|
||||
self.sharedAudioDevice = sharedAudioDevice
|
||||
|
||||
var didReceiveAudioOutputs = false
|
||||
|
||||
if !audioSession.getIsHeadsetPluggedIn() {
|
||||
self.currentSelectedAudioOutputValue = .speaker
|
||||
self.audioOutputStatePromise.set(.single(([], .speaker)))
|
||||
}
|
||||
|
||||
self.audioSessionDisposable = audioSession.push(audioSessionType: self.isStream ? .play(mixWithOthers: false) : .voiceCall, activateImmediately: true, manualActivate: { [weak self] control in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: control)
|
||||
}
|
||||
if self.sharedAudioDevice == nil {
|
||||
var didReceiveAudioOutputs = false
|
||||
|
||||
if !audioSession.getIsHeadsetPluggedIn() {
|
||||
self.currentSelectedAudioOutputValue = .speaker
|
||||
self.audioOutputStatePromise.set(.single(([], .speaker)))
|
||||
}
|
||||
}, deactivate: { [weak self] _ in
|
||||
return Signal { subscriber in
|
||||
|
||||
self.audioSessionDisposable = audioSession.push(audioSessionType: self.isStream ? .play(mixWithOthers: false) : .voiceCall, activateImmediately: true, manualActivate: { [weak self] control in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateIsAudioSessionActive(false)
|
||||
strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: nil)
|
||||
|
||||
if strongSelf.isStream {
|
||||
let _ = strongSelf.leave(terminateIfPossible: false)
|
||||
strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: control)
|
||||
}
|
||||
}
|
||||
}, deactivate: { [weak self] _ in
|
||||
return Signal { subscriber in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateIsAudioSessionActive(false)
|
||||
strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: nil)
|
||||
|
||||
if strongSelf.isStream {
|
||||
let _ = strongSelf.leave(terminateIfPossible: false)
|
||||
}
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.audioOutputStateValue = (availableOutputs, currentOutput)
|
||||
|
||||
var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput))
|
||||
if !didReceiveAudioOutputs {
|
||||
didReceiveAudioOutputs = true
|
||||
if currentOutput == .speaker {
|
||||
signal = .single((availableOutputs, .speaker))
|
||||
|> then(
|
||||
signal
|
||||
|> delay(1.0, queue: Queue.mainQueue())
|
||||
)
|
||||
}
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
strongSelf.audioOutputStatePromise.set(signal)
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in
|
||||
Queue.mainQueue().async {
|
||||
})
|
||||
|
||||
self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.audioOutputStateValue = (availableOutputs, currentOutput)
|
||||
|
||||
var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput))
|
||||
if !didReceiveAudioOutputs {
|
||||
didReceiveAudioOutputs = true
|
||||
if currentOutput == .speaker {
|
||||
signal = .single((availableOutputs, .speaker))
|
||||
|> then(
|
||||
signal
|
||||
|> delay(1.0, queue: Queue.mainQueue())
|
||||
)
|
||||
}
|
||||
}
|
||||
strongSelf.audioOutputStatePromise.set(signal)
|
||||
}
|
||||
})
|
||||
|
||||
self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
if let audioSessionControl = strongSelf.audioSessionControl {
|
||||
if !strongSelf.isStream, let callKitIntegration = strongSelf.callKitIntegration {
|
||||
_ = callKitIntegration.audioSessionActive
|
||||
|> filter { $0 }
|
||||
|> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { subscriber in
|
||||
subscriber.putNext(true)
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
})
|
||||
} else {
|
||||
audioSessionControl.activate({ _ in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
if value {
|
||||
if let audioSessionControl = strongSelf.audioSessionControl {
|
||||
if !strongSelf.isStream, let callKitIntegration = strongSelf.callKitIntegration {
|
||||
_ = callKitIntegration.audioSessionActive
|
||||
|> filter { $0 }
|
||||
|> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { subscriber in
|
||||
subscriber.putNext(true)
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
})
|
||||
} else {
|
||||
audioSessionControl.activate({ _ in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.audioSessionActive.set(.single(true))
|
||||
}
|
||||
strongSelf.audioSessionActive.set(.single(true))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
strongSelf.audioSessionActive.set(.single(false))
|
||||
}
|
||||
} else {
|
||||
strongSelf.audioSessionActive.set(.single(false))
|
||||
}
|
||||
} else {
|
||||
strongSelf.audioSessionActive.set(.single(false))
|
||||
}
|
||||
})
|
||||
|
||||
self.audioSessionActiveDisposable = (self.audioSessionActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateIsAudioSessionActive(value)
|
||||
}
|
||||
})
|
||||
|
||||
self.audioOutputStateDisposable = (self.audioOutputStatePromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] availableOutputs, currentOutput in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateAudioOutputs(availableOutputs: availableOutputs, currentOutput: currentOutput)
|
||||
})
|
||||
})
|
||||
|
||||
self.audioSessionActiveDisposable = (self.audioSessionActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateIsAudioSessionActive(value)
|
||||
}
|
||||
})
|
||||
|
||||
self.audioOutputStateDisposable = (self.audioOutputStatePromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] availableOutputs, currentOutput in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateAudioOutputs(availableOutputs: availableOutputs, currentOutput: currentOutput)
|
||||
})
|
||||
}
|
||||
|
||||
self.groupCallParticipantUpdatesDisposable = (self.account.stateManager.groupCallParticipantUpdates
|
||||
|> deliverOnMainQueue).start(next: { [weak self] updates in
|
||||
@ -1173,6 +1209,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.participantsContextStateDisposable.dispose()
|
||||
self.myAudioLevelDisposable.dispose()
|
||||
self.memberEventsPipeDisposable.dispose()
|
||||
self.hasActiveIncomingDataDisposable?.dispose()
|
||||
self.hasActiveIncomingDataTimer?.invalidate()
|
||||
|
||||
self.myAudioLevelTimer?.invalidate()
|
||||
self.typingDisposable.dispose()
|
||||
@ -1709,14 +1747,52 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.onMutedSpeechActivityDetected?(value)
|
||||
}
|
||||
}, encryptionKey: encryptionKey))
|
||||
}, encryptionKey: encryptionKey, isConference: self.isConference, sharedAudioDevice: self.sharedAudioDevice))
|
||||
}
|
||||
|
||||
self.genericCallContext = genericCallContext
|
||||
self.stateVersionValue += 1
|
||||
|
||||
let isEffectivelyMuted: Bool
|
||||
switch self.isMutedValue {
|
||||
case let .muted(isPushToTalkActive):
|
||||
isEffectivelyMuted = !isPushToTalkActive
|
||||
case .unmuted:
|
||||
isEffectivelyMuted = false
|
||||
}
|
||||
genericCallContext.setIsMuted(isEffectivelyMuted)
|
||||
|
||||
genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels)
|
||||
self.connectPendingVideoSubscribers()
|
||||
|
||||
if case let .call(callContext) = genericCallContext {
|
||||
var lastTimestamp: Double?
|
||||
self.hasActiveIncomingDataDisposable?.dispose()
|
||||
self.hasActiveIncomingDataDisposable = (callContext.ssrcActivities
|
||||
|> filter { !$0.isEmpty }
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
lastTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
self.hasActiveIncomingDataValue = true
|
||||
})
|
||||
|
||||
self.hasActiveIncomingDataTimer?.invalidate()
|
||||
self.hasActiveIncomingDataTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
if let lastTimestamp {
|
||||
if lastTimestamp + 1.0 < timestamp {
|
||||
self.hasActiveIncomingDataValue = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.signalBarsPromise.set(callContext.signalBars)
|
||||
}
|
||||
}
|
||||
|
||||
self.joinDisposable.set((genericCallContext.joinPayload
|
||||
@ -2570,10 +2646,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
self.markedAsCanBeRemoved = true
|
||||
|
||||
self.genericCallContext?.stop()
|
||||
self.genericCallContext?.stop(account: self.account, reportCallId: self.conferenceFromCallId)
|
||||
|
||||
//self.screencastIpcContext = nil
|
||||
self.screencastCallContext?.stop()
|
||||
self.screencastCallContext?.stop(account: self.account, reportCallId: nil)
|
||||
|
||||
self._canBeRemoved.set(.single(true))
|
||||
|
||||
@ -3024,7 +3100,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
self.hasScreencast = true
|
||||
|
||||
let screencastCallContext = OngoingGroupCallContext(audioSessionActive: .single(true), video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, enableSystemMute: false, preferX264: false, logPath: "", onMutedSpeechActivityDetected: { _ in }, encryptionKey: nil)
|
||||
let screencastCallContext = OngoingGroupCallContext(audioSessionActive: .single(true), video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, enableSystemMute: false, preferX264: false, logPath: "", onMutedSpeechActivityDetected: { _ in }, encryptionKey: nil, isConference: self.isConference, sharedAudioDevice: nil)
|
||||
self.screencastCallContext = screencastCallContext
|
||||
|
||||
self.screencastJoinDisposable.set((screencastCallContext.joinPayload
|
||||
@ -3059,7 +3135,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.hasScreencast = false
|
||||
if let screencastCallContext = self.screencastCallContext {
|
||||
self.screencastCallContext = nil
|
||||
screencastCallContext.stop()
|
||||
screencastCallContext.stop(account: self.account, reportCallId: nil)
|
||||
|
||||
let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo
|
||||
|
||||
@ -3133,6 +3209,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||
if self.sharedAudioDevice != nil {
|
||||
return
|
||||
}
|
||||
guard self.currentSelectedAudioOutputValue != output else {
|
||||
return
|
||||
}
|
||||
|
@ -789,12 +789,18 @@ private final class CallSessionManagerContext {
|
||||
return
|
||||
}
|
||||
|
||||
var idAndAccessHash: (id: Int64, accessHash: Int64)?
|
||||
switch context.state {
|
||||
case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, conferenceCall):
|
||||
if conferenceCall != nil {
|
||||
return
|
||||
}
|
||||
|
||||
idAndAccessHash = (id, accessHash)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let (id, accessHash) = idAndAccessHash {
|
||||
context.createConferenceCallDisposable = (createConferenceCall(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, callId: CallId(id: id, accessHash: accessHash))
|
||||
|> deliverOn(self.queue)).startStrict(next: { [weak self] result in
|
||||
guard let self else {
|
||||
@ -813,8 +819,6 @@ private final class CallSessionManagerContext {
|
||||
}
|
||||
}
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -447,7 +447,7 @@ public final class OngoingGroupCallContext {
|
||||
let queue: Queue
|
||||
let context: GroupCallThreadLocalContext
|
||||
#if os(iOS)
|
||||
let audioDevice: SharedCallAudioDevice?
|
||||
let audioDevice: OngoingCallContext.AudioDevice?
|
||||
#endif
|
||||
let sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max))
|
||||
|
||||
@ -456,6 +456,8 @@ public final class OngoingGroupCallContext {
|
||||
let isMuted = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||||
let isNoiseSuppressionEnabled = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||||
let audioLevels = ValuePipe<[(AudioLevelKey, Float, Bool)]>()
|
||||
let ssrcActivities = ValuePipe<[UInt32]>()
|
||||
let signalBars = ValuePromise<Int32>(0)
|
||||
|
||||
private var currentRequestedVideoChannels: [VideoChannel] = []
|
||||
|
||||
@ -463,6 +465,9 @@ public final class OngoingGroupCallContext {
|
||||
|
||||
private let audioSessionActiveDisposable = MetaDisposable()
|
||||
|
||||
private let logPath: String
|
||||
private let tempStatsLogFile: EngineTempBox.File
|
||||
|
||||
init(
|
||||
queue: Queue,
|
||||
inputDeviceId: String,
|
||||
@ -479,16 +484,28 @@ public final class OngoingGroupCallContext {
|
||||
preferX264: Bool,
|
||||
logPath: String,
|
||||
onMutedSpeechActivityDetected: @escaping (Bool) -> Void,
|
||||
encryptionKey: Data?
|
||||
encryptionKey: Data?,
|
||||
isConference: Bool,
|
||||
sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
) {
|
||||
self.queue = queue
|
||||
|
||||
self.logPath = logPath
|
||||
|
||||
self.tempStatsLogFile = EngineTempBox.shared.tempFile(fileName: "CallStats.json")
|
||||
let tempStatsLogPath = self.tempStatsLogFile.path
|
||||
|
||||
#if os(iOS)
|
||||
self.audioDevice = nil
|
||||
if sharedAudioDevice == nil {
|
||||
self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false)
|
||||
} else {
|
||||
self.audioDevice = sharedAudioDevice
|
||||
}
|
||||
let audioDevice = self.audioDevice
|
||||
#endif
|
||||
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
|
||||
var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)?
|
||||
var activityUpdatedImpl: (([UInt32]) -> Void)?
|
||||
|
||||
let _videoContentType: OngoingGroupCallVideoContentType
|
||||
switch videoContentType {
|
||||
@ -510,6 +527,9 @@ public final class OngoingGroupCallContext {
|
||||
audioLevelsUpdated: { levels in
|
||||
audioLevelsUpdatedImpl?(levels)
|
||||
},
|
||||
activityUpdated: { ssrcs in
|
||||
activityUpdatedImpl?(ssrcs.map { $0.uint32Value })
|
||||
},
|
||||
inputDeviceId: inputDeviceId,
|
||||
outputDeviceId: outputDeviceId,
|
||||
videoCapturer: video?.impl,
|
||||
@ -594,11 +614,13 @@ public final class OngoingGroupCallContext {
|
||||
enableSystemMute: enableSystemMute,
|
||||
preferX264: preferX264,
|
||||
logPath: logPath,
|
||||
statsLogPath: tempStatsLogPath,
|
||||
onMutedSpeechActivityDetected: { value in
|
||||
onMutedSpeechActivityDetected(value)
|
||||
},
|
||||
audioDevice: audioDevice,
|
||||
encryptionKey: encryptionKey
|
||||
audioDevice: audioDevice?.impl,
|
||||
encryptionKey: encryptionKey,
|
||||
isConference: isConference
|
||||
)
|
||||
#else
|
||||
self.context = GroupCallThreadLocalContext(
|
||||
@ -609,6 +631,9 @@ public final class OngoingGroupCallContext {
|
||||
audioLevelsUpdated: { levels in
|
||||
audioLevelsUpdatedImpl?(levels)
|
||||
},
|
||||
activityUpdated: { ssrcs in
|
||||
activityUpdatedImpl?(ssrcs.map { $0.uint32Value })
|
||||
},
|
||||
inputDeviceId: inputDeviceId,
|
||||
outputDeviceId: outputDeviceId,
|
||||
videoCapturer: video?.impl,
|
||||
@ -692,7 +717,9 @@ public final class OngoingGroupCallContext {
|
||||
disableAudioInput: disableAudioInput,
|
||||
preferX264: preferX264,
|
||||
logPath: logPath,
|
||||
encryptionKey: encryptionKey
|
||||
statsLogPath: tempStatsLogPath,
|
||||
encryptionKey: encryptionKey,
|
||||
isConference: isConference
|
||||
)
|
||||
#endif
|
||||
|
||||
@ -732,6 +759,20 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
let ssrcActivities = self.ssrcActivities
|
||||
activityUpdatedImpl = { ssrcs in
|
||||
queue.async {
|
||||
ssrcActivities.putNext(ssrcs)
|
||||
}
|
||||
}
|
||||
|
||||
let signalBars = self.signalBars
|
||||
self.context.signalBarsChanged = { value in
|
||||
queue.async {
|
||||
signalBars.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
self.context.emitJoinPayload({ [weak self] payload, ssrc in
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
@ -741,16 +782,18 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
})
|
||||
|
||||
self.audioSessionActiveDisposable.set((audioSessionActive
|
||||
|> deliverOn(queue)).start(next: { [weak self] isActive in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
// self.audioDevice?.setManualAudioSessionIsActive(isActive)
|
||||
#if os(iOS)
|
||||
self.context.setManualAudioSessionIsActive(isActive)
|
||||
#endif
|
||||
}))
|
||||
if sharedAudioDevice == nil {
|
||||
self.audioSessionActiveDisposable.set((audioSessionActive
|
||||
|> deliverOn(queue)).start(next: { [weak self] isActive in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
// self.audioDevice?.setManualAudioSessionIsActive(isActive)
|
||||
#if os(iOS)
|
||||
self.context.setManualAudioSessionIsActive(isActive)
|
||||
#endif
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -826,8 +869,40 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
func stop(account: Account, reportCallId: CallId?) {
|
||||
self.context.stop()
|
||||
|
||||
let logPath = self.logPath
|
||||
var statsLogPath = ""
|
||||
if !logPath.isEmpty {
|
||||
statsLogPath = logPath + ".json"
|
||||
}
|
||||
let tempStatsLogPath = self.tempStatsLogFile.path
|
||||
|
||||
let queue = self.queue
|
||||
self.context.stop({
|
||||
queue.async {
|
||||
if !statsLogPath.isEmpty {
|
||||
let logsPath = callLogsPath(account: account)
|
||||
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
||||
let _ = try? FileManager.default.moveItem(atPath: tempStatsLogPath, toPath: statsLogPath)
|
||||
}
|
||||
|
||||
if let callId = reportCallId, !statsLogPath.isEmpty, let data = try? Data(contentsOf: URL(fileURLWithPath: statsLogPath)), let dataString = String(data: data, encoding: .utf8) {
|
||||
let engine = TelegramEngine(account: account)
|
||||
let _ = engine.calls.saveCallDebugLog(callId: callId, log: dataString).start(next: { result in
|
||||
switch result {
|
||||
case .sendFullLog:
|
||||
if !logPath.isEmpty {
|
||||
let _ = engine.calls.saveCompleteCallDebugLog(callId: callId, logPath: logPath).start()
|
||||
}
|
||||
case .done:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool, isUnifiedBroadcast: Bool) {
|
||||
@ -988,6 +1063,30 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
public var ssrcActivities: Signal<[UInt32], NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.ssrcActivities.signal().start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public var signalBars: Signal<Int32, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.signalBars.get().start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public var isMuted: Signal<Bool, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
@ -1012,10 +1111,10 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal<Bool, NoError>, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, enableSystemMute: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void, encryptionKey: Data?) {
|
||||
public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal<Bool, NoError>, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, enableSystemMute: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void, encryptionKey: Data?, isConference: Bool, sharedAudioDevice: OngoingCallContext.AudioDevice?) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, enableSystemMute: enableSystemMute, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected, encryptionKey: encryptionKey)
|
||||
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, enableSystemMute: enableSystemMute, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected, encryptionKey: encryptionKey, isConference: isConference, sharedAudioDevice: sharedAudioDevice)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1103,9 +1202,9 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
public func stop(account: Account, reportCallId: CallId?) {
|
||||
self.impl.with { impl in
|
||||
impl.stop()
|
||||
impl.stop(account: account, reportCallId: reportCallId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -794,6 +794,11 @@ public final class OngoingCallContext {
|
||||
return self.audioLevelPromise.get()
|
||||
}
|
||||
|
||||
private let signalingDataPipe = ValuePipe<[Data]>()
|
||||
public var signalingData: Signal<[Data], NoError> {
|
||||
return self.signalingDataPipe.signal()
|
||||
}
|
||||
|
||||
private let audioSessionDisposable = MetaDisposable()
|
||||
private let audioSessionActiveDisposable = MetaDisposable()
|
||||
private var networkTypeDisposable: Disposable?
|
||||
@ -1122,7 +1127,13 @@ public final class OngoingCallContext {
|
||||
|
||||
strongSelf.signalingDataDisposable = callSessionManager.beginReceivingCallSignalingData(internalId: internalId, { [weak self] dataList in
|
||||
queue.async {
|
||||
self?.withContext { context in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.signalingDataPipe.putNext(dataList)
|
||||
|
||||
self.withContext { context in
|
||||
if let context = context as? OngoingCallThreadLocalContextWebrtc {
|
||||
for data in dataList {
|
||||
context.addSignaling(data)
|
||||
@ -1301,6 +1312,21 @@ public final class OngoingCallContext {
|
||||
context.addExternalAudioData(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
public func sendSignalingData(data: Data) {
|
||||
self.queue.async { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let signalingConnectionManager = strongSelf.signalingConnectionManager {
|
||||
signalingConnectionManager.with { impl in
|
||||
impl.send(payloadData: data)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.callSessionManager.sendSignalingData(internalId: strongSelf.internalId, data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private protocol CallSignalingConnection: AnyObject {
|
||||
|
@ -398,9 +398,12 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) {
|
||||
|
||||
@interface GroupCallThreadLocalContext : NSObject
|
||||
|
||||
@property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t);
|
||||
|
||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue
|
||||
networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated
|
||||
audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated
|
||||
activityUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))activityUpdated
|
||||
inputDeviceId:(NSString * _Nonnull)inputDeviceId
|
||||
outputDeviceId:(NSString * _Nonnull)outputDeviceId
|
||||
videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer
|
||||
@ -415,11 +418,13 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) {
|
||||
enableSystemMute:(bool)enableSystemMute
|
||||
preferX264:(bool)preferX264
|
||||
logPath:(NSString * _Nonnull)logPath
|
||||
statsLogPath:(NSString * _Nonnull)statsLogPath
|
||||
onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDetected
|
||||
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice
|
||||
encryptionKey:(NSData * _Nullable)encryptionKey;
|
||||
encryptionKey:(NSData * _Nullable)encryptionKey
|
||||
isConference:(bool)isConference;
|
||||
|
||||
- (void)stop;
|
||||
- (void)stop:(void (^ _Nullable)())completion;
|
||||
|
||||
- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive;
|
||||
|
||||
|
@ -1671,6 +1671,8 @@ private:
|
||||
SharedCallAudioDevice * _audioDevice;
|
||||
|
||||
void (^_onMutedSpeechActivityDetected)(bool);
|
||||
|
||||
int32_t _signalBars;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1680,6 +1682,7 @@ private:
|
||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue
|
||||
networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated
|
||||
audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated
|
||||
activityUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))activityUpdated
|
||||
inputDeviceId:(NSString * _Nonnull)inputDeviceId
|
||||
outputDeviceId:(NSString * _Nonnull)outputDeviceId
|
||||
videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer
|
||||
@ -1694,9 +1697,11 @@ private:
|
||||
enableSystemMute:(bool)enableSystemMute
|
||||
preferX264:(bool)preferX264
|
||||
logPath:(NSString * _Nonnull)logPath
|
||||
statsLogPath:(NSString * _Nonnull)statsLogPath
|
||||
onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDetected
|
||||
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice
|
||||
encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
encryptionKey:(NSData * _Nullable)encryptionKey
|
||||
isConference:(bool)isConference {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = queue;
|
||||
@ -1762,6 +1767,8 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
config.need_log = true;
|
||||
config.logPath.data = std::string(logPath.length == 0 ? "" : logPath.UTF8String);
|
||||
|
||||
std::string statsLogPathValue(statsLogPath.length == 0 ? "" : statsLogPath.UTF8String);
|
||||
|
||||
std::optional<tgcalls::EncryptionKey> mappedEncryptionKey;
|
||||
if (encryptionKey) {
|
||||
auto encryptionKeyValue = std::make_shared<std::array<uint8_t, 256>>();
|
||||
@ -1774,6 +1781,7 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
_instance.reset(new tgcalls::GroupInstanceCustomImpl((tgcalls::GroupInstanceDescriptor){
|
||||
.threads = tgcalls::StaticThreads::getThreads(),
|
||||
.config = config,
|
||||
.statsLogPath = statsLogPathValue,
|
||||
.networkStateUpdated = [weakSelf, queue, networkStateUpdated](tgcalls::GroupNetworkState networkState) {
|
||||
[queue dispatch:^{
|
||||
__strong GroupCallThreadLocalContext *strongSelf = weakSelf;
|
||||
@ -1786,6 +1794,17 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
networkStateUpdated(mappedState);
|
||||
}];
|
||||
},
|
||||
.signalBarsUpdated = [weakSelf, queue](int value) {
|
||||
[queue dispatch:^{
|
||||
__strong GroupCallThreadLocalContext *strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
strongSelf->_signalBars = value;
|
||||
if (strongSelf->_signalBarsChanged) {
|
||||
strongSelf->_signalBarsChanged(value);
|
||||
}
|
||||
}
|
||||
}];
|
||||
},
|
||||
.audioLevelsUpdated = [audioLevelsUpdated](tgcalls::GroupLevelsUpdate const &levels) {
|
||||
NSMutableArray *result = [[NSMutableArray alloc] init];
|
||||
for (auto &it : levels.updates) {
|
||||
@ -1799,6 +1818,13 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
}
|
||||
audioLevelsUpdated(result);
|
||||
},
|
||||
.ssrcActivityUpdated = [activityUpdated](tgcalls::GroupActivitiesUpdate const &update) {
|
||||
NSMutableArray *result = [[NSMutableArray alloc] init];
|
||||
for (auto &it : update.updates) {
|
||||
[result addObject:@(it.ssrc)];
|
||||
}
|
||||
activityUpdated(result);
|
||||
},
|
||||
.initialInputDeviceId = inputDeviceId.UTF8String,
|
||||
.initialOutputDeviceId = outputDeviceId.UTF8String,
|
||||
.videoCapture = [_videoCapturer getInterface],
|
||||
@ -1968,7 +1994,8 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
}
|
||||
}];
|
||||
},
|
||||
.encryptionKey = mappedEncryptionKey
|
||||
.encryptionKey = mappedEncryptionKey,
|
||||
.isConference = isConference
|
||||
}));
|
||||
}
|
||||
return self;
|
||||
@ -1984,7 +2011,7 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
- (void)stop:(void (^ _Nullable)())completion {
|
||||
if (_currentAudioDeviceModuleThread) {
|
||||
auto currentAudioDeviceModule = _currentAudioDeviceModule;
|
||||
_currentAudioDeviceModule = nullptr;
|
||||
@ -1994,8 +2021,17 @@ encryptionKey:(NSData * _Nullable)encryptionKey {
|
||||
}
|
||||
|
||||
if (_instance) {
|
||||
_instance->stop();
|
||||
void (^capturedCompletion)() = [completion copy];
|
||||
_instance->stop([capturedCompletion] {
|
||||
if (capturedCompletion) {
|
||||
capturedCompletion();
|
||||
}
|
||||
});
|
||||
_instance.reset();
|
||||
} else {
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 518e1ed9dff6b897fc3cd07394edc9e2987e0fdb
|
||||
Subproject commit 965c46f32425cb270e88ab0aab7c3593b5be574e
|
2
third-party/webrtc/webrtc
vendored
2
third-party/webrtc/webrtc
vendored
@ -1 +1 @@
|
||||
Subproject commit cff7487b9c9a856678d645879d363e55812f3039
|
||||
Subproject commit 77d3d1fe2ff2f364e8edee58179a7b7b95239b01
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "11.5.2",
|
||||
"app": "11.5.3",
|
||||
"xcode": "16.0",
|
||||
"bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff",
|
||||
"macos": "15.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user