[WIP] Conference calls

This commit is contained in:
Isaac 2025-02-11 18:46:59 +04:00
parent 0bb33c215a
commit 1f517e187a
15 changed files with 341 additions and 135 deletions

View File

@ -401,6 +401,22 @@ public extension GroupCallParticipantsContext.Participant {
}
}
public struct PresentationGroupCallInvitedPeer: Equatable {
public enum State {
case requesting
case ringing
case connecting
}
public var id: EnginePeer.Id
public var state: State?
public init(id: EnginePeer.Id, state: State?) {
self.id = id
self.state = state
}
}
public protocol PresentationGroupCall: AnyObject {
var account: Account { get }
var accountContext: AccountContext { get }
@ -468,7 +484,7 @@ public protocol PresentationGroupCall: AnyObject {
func invitePeer(_ peerId: EnginePeer.Id) -> Bool
func removedPeer(_ peerId: EnginePeer.Id)
var invitedPeers: Signal<[EnginePeer.Id], NoError> { get }
var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get }
var inviteLinks: Signal<GroupCallInviteLinks?, NoError> { get }

View File

@ -470,7 +470,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
case .busy:
mappedReason = .busy
case .hungUp:
case .hungUp, .switchedToConference:
if self.callStartTimestamp != nil {
mappedReason = .hangUp
} else {
@ -687,12 +687,12 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
func animateOutToGroupChat(completion: @escaping () -> Void) -> CallController.AnimateOutToGroupChat {
self.callScreen.animateOutToGroupChat(completion: completion)
let takenIncomingVideoLayer = self.callScreen.takeIncomingVideoLayer()
let takeSource = self.callScreen.takeIncomingVideoLayer()
return CallController.AnimateOutToGroupChat(
containerView: self.containerView,
incomingPeerId: self.call.peerId,
incomingVideoLayer: takenIncomingVideoLayer?.0,
incomingVideoPlaceholder: takenIncomingVideoLayer?.1
incomingPeerId: (takeSource?.1 ?? true) ? self.call.peerId : self.call.context.account.peerId,
incomingVideoLayer: takeSource?.0.0,
incomingVideoPlaceholder: takeSource?.0.1
)
}

View File

@ -225,7 +225,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
switch type {
case .busy:
statusValue = .text(self.presentationData.strings.Call_StatusBusy)
case .hungUp, .missed:
case .hungUp, .missed, .switchedToConference:
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
}
case .error:

View File

@ -909,7 +909,7 @@ public final class PresentationCallImpl: PresentationCall {
self.conferenceCallImpl = conferenceCall
conferenceCall.upgradedConferenceCall = self
conferenceCall.setInvitedPeers(self.pendingInviteToConferencePeerIds)
conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds)
for peerId in self.pendingInviteToConferencePeerIds {
let _ = conferenceCall.invitePeer(peerId)
}
@ -990,8 +990,6 @@ public final class PresentationCallImpl: PresentationCall {
return
}
self.ongoingContext?.stop(debugLogValue: Promise())
self.ongoingContext = nil
self.ongoingContextStateDisposable?.dispose()
self.conferenceStateValue = .ready
@ -1197,6 +1195,8 @@ public final class PresentationCallImpl: PresentationCall {
tone = .busy
case .hungUp, .missed:
tone = .ended
case .switchedToConference:
tone = nil
}
case .error:
tone = .failed

View File

@ -731,7 +731,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
guard let groupCall = conferenceSource.conferenceCall else {
return
}
(conferenceSource as! PresentationCallImpl).resetAsMovedToConference()
(groupCall as! PresentationGroupCallImpl).moveConferenceCall(source: conferenceSource)
self.updateCurrentGroupCall(.group(groupCall))
})

View File

