diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 5c53624fbd..504def0046 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -280,6 +280,8 @@ public protocol PresentationGroupCall: class { var internalId: CallSessionInternalId { get } var peerId: PeerId { get } + var isVideo: Bool { get } + var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get } var canBeRemoved: Signal { get } @@ -296,6 +298,8 @@ public protocol PresentationGroupCall: class { func toggleIsMuted() func setIsMuted(action: PresentationGroupCallMuteAction) + func requestVideo() + func disableVideo() func updateDefaultParticipantsAreMuted(isMuted: Bool) func setVolume(peerId: PeerId, volume: Double) func setFullSizeVideo(peerId: PeerId?) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index b0bed6e24c..858fec1d7d 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -312,6 +312,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public let peerId: PeerId public let peer: Peer? + public private(set) var isVideo: Bool + private let temporaryJoinTimestamp: Int32 private var internalState: InternalState = .requesting @@ -319,6 +321,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var callContext: OngoingGroupCallContext? private var ssrcMapping: [UInt32: PeerId] = [:] + private var requestedSsrcs = Set() + private var summaryInfoState = Promise(nil) private var summaryParticipantsState = Promise(nil) @@ -433,6 +437,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } private let memberEventsPipeDisposable = MetaDisposable() + private let joinDisposable = MetaDisposable() private let requestDisposable = MetaDisposable() private var groupCallParticipantUpdatesDisposable: Disposable? @@ -461,6 +466,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return self.incomingVideoSourcePromise.get() } + private var missingSsrcs = Set() + private let missingSsrcsDisposable = MetaDisposable() + private var isRequestingMissingSsrcs: Bool = false + init( accountContext: AccountContext, audioSession: ManagedAudioSession, @@ -484,7 +493,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - self.videoCapturer = OngoingCallVideoCapturer() + self.isVideo = false var didReceiveAudioOutputs = false @@ -623,9 +632,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if !removedSsrc.isEmpty { strongSelf.callContext?.removeSsrcs(ssrcs: removedSsrc) } - if !addedParticipants.isEmpty { - strongSelf.callContext?.addParticipants(participants: addedParticipants) - } } }) @@ -745,6 +751,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.audioSessionActiveDisposable?.dispose() self.summaryStateDisposable?.dispose() self.audioSessionDisposable?.dispose() + self.joinDisposable.dispose() self.requestDisposable.dispose() self.groupCallParticipantUpdatesDisposable?.dispose() self.leaveDisposable.dispose() @@ -756,6 +763,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.participantsContextStateDisposable.dispose() self.myAudioLevelDisposable.dispose() self.memberEventsPipeDisposable.dispose() + self.missingSsrcsDisposable.dispose() self.myAudioLevelTimer?.invalidate() self.typingDisposable.dispose() @@ -802,7 +810,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { break default: if case let .active(callInfo) = internalState { - let callContext = OngoingGroupCallContext(video: self.videoCapturer) + let callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + strongSelf.maybeRequestParticipants(ssrcs: ssrcs) + } + }) self.incomingVideoSourcePromise.set(callContext.videoSources |> deliverOnMainQueue |> map { [weak self] sources -> [PeerId: UInt32] in @@ -818,8 +833,16 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return result }) self.callContext = callContext - self.requestDisposable.set((callContext.joinPayload - |> take(1) + self.joinDisposable.set((callContext.joinPayload + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + }) |> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in guard let strongSelf = self else { return @@ -963,14 +986,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } self.ssrcMapping.removeAll() - var addedParticipants: [(UInt32, String?)] = [] for participant in initialState.participants { self.ssrcMapping[participant.ssrc] = participant.peer.id - if participant.peer.id != self.accountContext.account.peerId { - addedParticipants.append((participant.ssrc, participant.jsonParams)) - } } - self.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants) + self.callContext?.setJoinResponse(payload: clientParams, participants: []) let accountContext = self.accountContext let peerId = self.peerId @@ -1135,6 +1154,65 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + private func maybeRequestParticipants(ssrcs: Set) { + var missingSsrcs = ssrcs + + var addedParticipants: [(UInt32, String?)] = [] + + if let membersValue = self.membersValue { + for participant in membersValue.participants { + if missingSsrcs.contains(participant.ssrc) { + missingSsrcs.remove(participant.ssrc) + + addedParticipants.append((participant.ssrc, participant.jsonParams)) + } + } + } + + if !addedParticipants.isEmpty { + self.callContext?.addParticipants(participants: addedParticipants) + } + + if !missingSsrcs.isEmpty { + self.missingSsrcs.formUnion(missingSsrcs) + self.maybeRequestMissingSsrcs() + } + } + + private func maybeRequestMissingSsrcs() { + if self.isRequestingMissingSsrcs { + return + } + if self.missingSsrcs.isEmpty { + return + } + if case let .estabilished(callInfo, _, ssrc, _) = self.internalState { + self.isRequestingMissingSsrcs = true + + let requestedSsrcs = self.missingSsrcs + self.missingSsrcsDisposable.set((getGroupCallParticipants(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, ssrcs: Array(requestedSsrcs), offset: "", limit: 100) + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + strongSelf.isRequestingMissingSsrcs = false + strongSelf.missingSsrcs.subtract(requestedSsrcs) + + var addedParticipants: [(UInt32, String?)] = [] + + for participant in state.participants { + addedParticipants.append((participant.ssrc, participant.jsonParams)) + } + + if !addedParticipants.isEmpty { + strongSelf.callContext?.addParticipants(participants: addedParticipants) + } + + strongSelf.maybeRequestMissingSsrcs() + })) + } + } + private func startCheckingCallIfNeeded() { if self.checkCallDisposable != nil { return @@ -1271,6 +1349,25 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + public func requestVideo() { + if self.videoCapturer == nil { + let videoCapturer = OngoingCallVideoCapturer() + self.videoCapturer = videoCapturer + } + self.isVideo = true + if let videoCapturer = self.videoCapturer { + self.callContext?.requestVideo(videoCapturer) + } + } + + public func disableVideo() { + self.isVideo = false + if let _ = self.videoCapturer { + self.videoCapturer = nil + self.callContext?.disableVideo() + } + } + public func setVolume(peerId: PeerId, volume: Double) { for (ssrc, id) in self.ssrcMapping { if id == peerId { @@ -1422,6 +1519,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + self.joinDisposable.set(nil) self.requestDisposable.set((currentOrRequestedCall |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 07ea166886..41013e1bc2 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -567,6 +567,7 @@ public final class VoiceChatController: ViewController { private let bottomPanelBackgroundNode: ASDisplayNode private let bottomCornersNode: ASImageNode fileprivate let audioOutputNode: CallControllerButtonItemNode + fileprivate let cameraButtonNode: CallControllerButtonItemNode fileprivate let leaveNode: CallControllerButtonItemNode fileprivate let actionButton: VoiceChatActionButton private let leftBorderNode: ASDisplayNode @@ -699,6 +700,7 @@ public final class VoiceChatController: ViewController { self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: false) self.audioOutputNode = CallControllerButtonItemNode() + self.cameraButtonNode = CallControllerButtonItemNode() self.leaveNode = CallControllerButtonItemNode() self.actionButton = VoiceChatActionButton() @@ -1094,6 +1096,7 @@ public final class VoiceChatController: ViewController { self.bottomPanelNode.addSubnode(self.bottomCornersNode) self.bottomPanelNode.addSubnode(self.bottomPanelBackgroundNode) self.bottomPanelNode.addSubnode(self.audioOutputNode) + self.bottomPanelNode.addSubnode(self.cameraButtonNode) self.bottomPanelNode.addSubnode(self.leaveNode) self.bottomPanelNode.addSubnode(self.actionButton) @@ -1293,6 +1296,8 @@ public final class VoiceChatController: ViewController { self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside) + self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside) + self.optionsButton.contextAction = { [weak self, weak optionsButton] sourceNode, gesture in guard let strongSelf = self, let controller = strongSelf.controller, let strongOptionsButton = optionsButton else { return @@ -1705,6 +1710,14 @@ public final class VoiceChatController: ViewController { } } + @objc private func cameraPressed() { + if self.call.isVideo { + self.call.disableVideo() + } else { + self.call.requestVideo() + } + } + private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { guard let (layout, _) = self.validLayout else { return @@ -1960,6 +1973,10 @@ public final class VoiceChatController: ViewController { let sideButtonSize = CGSize(width: 60.0, height: 60.0) self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: soundTitle, transition: .animated(duration: 0.3, curve: .linear)) + let cameraButtonSize = CGSize(width: 40.0, height: 40.0) + + self.cameraButtonNode.update(size: cameraButtonSize, content: CallControllerButtonItemNode.Content(appearance: CallControllerButtonItemNode.Content.Appearance.blurred(isFilled: false), image: .camera), text: " ", transition: .animated(duration: 0.3, curve: .linear)) + self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .end), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate) } @@ -2039,6 +2056,7 @@ public final class VoiceChatController: ViewController { transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame) let sideButtonSize = CGSize(width: 60.0, height: 60.0) + let cameraButtonSize = CGSize(width: 40.0, height: 40.0) let centralButtonSize = CGSize(width: 440.0, height: 440.0) let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floorToScreenPixels((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize) @@ -2112,8 +2130,14 @@ public final class VoiceChatController: ViewController { let sideButtonOrigin = max(sideButtonMinimalInset, floor((size.width - 144.0) / 2.0) - sideButtonOffset - sideButtonSize.width) if self.audioOutputNode.supernode === self.bottomPanelNode { - transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)) + let cameraButtonDistance: CGFloat = 4.0 + + let audioOutputFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height - cameraButtonDistance - cameraButtonSize.height) / 2.0) + cameraButtonDistance + cameraButtonSize.height), size: sideButtonSize) + + transition.updateFrame(node: self.audioOutputNode, frame: audioOutputFrame) transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: size.width - sideButtonOrigin - sideButtonSize.width, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)) + + transition.updateFrame(node: self.cameraButtonNode, frame: CGRect(origin: CGPoint(x: floor(audioOutputFrame.midX - cameraButtonSize.width / 2.0), y: audioOutputFrame.minY - cameraButtonDistance - cameraButtonSize.height), size: cameraButtonSize)) } if isFirstTime { while !self.enqueuedTransitions.isEmpty { @@ -2139,10 +2163,13 @@ public final class VoiceChatController: ViewController { if self.actionButton.supernode !== self.bottomPanelNode { self.actionButton.ignoreHierarchyChanges = true self.audioOutputNode.isHidden = false + self.cameraButtonNode.isHidden = false self.leaveNode.isHidden = false self.audioOutputNode.layer.removeAllAnimations() + self.cameraButtonNode.layer.removeAllAnimations() self.leaveNode.layer.removeAllAnimations() self.bottomPanelNode.addSubnode(self.audioOutputNode) + self.bottomPanelNode.addSubnode(self.cameraButtonNode) self.bottomPanelNode.addSubnode(self.leaveNode) self.bottomPanelNode.addSubnode(self.actionButton) self.containerLayoutUpdated(layout, navigationHeight :navigationHeight, transition: .immediate) @@ -2386,7 +2413,7 @@ public final class VoiceChatController: ViewController { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is DirectionalPanGestureRecognizer { let location = gestureRecognizer.location(in: self.bottomPanelNode.view) - if self.audioOutputNode.frame.contains(location) || self.leaveNode.frame.contains(location) { + if self.audioOutputNode.frame.contains(location) || (!self.cameraButtonNode.isHidden && self.cameraButtonNode.frame.contains(location)) || self.leaveNode.frame.contains(location) { return false } } diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index b12dbff1b1..269bdfd16b 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -190,8 +190,10 @@ public enum GetGroupCallParticipantsError { case generic } -public func getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, offset: String, limit: Int32) -> Signal { - return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: [], sources: [], offset: offset, limit: limit)) +public func getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, ssrcs: [UInt32] = [], offset: String, limit: Int32) -> Signal { + return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: [], sources: ssrcs.map { + Int32(bitPattern: $0) + }, offset: offset, limit: limit)) |> mapError { _ -> GetGroupCallParticipantsError in return .generic } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index f9b6778bfb..59aee58f30 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -50,11 +50,12 @@ public final class OngoingGroupCallContext { let videoSources = ValuePromise>(Set(), ignoreRepeated: true) - init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?) { + init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set) -> Void) { self.queue = queue var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)? + var participantDescriptionsRequiredImpl: (([NSNumber]) -> Void)? let videoSources = self.videoSources self.context = GroupCallThreadLocalContext( @@ -70,6 +71,9 @@ public final class OngoingGroupCallContext { videoCapturer: video?.impl, incomingVideoSourcesUpdated: { ssrcs in videoSources.set(Set(ssrcs.map { $0.uint32Value })) + }, + participantDescriptionsRequired: { ssrcs in + participantDescriptionsRequired(Set(ssrcs.map { $0.uint32Value })) } ) @@ -167,6 +171,30 @@ public final class OngoingGroupCallContext { self.context.setIsMuted(isMuted) } + func requestVideo(_ capturer: OngoingCallVideoCapturer?) { + let queue = self.queue + self.context.requestVideo(capturer?.impl, completion: { [weak self] payload, ssrc in + queue.async { + guard let strongSelf = self else { + return + } + strongSelf.joinPayload.set(.single((payload, ssrc))) + } + }) + } + + public func disableVideo() { + let queue = self.queue + self.context.disableVideo({ [weak self] payload, ssrc in + queue.async { + guard let strongSelf = self else { + return + } + strongSelf.joinPayload.set(.single((payload, ssrc))) + } + }) + } + func switchAudioInput(_ deviceId: String) { self.context.switchAudioInput(deviceId) } @@ -278,10 +306,10 @@ public final class OngoingGroupCallContext { } } - public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?) { + public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set) -> Void) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video) + return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, participantDescriptionsRequired: participantDescriptionsRequired) }) } @@ -291,6 +319,18 @@ public final class OngoingGroupCallContext { } } + public func requestVideo(_ capturer: OngoingCallVideoCapturer?) { + self.impl.with { impl in + impl.requestVideo(capturer) + } + } + + public func disableVideo() { + self.impl.with { impl in + impl.disableVideo() + } + } + public func switchAudioInput(_ deviceId: String) { self.impl.with { impl in impl.switchAudioInput(deviceId) diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index eaa148f0cd..b0b0dd038c 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -168,7 +168,7 @@ typedef NS_ENUM(int32_t, GroupCallNetworkState) { @interface GroupCallThreadLocalContext : NSObject -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated inputDeviceId:(NSString * _Nonnull)inputDeviceId outputDeviceId:(NSString * _Nonnull)outputDeviceId videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer incomingVideoSourcesUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))incomingVideoSourcesUpdated; +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated inputDeviceId:(NSString * _Nonnull)inputDeviceId outputDeviceId:(NSString * _Nonnull)outputDeviceId videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer incomingVideoSourcesUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))incomingVideoSourcesUpdated participantDescriptionsRequired:(void (^ _Nonnull)(NSArray * _Nonnull))participantDescriptionsRequired; - (void)stop; @@ -177,6 +177,8 @@ typedef NS_ENUM(int32_t, GroupCallNetworkState) { - (void)removeSsrcs:(NSArray * _Nonnull)ssrcs; - (void)addParticipants:(NSArray * _Nonnull)participants; - (void)setIsMuted:(bool)isMuted; +- (void)requestVideo:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer completion:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; +- (void)disableVideo:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; - (void)setVolumeForSsrc:(uint32_t)ssrc volume:(double)volume; - (void)setFullSizeVideoSsrc:(uint32_t)ssrc; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 58c9ce70e8..b29955e061 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -818,7 +818,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; @implementation GroupCallThreadLocalContext -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated inputDeviceId:(NSString * _Nonnull)inputDeviceId outputDeviceId:(NSString * _Nonnull)outputDeviceId videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer incomingVideoSourcesUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))incomingVideoSourcesUpdated { +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated inputDeviceId:(NSString * _Nonnull)inputDeviceId outputDeviceId:(NSString * _Nonnull)outputDeviceId videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer incomingVideoSourcesUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))incomingVideoSourcesUpdated participantDescriptionsRequired:(void (^ _Nonnull)(NSArray * _Nonnull))participantDescriptionsRequired { self = [super init]; if (self != nil) { _queue = queue; @@ -855,6 +855,13 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; [mappedSources addObject:@(it)]; } incomingVideoSourcesUpdated(mappedSources); + }, + .participantDescriptionsRequired = [participantDescriptionsRequired](std::vector const &ssrcs) { + NSMutableArray *mappedSources = [[NSMutableArray alloc] init]; + for (auto it : ssrcs) { + [mappedSources addObject:@(it)]; + } + participantDescriptionsRequired(mappedSources); } })); } @@ -868,105 +875,106 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; } } +static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonnull completion)(NSString * _Nonnull, uint32_t)) { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + + int32_t signedSsrc = *(int32_t *)&payload.ssrc; + + dict[@"ssrc"] = @(signedSsrc); + dict[@"ufrag"] = [NSString stringWithUTF8String:payload.ufrag.c_str()]; + dict[@"pwd"] = [NSString stringWithUTF8String:payload.pwd.c_str()]; + + NSMutableArray *fingerprints = [[NSMutableArray alloc] init]; + for (auto &fingerprint : payload.fingerprints) { + [fingerprints addObject:@{ + @"hash": [NSString stringWithUTF8String:fingerprint.hash.c_str()], + @"fingerprint": [NSString stringWithUTF8String:fingerprint.fingerprint.c_str()], + @"setup": [NSString stringWithUTF8String:fingerprint.setup.c_str()] + }]; + } + + dict[@"fingerprints"] = fingerprints; + + NSMutableArray *parsedVideoSsrcGroups = [[NSMutableArray alloc] init]; + NSMutableArray *parsedVideoSources = [[NSMutableArray alloc] init]; + for (auto &group : payload.videoSourceGroups) { + NSMutableDictionary *parsedGroup = [[NSMutableDictionary alloc] init]; + parsedGroup[@"semantics"] = [NSString stringWithUTF8String:group.semantics.c_str()]; + NSMutableArray *sources = [[NSMutableArray alloc] init]; + for (auto &source : group.ssrcs) { + [sources addObject:@(source)]; + if (![parsedVideoSources containsObject:@(source)]) { + [parsedVideoSources addObject:@(source)]; + } + } + parsedGroup[@"sources"] = sources; + [parsedVideoSsrcGroups addObject:parsedGroup]; + } + if (parsedVideoSsrcGroups.count != 0) { + dict[@"ssrc-groups"] = parsedVideoSsrcGroups; + } + + NSMutableArray *videoPayloadTypes = [[NSMutableArray alloc] init]; + for (auto &payloadType : payload.videoPayloadTypes) { + NSMutableDictionary *parsedType = [[NSMutableDictionary alloc] init]; + parsedType[@"id"] = @(payloadType.id); + NSString *name = [NSString stringWithUTF8String:payloadType.name.c_str()]; + parsedType[@"name"] = name; + parsedType[@"clockrate"] = @(payloadType.clockrate); + if (![name isEqualToString:@"rtx"]) { + parsedType[@"channels"] = @(payloadType.channels); + } + + NSMutableDictionary *parsedParameters = [[NSMutableDictionary alloc] init]; + for (auto &it : payloadType.parameters) { + NSString *key = [NSString stringWithUTF8String:it.first.c_str()]; + NSString *value = [NSString stringWithUTF8String:it.second.c_str()]; + parsedParameters[key] = value; + } + if (parsedParameters.count != 0) { + parsedType[@"parameters"] = parsedParameters; + } + + if (![name isEqualToString:@"rtx"]) { + NSMutableArray *parsedFbs = [[NSMutableArray alloc] init]; + for (auto &it : payloadType.feedbackTypes) { + NSMutableDictionary *parsedFb = [[NSMutableDictionary alloc] init]; + parsedFb[@"type"] = [NSString stringWithUTF8String:it.type.c_str()]; + if (it.subtype.size() != 0) { + parsedFb[@"subtype"] = [NSString stringWithUTF8String:it.subtype.c_str()]; + } + [parsedFbs addObject:parsedFb]; + } + parsedType[@"rtcp-fbs"] = parsedFbs; + } + + [videoPayloadTypes addObject:parsedType]; + } + if (videoPayloadTypes.count != 0) { + dict[@"payload-types"] = videoPayloadTypes; + } + + NSMutableArray *parsedExtensions = [[NSMutableArray alloc] init]; + for (auto &it : payload.videoExtensionMap) { + NSMutableDictionary *parsedExtension = [[NSMutableDictionary alloc] init]; + parsedExtension[@"id"] = @(it.first); + parsedExtension[@"uri"] = [NSString stringWithUTF8String:it.second.c_str()]; + [parsedExtensions addObject:parsedExtension]; + } + if (parsedExtensions.count != 0) { + dict[@"rtp-hdrexts"] = parsedExtensions; + } + + NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + completion(string, payload.ssrc); +} + - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { if (_instance) { _instance->emitJoinPayload([completion](tgcalls::GroupJoinPayload payload) { - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - - int32_t signedSsrc = *(int32_t *)&payload.ssrc; - - dict[@"ssrc"] = @(signedSsrc); - dict[@"ufrag"] = [NSString stringWithUTF8String:payload.ufrag.c_str()]; - dict[@"pwd"] = [NSString stringWithUTF8String:payload.pwd.c_str()]; - - NSMutableArray *fingerprints = [[NSMutableArray alloc] init]; - for (auto &fingerprint : payload.fingerprints) { - [fingerprints addObject:@{ - @"hash": [NSString stringWithUTF8String:fingerprint.hash.c_str()], - @"fingerprint": [NSString stringWithUTF8String:fingerprint.fingerprint.c_str()], - @"setup": [NSString stringWithUTF8String:fingerprint.setup.c_str()] - }]; - } - - dict[@"fingerprints"] = fingerprints; - - NSMutableArray *parsedVideoSsrcGroups = [[NSMutableArray alloc] init]; - NSMutableArray *parsedVideoSources = [[NSMutableArray alloc] init]; - for (auto &group : payload.videoSourceGroups) { - NSMutableDictionary *parsedGroup = [[NSMutableDictionary alloc] init]; - parsedGroup[@"semantics"] = [NSString stringWithUTF8String:group.semantics.c_str()]; - NSMutableArray *sources = [[NSMutableArray alloc] init]; - for (auto &source : group.ssrcs) { - [sources addObject:@(source)]; - if (![parsedVideoSources containsObject:@(source)]) { - [parsedVideoSources addObject:@(source)]; - } - } - parsedGroup[@"sources"] = sources; - [parsedVideoSsrcGroups addObject:parsedGroup]; - } - if (parsedVideoSsrcGroups.count != 0) { - dict[@"ssrc-groups"] = parsedVideoSsrcGroups; - } - if (parsedVideoSources.count != 0) { - //dict[@"sources"] = parsedVideoSources; - } - - NSMutableArray *videoPayloadTypes = [[NSMutableArray alloc] init]; - for (auto &payloadType : payload.videoPayloadTypes) { - NSMutableDictionary *parsedType = [[NSMutableDictionary alloc] init]; - parsedType[@"id"] = @(payloadType.id); - NSString *name = [NSString stringWithUTF8String:payloadType.name.c_str()]; - parsedType[@"name"] = name; - parsedType[@"clockrate"] = @(payloadType.clockrate); - if (![name isEqualToString:@"rtx"]) { - parsedType[@"channels"] = @(payloadType.channels); - } - - NSMutableDictionary *parsedParameters = [[NSMutableDictionary alloc] init]; - for (auto &it : payloadType.parameters) { - NSString *key = [NSString stringWithUTF8String:it.first.c_str()]; - NSString *value = [NSString stringWithUTF8String:it.second.c_str()]; - parsedParameters[key] = value; - } - if (parsedParameters.count != 0) { - parsedType[@"parameters"] = parsedParameters; - } - - if (![name isEqualToString:@"rtx"]) { - NSMutableArray *parsedFbs = [[NSMutableArray alloc] init]; - for (auto &it : payloadType.feedbackTypes) { - NSMutableDictionary *parsedFb = [[NSMutableDictionary alloc] init]; - parsedFb[@"type"] = [NSString stringWithUTF8String:it.type.c_str()]; - if (it.subtype.size() != 0) { - parsedFb[@"subtype"] = [NSString stringWithUTF8String:it.subtype.c_str()]; - } - [parsedFbs addObject:parsedFb]; - } - parsedType[@"rtcp-fbs"] = parsedFbs; - } - - [videoPayloadTypes addObject:parsedType]; - } - if (videoPayloadTypes.count != 0) { - dict[@"payload-types"] = videoPayloadTypes; - } - - NSMutableArray *parsedExtensions = [[NSMutableArray alloc] init]; - for (auto &it : payload.videoExtensionMap) { - NSMutableDictionary *parsedExtension = [[NSMutableDictionary alloc] init]; - parsedExtension[@"id"] = @(it.first); - parsedExtension[@"uri"] = [NSString stringWithUTF8String:it.second.c_str()]; - [parsedExtensions addObject:parsedExtension]; - } - if (parsedExtensions.count != 0) { - dict[@"rtp-hdrexts"] = parsedExtensions; - } - - NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; - NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - - completion(string, payload.ssrc); + processJoinPayload(payload, completion); }); } } @@ -1295,6 +1303,22 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; } } +- (void)requestVideo:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer completion:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { + if (_instance) { + _instance->setVideoCapture([videoCapturer getInterface], [completion](auto payload){ + processJoinPayload(payload, completion); + }); + } +} + +- (void)disableVideo:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { + if (_instance) { + _instance->setVideoCapture(nullptr, [completion](auto payload){ + processJoinPayload(payload, completion); + }); + } +} + - (void)setVolumeForSsrc:(uint32_t)ssrc volume:(double)volume { if (_instance) { _instance->setVolume(ssrc, volume); diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index bca416c0b7..d874859813 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit bca416c0b76eac786f01b34e9d09e92df7863168 +Subproject commit d8748598133e39c3fc8c52a77a443c2d1da25032