@ -601,6 +601,10 @@ private final class ScreencastEmbeddedIPCContext: ScreencastIPCContext {
}
private final class PendingConferenceInvitationContext {
enum State {
case ringing
}
private let callSessionManager: CallSessionManager
private var requestDisposable: Disposable?
private var stateDisposable: Disposable?
@ -608,7 +612,7 @@ private final class PendingConferenceInvitationContext {
private var didNotifyEnded: Bool = false
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, encryptionKey: Data, peerId: PeerId, onEnded: @escaping () -> Void) {
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, encryptionKey: Data, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
self.callSessionManager = callSessionManager
self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey))
@ -624,10 +628,14 @@ private final class PendingConferenceInvitationContext {
return
}
switch state.state {
case .dropping, .terminated:
case let .requesting(ringing, _):
if ringing {
onStateUpdated(.ringing)
}
case let .dropping(reason), let .terminated(_, reason, _):
if !self.didNotifyEnded {
self.didNotifyEnded = true
onEnded()
onEnded(reason == .ended(.switchedToConference))
}
default:
break
@ -972,15 +980,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return self.membersPromise.get()
}
private var invitedPeersValue: [EnginePeer.Id] = [] {
private var invitedPeersValue: [PresentationGroupCallInvitedPeer] = [] {
didSet {
if self.invitedPeersValue != oldValue {
self.inivitedPeersPromise.set(self.invitedPeersValue)
}
}
}
private let inivitedPeersPromise = ValuePromise<[EnginePeer.Id]>([])
public var invitedPeers: Signal<[EnginePeer.Id], NoError> {
private let inivitedPeersPromise = ValuePromise<[PresentationGroupCallInvitedPeer]>([])
public var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> {
return self.inivitedPeersPromise.get()
}
@ -1062,14 +1070,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return self.conferenceSourceId
}
var internal_isRemoteConnected = Promise<Bool>()
private var internal_isRemoteConnectedDisposable: Disposable?
public var onMutedSpeechActivityDetected: ((Bool) -> Void)?
let debugLog = Promise<String?>()
public weak var upgradedConferenceCall: PresentationCallImpl?
public var pendingDisconnedUpgradedConferenceCall: PresentationCallImpl?
private var pendingDisconnedUpgradedConferenceCallTimer: Foundation.Timer?
private var conferenceInvitationContexts: [PeerId: PendingConferenceInvitationContext] = [:]
init(
@ -1428,14 +1435,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
self.audioOutputStateDisposable?.dispose()
self.removedChannelMembersDisposable?.dispose()
self.peerUpdatesSubscription?.dispose()
self.screencastStateDisposable?.dispose()
self.internal_isRemoteConnectedDisposable?.dispose()
self.pendingDisconnedUpgradedConferenceCallTimer?.invalidate()
}
private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) {
@ -1529,7 +1532,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
topParticipants.append(participant)
}
if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) {
if let index = updatedInvitedPeers.firstIndex(where: { $0.id == participant.peer.id }) {
updatedInvitedPeers.remove(at: index)
didUpdateInvitedPeers = true
}
@ -1986,6 +1989,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
lastTimestamp = CFAbsoluteTimeGetCurrent()
self.hasActiveIncomingDataValue = true
self.activateIncomingAudioIfNeeded()
})
self.hasActiveIncomingDataTimer?.invalidate()
@ -2002,15 +2007,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})
self.signalBarsPromise.set(callContext.signalBars)
self.internal_isRemoteConnectedDisposable = (self.internal_isRemoteConnected.get()
|> distinctUntilChanged
|> deliverOnMainQueue).startStrict(next: { [weak callContext] isRemoteConnected in
guard let callContext else {
return
}
callContext.addRemoteConnectedEvent(isRemoteConntected: isRemoteConnected)
})
}
}
@ -2645,7 +2641,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) {
if let index = updatedInvitedPeers.firstIndex(where: { $0.id == participant.peer.id }) {
updatedInvitedPeers.remove(at: index)
didUpdateInvitedPeers = true
}
@ -2741,6 +2737,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
private func activateIncomingAudioIfNeeded() {
if let genericCallContext = self.genericCallContext, case let .call(groupCall) = genericCallContext {
groupCall.activateIncomingAudio()
}
}
private func requestMediaChannelDescriptions(ssrcs: Set<UInt32>, completion: @escaping ([OngoingGroupCallContext.MediaChannelDescription]) -> Void) -> Disposable {
func extractMediaChannelDescriptions(remainingSsrcs: inout Set<UInt32>, participants: [GroupCallParticipantsContext.Participant], into result: inout [OngoingGroupCallContext.MediaChannelDescription]) {
for participant in participants {
@ -3643,42 +3645,68 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if conferenceInvitationContexts[peerId] != nil {
return false
}
var onEnded: (() -> Void)?
var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)?
var onEnded: ((Bool) -> Void)?
var didEndAlready = false
let invitationContext = PendingConferenceInvitationContext(
callSessionManager: self.accountContext.account.callSessionManager,
groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash),
encryptionKey: encryptionKey.key,
peerId: peerId,
onEnded: {
onStateUpdated: { state in
onStateUpdated?(state)
},
onEnded: { success in
didEndAlready = true
onEnded?()
onEnded?(success)
}
)
if !didEndAlready {
conferenceInvitationContexts[peerId] = invitationContext
if !self.invitedPeersValue.contains(peerId) {
self.invitedPeersValue.append(peerId)
if !self.invitedPeersValue.contains(where: { $0.id == peerId }) {
self.invitedPeersValue.append(PresentationGroupCallInvitedPeer(id: peerId, state: .requesting))
}
onEnded = { [weak self, weak invitationContext] in
onStateUpdated = { [weak self] state in
guard let self else {
return
}
if let index = self.invitedPeersValue.firstIndex(where: { $0.id == peerId }) {
var invitedPeer = self.invitedPeersValue[index]
switch state {
case .ringing:
invitedPeer.state = .ringing
}
self.invitedPeersValue[index] = invitedPeer
}
}
onEnded = { [weak self, weak invitationContext] success in
guard let self, let invitationContext else {
return
}
if self.conferenceInvitationContexts[peerId] === invitationContext {
self.conferenceInvitationContexts.removeValue(forKey: peerId)
self.invitedPeersValue.removeAll(where: { $0 == peerId })
if success {
if let index = self.invitedPeersValue.firstIndex(where: { $0.id == peerId }) {
var invitedPeer = self.invitedPeersValue[index]
invitedPeer.state = .connecting
self.invitedPeersValue[index] = invitedPeer
}
} else {
self.invitedPeersValue.removeAll(where: { $0.id == peerId })
}
}
}
}
return false
} else {
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else {
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else {
return false
}
var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.insert(peerId, at: 0)
updatedInvitedPeers.insert(PresentationGroupCallInvitedPeer(id: peerId, state: nil), at: 0)
self.invitedPeersValue = updatedInvitedPeers
let _ = self.accountContext.engine.calls.inviteToGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()
@ -3687,13 +3715,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
func setInvitedPeers(_ peerIds: [PeerId]) {
self.invitedPeersValue = peerIds
func setConferenceInvitedPeers(_ peerIds: [PeerId]) {
self.invitedPeersValue = peerIds.map {
PresentationGroupCallInvitedPeer(id: $0, state: .requesting)
}
}
public func removedPeer(_ peerId: PeerId) {
var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.removeAll(where: { $0 == peerId})
updatedInvitedPeers.removeAll(where: { $0.id == peerId})
self.invitedPeersValue = updatedInvitedPeers
}
@ -3859,6 +3889,26 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
|> runOn(.mainQueue())
}
func moveConferenceCall(source: PresentationCall) {
guard let source = source as? PresentationCallImpl else {
return
}
self.pendingDisconnedUpgradedConferenceCall?.resetAsMovedToConference()
self.pendingDisconnedUpgradedConferenceCall = source
self.pendingDisconnedUpgradedConferenceCallTimer?.invalidate()
self.pendingDisconnedUpgradedConferenceCallTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
if let pendingDisconnedUpgradedConferenceCall = self.pendingDisconnedUpgradedConferenceCall {
self.pendingDisconnedUpgradedConferenceCall = nil
pendingDisconnedUpgradedConferenceCall.resetAsMovedToConference()
}
})
}
}
private protocol ScreencastContext: AnyObject {

View File

@ -428,6 +428,15 @@ final class VideoChatParticipantVideoComponent: Component {
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
}
var previousVideoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
if let previousComponent {
if previousComponent.isMyPeer && previousComponent.isPresentation {
previousVideoDescription = nil
} else {
previousVideoDescription = previousComponent.maxVideoQuality == 0 ? nil : (previousComponent.isPresentation ? previousComponent.participant.presentationDescription : previousComponent.participant.videoDescription)
}
}
let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
if component.isMyPeer && component.isPresentation {
videoDescription = nil
@ -506,8 +515,13 @@ final class VideoChatParticipantVideoComponent: Component {
}
let videoLayer: PrivateCallVideoLayer
var resetVideoSource = false
if let current = self.videoLayer {
videoLayer = current
if let previousVideoDescription, previousVideoDescription.endpointId != videoDescription.endpointId {
resetVideoSource = true
}
} else {
videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer
@ -517,6 +531,10 @@ final class VideoChatParticipantVideoComponent: Component {
videoLayer.blurredLayer.opacity = 0.0
resetVideoSource = true
}
if resetVideoSource {
if let input = component.call.video(endpointId: videoDescription.endpointId) {
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
self.videoSource = videoSource

View File

@ -128,7 +128,7 @@ final class VideoChatParticipantsComponent: Component {
let call: VideoChatCall
let participants: Participants?
let invitedPeers: [EnginePeer]
let invitedPeers: [VideoChatScreenComponent.InvitedPeer]
let speakingParticipants: Set<EnginePeer.Id>
let expandedVideoState: ExpandedVideoState?
let maxVideoQuality: Int
@ -148,7 +148,7 @@ final class VideoChatParticipantsComponent: Component {
init(
call: VideoChatCall,
participants: Participants?,
invitedPeers: [EnginePeer],
invitedPeers: [VideoChatScreenComponent.InvitedPeer],
speakingParticipants: Set<EnginePeer.Id>,
expandedVideoState: ExpandedVideoState?,
maxVideoQuality: Int,
@ -1259,10 +1259,20 @@ final class VideoChatParticipantsComponent: Component {
)
} else {
let invitedPeer = component.invitedPeers[i - self.listParticipants.count]
participantPeerId = invitedPeer.id
participantPeerId = invitedPeer.peer.id
let subtitle: PeerListItemComponent.Subtitle
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusInvited, color: .neutral)
//TODO:localize
switch invitedPeer.state {
case .none:
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusInvited, color: .neutral)
case .connecting:
subtitle = PeerListItemComponent.Subtitle(text: "connecting...", color: .neutral)
case .requesting:
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
case .ringing:
subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral)
}
peerItemComponent = PeerListItemComponent(
context: component.call.accountContext,
@ -1270,15 +1280,15 @@ final class VideoChatParticipantsComponent: Component {
strings: component.strings,
style: .generic,
sideInset: 0.0,
title: invitedPeer.displayTitle(strings: component.strings, displayOrder: .firstLast),
title: invitedPeer.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent(
call: component.call,
peer: invitedPeer,
peer: invitedPeer.peer,
myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId,
isSpeaking: false,
theme: component.theme
)),
peer: invitedPeer,
peer: invitedPeer.peer,
subtitle: subtitle,
subtitleAccessory: .none,
presence: nil,

View File

@ -190,6 +190,16 @@ final class VideoChatScreenComponent: Component {
self.scrollView = scrollView
}
}
struct InvitedPeer: Equatable {
var peer: EnginePeer
var state: PresentationGroupCallInvitedPeer.State?
init(peer: EnginePeer, state: PresentationGroupCallInvitedPeer.State?) {
self.peer = peer
self.state = state
}
}
final class View: UIView, UIGestureRecognizerDelegate {
let containerView: UIView
@ -242,7 +252,7 @@ final class VideoChatScreenComponent: Component {
var members: PresentationGroupCallMembers?
var membersDisposable: Disposable?
var invitedPeers: [EnginePeer] = []
var invitedPeers: [InvitedPeer] = []
var invitedPeersDisposable: Disposable?
var speakingParticipantPeers: [EnginePeer] = []
@ -323,8 +333,13 @@ final class VideoChatScreenComponent: Component {
}
var expandedPeer: (id: EnginePeer.Id, isPresentation: Bool)?
if let animateOutData, animateOutData.incomingVideoLayer != nil {
if let members = self.members, let participant = members.participants.first(where: { $0.peer.id == animateOutData.incomingPeerId }) {
if let animateOutData, animateOutData.incomingVideoLayer != nil, let members = self.members {
if let participant = members.participants.first(where: { $0.peer.id == animateOutData.incomingPeerId }) {
if let _ = participant.videoDescription {
expandedPeer = (participant.peer.id, false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true)
}
} else if let participant = members.participants.first(where: { $0.peer.id == sourceCallController.call.context.account.peerId }) {
if let _ = participant.videoDescription {
expandedPeer = (participant.peer.id, false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true)
@ -969,7 +984,7 @@ final class VideoChatScreenComponent: Component {
}
}
static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [EnginePeer]), NoError> {
static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> {
let invitedPeers = conferenceSource.context.engine.data.subscribe(
EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) })
)
@ -982,7 +997,7 @@ final class VideoChatScreenComponent: Component {
conferenceSource.isMuted,
invitedPeers
)
|> mapToSignal { state, isMuted, invitedPeers -> Signal<(state: PresentationGroupCallState, invitedPeers: [EnginePeer]), NoError> in
|> mapToSignal { state, isMuted, invitedPeers -> Signal<(state: PresentationGroupCallState, invitedPeers: [VideoChatScreenComponent.InvitedPeer]), NoError> in
let mappedNetworkState: PresentationGroupCallState.NetworkState
switch state.state {
case .active:
@ -1007,7 +1022,12 @@ final class VideoChatScreenComponent: Component {
isVideoWatchersLimitReached: false
)
return .single((callState, invitedPeers.compactMap({ $0 })))
return .single((callState, invitedPeers.compactMap({ peer -> VideoChatScreenComponent.InvitedPeer? in
guard let peer else {
return nil
}
return VideoChatScreenComponent.InvitedPeer(peer: peer, state: .requesting)
})))
}
}
@ -1023,10 +1043,18 @@ final class VideoChatScreenComponent: Component {
var participants: [GroupCallParticipantsContext.Participant] = []
let (myPeer, remotePeer) = peers
if let myPeer {
var myVideoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
switch state.videoState {
case .active:
myVideoDescription = GroupCallParticipantsContext.Participant.VideoDescription(endpointId: "temp-local", ssrcGroups: [], audioSsrc: nil, isPaused: false)
default:
break
}
participants.append(GroupCallParticipantsContext.Participant(
peer: myPeer._asPeer(),
ssrc: nil,
videoDescription: nil,
videoDescription: myVideoDescription,
presentationDescription: nil,
joinTimestamp: 0,
raiseHandRating: nil,
@ -1040,10 +1068,18 @@ final class VideoChatScreenComponent: Component {
))
}
if let remotePeer {
var remoteVideoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
switch state.remoteVideoState {
case .active:
remoteVideoDescription = GroupCallParticipantsContext.Participant.VideoDescription(endpointId: "temp-remote", ssrcGroups: [], audioSsrc: nil, isPaused: false)
default:
break
}
participants.append(GroupCallParticipantsContext.Participant(
peer: remotePeer._asPeer(),
ssrc: nil,
videoDescription: nil,
videoDescription: remoteVideoDescription,
presentationDescription: nil,
joinTimestamp: 0,
raiseHandRating: nil,
@ -1087,7 +1123,7 @@ final class VideoChatScreenComponent: Component {
self.members = component.initialData.members
self.invitedPeers = component.initialData.invitedPeers
if let members = self.members {
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.id }) })
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.peer.id }) })
}
self.callState = component.initialData.callState
}
@ -1128,7 +1164,7 @@ final class VideoChatScreenComponent: Component {
self.members = members
if let members {
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.id }) })
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.peer.id }) })
}
if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden {
@ -1234,10 +1270,16 @@ final class VideoChatScreenComponent: Component {
self.invitedPeersDisposable = (groupCall.invitedPeers
|> mapToSignal { invitedPeers in
return accountContext.engine.data.get(
EngineDataList(invitedPeers.map({ TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }))
EngineDataMap(invitedPeers.map({ TelegramEngine.EngineData.Item.Peer.Peer(id: $0.id) }))
)
|> map { peers in
return peers.compactMap { $0 }
|> map { peers -> [InvitedPeer] in
var result: [InvitedPeer] = []
for invitedPeer in invitedPeers {
if let maybePeer = peers[invitedPeer.id], let peer = maybePeer {
result.append(InvitedPeer(peer: peer, state: invitedPeer.state))
}
}
return result
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] invitedPeers in
@ -1247,7 +1289,7 @@ final class VideoChatScreenComponent: Component {
var invitedPeers = invitedPeers
if let members {
invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.id }) })
invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.peer.id }) })
}
if self.invitedPeers != invitedPeers {
@ -2461,13 +2503,13 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
let peer: EnginePeer?
let members: PresentationGroupCallMembers?
let callState: PresentationGroupCallState
let invitedPeers: [EnginePeer]
let invitedPeers: [VideoChatScreenComponent.InvitedPeer]
init(
peer: EnginePeer?,
members: PresentationGroupCallMembers?,
callState: PresentationGroupCallState,
invitedPeers: [EnginePeer]
invitedPeers: [VideoChatScreenComponent.InvitedPeer]
) {
self.peer = peer
self.members = members
@ -2633,7 +2675,7 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
let accountContext = groupCall.accountContext
let invitedPeers = groupCall.invitedPeers |> take(1) |> mapToSignal { invitedPeers in
return accountContext.engine.data.get(
EngineDataList(invitedPeers.map({ TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }))
EngineDataList(invitedPeers.map(\.id).map({ TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }))
)
}
return combineLatest(
@ -2647,7 +2689,12 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
peer: peer,
members: members,
callState: callState,
invitedPeers: invitedPeers.compactMap { $0 }
invitedPeers: invitedPeers.compactMap { peer -> VideoChatScreenComponent.InvitedPeer? in
guard let peer else {
return nil
}
return VideoChatScreenComponent.InvitedPeer(peer: peer, state: nil)
}
)
}
case let .conferenceSource(conferenceSource):

View File

@ -1891,7 +1891,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
self.updateDecorationsColors()
let invitedPeers: Signal<[EnginePeer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[EnginePeer], NoError> in
|> mapToSignal { peers -> Signal<[EnginePeer], NoError> in
let ids = peers.map(\.id)
return context.engine.data.get(EngineDataList(
ids.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
))

View File

@ -19,6 +19,7 @@ public enum CallSessionEndedType {
case hungUp
case busy
case missed
case switchedToConference
}
public enum CallSessionTerminationReason: Equatable {
@ -709,7 +710,7 @@ private final class CallSessionManagerContext {
case .missed:
mappedReason = .ended(.missed)
case .switchToConference:
mappedReason = .ended(.hungUp)
mappedReason = .ended(.switchedToConference)
}
context.state = .dropping(reason: mappedReason, disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: reason)
|> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in
@ -763,7 +764,7 @@ private final class CallSessionManagerContext {
if let (id, accessHash) = dropData {
self.contextIdByStableId.removeValue(forKey: id)
context.state = .dropping(reason: .ended(.hungUp), disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: .switchToConference(encryptedGroupKey: encryptedGroupKey))
context.state = .dropping(reason: .ended(.switchedToConference), disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: .switchToConference(encryptedGroupKey: encryptedGroupKey))
|> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in
if let strongSelf = self {
if let context = strongSelf.contexts[internalId] {
@ -962,11 +963,15 @@ private final class CallSessionManagerContext {
case .phoneCallDiscardReasonDisconnect:
parsedReason = .error(.disconnected)
case .phoneCallDiscardReasonHangup:
parsedReason = .ended(.hungUp)
if context.pendingConference != nil {
parsedReason = .ended(.switchedToConference)
} else {
parsedReason = .ended(.hungUp)
}
case .phoneCallDiscardReasonMissed:
parsedReason = .ended(.missed)
case .phoneCallDiscardReasonAllowGroupCall:
parsedReason = .ended(.hungUp)
parsedReason = .ended(.switchedToConference)
}
} else {
parsedReason = .ended(.hungUp)

View File

@ -503,21 +503,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.update(transition: .easeInOut(duration: 0.25))
}
public func takeIncomingVideoLayer() -> (CALayer, VideoSource.Output?)? {
var remoteVideoContainerKey: VideoContainerView.Key?
public func takeIncomingVideoLayer() -> ((CALayer, VideoSource.Output?), Bool)? {
var activeVideoSources: [(VideoContainerView.Key, Bool)] = []
if self.swapLocalAndRemoteVideo {
if let _ = self.activeLocalVideoSource {
activeVideoSources.append((.background, false))
}
if let _ = self.activeRemoteVideoSource {
remoteVideoContainerKey = .foreground
activeVideoSources.append((.foreground, true))
}
} else {
if let _ = self.activeRemoteVideoSource {
remoteVideoContainerKey = .background
activeVideoSources.append((.background, true))
}
if let _ = self.activeLocalVideoSource {
activeVideoSources.append((.foreground, false))
}
}
if let remoteVideoContainerKey, let videoContainerView = self.videoContainerViews.first(where: { $0.key == remoteVideoContainerKey }) {
if let videoSource = activeVideoSources.first, let videoContainerView = self.videoContainerViews.first(where: { $0.key == videoSource.0 }) {
videoContainerView.videoContainerLayerTaken = true
return (videoContainerView.videoContainerLayer, videoContainerView.currentVideoOutput)
return ((videoContainerView.videoContainerLayer, videoContainerView.currentVideoOutput), videoSource.1)
}
return nil

View File

@ -1087,10 +1087,8 @@ public final class OngoingGroupCallContext {
}
func addRemoteConnectedEvent(isRemoteConntected: Bool) {
#if os(iOS)
self.context.addRemoteConnectedEvent(isRemoteConntected)
#endif
func activateIncomingAudio() {
self.context.activateIncomingAudio()
}
}
@ -1314,9 +1312,9 @@ public final class OngoingGroupCallContext {
}
}
public func addRemoteConnectedEvent(isRemoteConntected: Bool) {
public func activateIncomingAudio() {
self.impl.with { impl in
impl.addRemoteConnectedEvent(isRemoteConntected: isRemoteConntected)
impl.activateIncomingAudio()
}
}
}

View File

@ -453,7 +453,7 @@ isConference:(bool)isConference;
- (void)getStats:(void (^ _Nonnull)(OngoingGroupCallStats * _Nonnull))completion;
- (void)addRemoteConnectedEvent:(bool)isRemoteConnected;
- (void)activateIncomingAudio;
@end

View File

@ -66,6 +66,29 @@
namespace tgcalls {
class WrappedChildAudioDeviceModuleControl {
public:
WrappedChildAudioDeviceModuleControl() {
}
virtual ~WrappedChildAudioDeviceModuleControl() {
_mutex.Lock();
_mutex.Unlock();
}
public:
void setActive() {
_mutex.Lock();
_mutex.Unlock();
}
private:
webrtc::Mutex _mutex;
};
class SharedAudioDeviceModule {
public:
virtual ~SharedAudioDeviceModule() = default;
@ -93,12 +116,15 @@ public:
}
void UpdateAudioCallback(webrtc::AudioTransport *previousAudioCallback, webrtc::AudioTransport *audioCallback) {
if (audioCallback == nil) {
if (_currentAudioTransport == previousAudioCallback) {
_currentAudioTransport = nil;
if (audioCallback) {
_audioTransports.push_back(audioCallback);
} else if (previousAudioCallback) {
for (size_t i = 0; i < _audioTransports.size(); i++) {
if (_audioTransports[i] == previousAudioCallback) {
_audioTransports.erase(_audioTransports.begin() + i);
break;
}
}
} else {
_currentAudioTransport = audioCallback;
}
}
@ -409,19 +435,26 @@ public:
bool keyPressed,
uint32_t& newMicLevel
) override {
if (_currentAudioTransport) {
return _currentAudioTransport->RecordedDataIsAvailable(
audioSamples,
nSamples,
nBytesPerSample,
nChannels,
samplesPerSec,
totalDelayMS,
clockDrift,
currentMicLevel,
keyPressed,
newMicLevel
);
if (!_audioTransports.empty()) {
for (size_t i = 0; i < _audioTransports.size(); i++) {
auto result = _audioTransports[_audioTransports.size() - 1]->RecordedDataIsAvailable(
audioSamples,
nSamples,
nBytesPerSample,
nChannels,
samplesPerSec,
totalDelayMS,
clockDrift,
currentMicLevel,
keyPressed,
newMicLevel
);
if (i == _audioTransports.size() - 1) {
return result;
}
}
return 0;
} else {
return 0;
}
@ -440,20 +473,26 @@ public:
uint32_t& newMicLevel,
absl::optional<int64_t> estimatedCaptureTimeNS
) override {
if (_currentAudioTransport) {
return _currentAudioTransport->RecordedDataIsAvailable(
audioSamples,
nSamples,
nBytesPerSample,
nChannels,
samplesPerSec,
totalDelayMS,
clockDrift,
currentMicLevel,
keyPressed,
newMicLevel,
estimatedCaptureTimeNS
);
if (!_audioTransports.empty()) {
for (size_t i = 0; i < _audioTransports.size(); i++) {
auto result = _audioTransports[_audioTransports.size() - 1]->RecordedDataIsAvailable(
audioSamples,
nSamples,
nBytesPerSample,
nChannels,
samplesPerSec,
totalDelayMS,
clockDrift,
currentMicLevel,
keyPressed,
newMicLevel,
estimatedCaptureTimeNS
);
if (i == _audioTransports.size() - 1) {
return result;
}
}
return 0;
} else {
return 0;
}
@ -470,8 +509,8 @@ public:
int64_t* elapsed_time_ms,
int64_t* ntp_time_ms
) override {
if (_currentAudioTransport) {
return _currentAudioTransport->NeedMorePlayData(
if (!_audioTransports.empty()) {
return _audioTransports[_audioTransports.size() - 1]->NeedMorePlayData(
nSamples,
nBytesPerSample,
nChannels,
@ -496,8 +535,8 @@ public:
int64_t* elapsed_time_ms,
int64_t* ntp_time_ms
) override {
if (_currentAudioTransport) {
_currentAudioTransport->PullRenderData(
if (!_audioTransports.empty()) {
_audioTransports[_audioTransports.size() - 1]->PullRenderData(
bits_per_sample,
sample_rate,
number_of_channels,
@ -540,7 +579,7 @@ public:
private:
bool _isStarted = false;
webrtc::AudioTransport *_currentAudioTransport = nullptr;
std::vector<webrtc::AudioTransport *> _audioTransports;
};
class WrappedChildAudioDeviceModule : public tgcalls::DefaultWrappedAudioDeviceModule {
@ -556,13 +595,28 @@ public:
auto previousAudioCallback = _audioCallback;
_audioCallback = audioCallback;
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(previousAudioCallback, audioCallback);
if (_isActive) {
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(previousAudioCallback, audioCallback);
}
return 0;
}
public:
void setIsActive() {
if (_isActive) {
return;
}
_isActive = true;
if (_audioCallback) {
((WrappedAudioDeviceModuleIOS *)WrappedInstance().get())->UpdateAudioCallback(nullptr, _audioCallback);
}
}
private:
webrtc::AudioTransport *_audioCallback = nullptr;
bool _isActive = false;
};
class SharedAudioDeviceModuleImpl: public tgcalls::SharedAudioDeviceModule {
@ -1785,7 +1839,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
},
.createWrappedAudioDeviceModule = [audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
if (audioDeviceModule) {
return audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule();
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule();
((WrappedChildAudioDeviceModule *)result.get())->setIsActive();
return result;
} else {
return nullptr;
}
@ -2493,7 +2549,9 @@ isConference:(bool)isConference {
},
.createWrappedAudioDeviceModule = [audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
if (audioDeviceModule) {
return audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule();
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule();
((WrappedChildAudioDeviceModule *)result.get())->setIsActive();
return result;
} else {
return nullptr;
}
@ -2835,10 +2893,7 @@ isConference:(bool)isConference {
}
}
- (void)addRemoteConnectedEvent:(bool)isRemoteConnected {
if (_instance) {
_instance->internal_addCustomNetworkEvent(isRemoteConnected);
}
- (void)activateIncomingAudio {
}
@end