diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index d88f8a329a..431d10c532 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -198,6 +198,7 @@ public protocol PresentationGroupCall: class { var canBeRemoved: Signal { get } var state: Signal { get } var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get } + var audioLevels: Signal<[(PeerId, Float)], NoError> { get } func leave() -> Signal diff --git a/submodules/AudioBlob/BUILD b/submodules/AudioBlob/BUILD new file mode 100644 index 0000000000..4e0c55ac05 --- /dev/null +++ b/submodules/AudioBlob/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AudioBlob", + module_name = "AudioBlob", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/LegacyComponents:LegacyComponents", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/BlobView.swift b/submodules/AudioBlob/Sources/BlobView.swift similarity index 97% rename from submodules/TelegramUI/Sources/BlobView.swift rename to submodules/AudioBlob/Sources/BlobView.swift index 4c62a3741a..8d753361e2 100644 --- a/submodules/TelegramUI/Sources/BlobView.swift +++ b/submodules/AudioBlob/Sources/BlobView.swift @@ -3,8 +3,7 @@ import UIKit import Display import LegacyComponents -final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration { - +public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration { private let smallBlob: BlobView private let mediumBlob: BlobView private let bigBlob: BlobView @@ -18,9 +17,9 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration private(set) var isAnimating = false - typealias BlobRange = (min: CGFloat, max: CGFloat) + public typealias BlobRange = (min: CGFloat, max: CGFloat) - init( + public init( frame: CGRect, maxLevel: CGFloat, smallBlobRange: BlobRange, @@ -84,13 +83,13 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration fatalError("init(coder:) has not been implemented") } - func setColor(_ color: UIColor) { + public func setColor(_ color: UIColor) { smallBlob.setColor(color) mediumBlob.setColor(color.withAlphaComponent(0.3)) bigBlob.setColor(color.withAlphaComponent(0.15)) } - func updateLevel(_ level: CGFloat) { + public func updateLevel(_ level: CGFloat) { let normalizedLevel = min(1, max(level / maxLevel, 0)) smallBlob.updateSpeedLevel(to: normalizedLevel) @@ -100,7 +99,7 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration audioLevel = normalizedLevel } - func startAnimating() { + public func startAnimating() { guard !isAnimating else { return } isAnimating = true @@ -112,7 +111,7 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration displayLinkAnimator?.isPaused = false } - func stopAnimating() { + public func stopAnimating() { guard isAnimating else { return } isAnimating = false @@ -138,7 +137,7 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration } } - override func layoutSubviews() { + override public func layoutSubviews() { super.layoutSubviews() smallBlob.frame = bounds diff --git a/submodules/ItemListPeerItem/BUILD b/submodules/ItemListPeerItem/BUILD index 804ffb8a2f..9828730878 100644 --- a/submodules/ItemListPeerItem/BUILD +++ b/submodules/ItemListPeerItem/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/ContextUI:ContextUI", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/AccountContext:AccountContext", + "//submodules/AudioBlob:AudioBlob", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 7a9036e188..fa3b5e1a3d 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -15,6 +15,7 @@ import TelegramStringFormatting import PeerPresenceStatusManager import ContextUI import AccountContext +import AudioBlob private final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? @@ -346,8 +347,9 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let shimmering: ItemListPeerItemShimmering? let displayDecorations: Bool let disableInteractiveTransitionIfNecessary: Bool + let audioLevel: Signal? - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) { + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, audioLevel: Signal? = nil) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -380,6 +382,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { self.shimmering = shimmering self.displayDecorations = displayDecorations self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary + self.audioLevel = audioLevel } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -452,6 +455,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private let containerNode: ContextControllerSourceNode + private var audioLevelView: VoiceBlobView? fileprivate let avatarNode: AvatarNode private let titleNode: TextNode private let labelNode: TextNode @@ -470,6 +474,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private var editableControlNode: ItemListEditableControlNode? private var reorderControlNode: ItemListEditableReorderControlNode? + private let audioLevelDisposable = MetaDisposable() + override public var canBeSelected: Bool { if self.editableControlNode != nil || self.disabledOverlayNode != nil { return false @@ -499,7 +505,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.containerNode = ContextControllerSourceNode() self.avatarNode = AvatarNode(font: avatarFont) - self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() + //self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -550,6 +556,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } } + deinit { + self.audioLevelDisposable.dispose() + } + override public func didLoad() { super.didLoad() @@ -1103,7 +1113,53 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) + let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + + let blobFrame = avatarFrame.insetBy(dx: -12.0, dy: -12.0) + + if let audioLevel = item.audioLevel { + strongSelf.audioLevelView?.frame = blobFrame + strongSelf.audioLevelDisposable.set((audioLevel + |> deliverOnMainQueue).start(next: { value in + guard let strongSelf = self else { + return + } + + if strongSelf.audioLevelView == nil { + let audioLevelView = VoiceBlobView( + frame: blobFrame, + maxLevel: 0.3, + smallBlobRange: (0, 0), + mediumBlobRange: (0.7, 0.8), + bigBlobRange: (0.8, 0.9) + ) + + let maskRect = CGRect(origin: .zero, size: blobFrame.size) + let playbackMaskLayer = CAShapeLayer() + playbackMaskLayer.frame = maskRect + playbackMaskLayer.fillRule = .evenOdd + let maskPath = UIBezierPath() + maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22)) + maskPath.append(UIBezierPath(rect: maskRect)) + playbackMaskLayer.path = maskPath.cgPath + audioLevelView.layer.mask = playbackMaskLayer + + audioLevelView.setColor(.green) + strongSelf.audioLevelView = audioLevelView + strongSelf.containerNode.view.insertSubview(audioLevelView, at: 0) + audioLevelView.startAnimating() + } + + strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0) + })) + } else if let audioLevelView = strongSelf.audioLevelView { + strongSelf.audioLevelView = nil + audioLevelView.removeFromSuperview() + + strongSelf.audioLevelDisposable.set(nil) + } + + transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling { strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift index 378c76240c..713b0ae6ca 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift @@ -179,10 +179,10 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa } } if let fetchedData = fetchedData { - precondition(fetchedData.count <= readCount) + assert(fetchedData.count <= readCount) fetchedData.withUnsafeBytes { bytes -> Void in precondition(bytes.baseAddress != nil) - memcpy(buffer, bytes.baseAddress, fetchedData.count) + memcpy(buffer, bytes.baseAddress, min(fetchedData.count, readCount)) } fetchedCount = Int32(fetchedData.count) context.readingOffset += Int(fetchedCount) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 635d25785f..dc4989ab65 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -787,6 +787,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-202219658] = { return Api.MessageAction.parse_messageActionContactSignUp($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) } + dict[254144570] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) } dict[1399245077] = { return Api.PhoneCall.parse_phoneCallEmpty($0) } dict[462375633] = { return Api.PhoneCall.parse_phoneCallWaiting($0) } dict[-2014659757] = { return Api.PhoneCall.parse_phoneCallRequested($0) } @@ -880,7 +881,7 @@ public struct Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index cf6dfdd91b..5a3022bcde 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -21479,6 +21479,7 @@ public extension Api { case messageActionContactSignUp case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) + case messageActionInviteToGroupCall(call: Api.InputGroupCall, userId: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -21666,6 +21667,13 @@ public extension Api { call.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(duration!, buffer: buffer, boxed: false)} break + case .messageActionInviteToGroupCall(let call, let userId): + if boxed { + buffer.appendInt32(254144570) + } + call.serialize(buffer, true) + serializeInt32(userId, buffer: buffer, boxed: false) + break } } @@ -21721,6 +21729,8 @@ public extension Api { return ("messageActionGeoProximityReached", [("fromId", fromId), ("toId", toId), ("distance", distance)]) case .messageActionGroupCall(let flags, let call, let duration): return ("messageActionGroupCall", [("flags", flags), ("call", call), ("duration", duration)]) + case .messageActionInviteToGroupCall(let call, let userId): + return ("messageActionInviteToGroupCall", [("call", call), ("userId", userId)]) } } @@ -22029,6 +22039,22 @@ public extension Api { return nil } } + public static func parse_messageActionInviteToGroupCall(_ reader: BufferReader) -> MessageAction? { + var _1: Api.InputGroupCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputGroupCall + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageAction.messageActionInviteToGroupCall(call: _1!, userId: _2!) + } + else { + return nil + } + } } public enum PhoneCall: TypeConstructorDescription { diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 0639295a6c..c096955a8a 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -7310,6 +7310,21 @@ public extension Api { }) } + public static func inviteToGroupCall(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-580284540) + call.serialize(buffer, true) + userId.serialize(buffer, true) + return (FunctionDescription(name: "phone.inviteToGroupCall", parameters: [("call", call), ("userId", userId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + public static func discardGroupCall(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() buffer.appendInt32(2054648117) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 5ad3be6624..06583f0b6c 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -27,7 +27,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private enum InternalState { case requesting case active(GroupCallInfo) - case estabilished(GroupCallInfo, String, [UInt32], [UInt32: PeerId]) + case estabilished(GroupCallInfo, String, UInt32, [UInt32], [UInt32: PeerId]) var callInfo: GroupCallInfo? { switch self { @@ -35,7 +35,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return nil case let .active(info): return info - case let .estabilished(info, _, _, _): + case let .estabilished(info, _, _, _, _): return info } } @@ -75,6 +75,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return self.audioOutputStatePromise.get() } + private let audioLevelsPipe = ValuePipe<[(PeerId, Float)]>() + public var audioLevels: Signal<[(PeerId, Float)], NoError> { + return self.audioLevelsPipe.signal() + } + private var audioLevelsDisposable = MetaDisposable() + private var audioSessionControl: ManagedAudioSessionControl? private var audioSessionDisposable: Disposable? private let audioSessionShouldBeActive = ValuePromise(false, ignoreRepeated: true) @@ -120,6 +126,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private let memberStatesDisposable = MetaDisposable() private let leaveDisposable = MetaDisposable() + private var checkCallDisposable: Disposable? + private var isCurrentlyConnecting: Bool? + init( accountContext: AccountContext, audioSession: ManagedAudioSession, @@ -224,7 +233,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } - if case let .estabilished(callInfo, _, _, _) = strongSelf.internalState { + if case let .estabilished(callInfo, _, _, _, _) = strongSelf.internalState { var addedSsrc: [UInt32] = [] var removedSsrc: [UInt32] = [] for (callId, peerId, ssrc, isAdded) in updates { @@ -259,6 +268,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.isMutedDisposable.dispose() self.memberStatesDisposable.dispose() self.networkStateDisposable.dispose() + self.checkCallDisposable?.dispose() + self.audioLevelsDisposable.dispose() } private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) { @@ -275,6 +286,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.audioSessionShouldBeActive.set(true) + switch previousInternalState { + case .requesting: + break + default: + if case .requesting = internalState { + self.isCurrentlyConnecting = nil + } + } + switch previousInternalState { case .active: break @@ -284,7 +304,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.callContext = callContext self.requestDisposable.set((callContext.joinPayload |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] joinPayload in + |> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in guard let strongSelf = self else { return } @@ -299,7 +319,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } if let clientParams = joinCallResult.callInfo.clientParams { - strongSelf.updateSessionState(internalState: .estabilished(joinCallResult.callInfo, clientParams, joinCallResult.ssrcs, joinCallResult.ssrcMapping), audioSessionControl: strongSelf.audioSessionControl) + strongSelf.updateSessionState(internalState: .estabilished(joinCallResult.callInfo, clientParams, ssrc, joinCallResult.ssrcs, joinCallResult.ssrcMapping), audioSessionControl: strongSelf.audioSessionControl) } })) })) @@ -324,7 +344,21 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { case .connected: mappedState = .connected } - strongSelf.stateValue.networkState = mappedState + if strongSelf.stateValue.networkState != mappedState { + strongSelf.stateValue.networkState = mappedState + } + + let isConnecting = mappedState == .connecting + + if strongSelf.isCurrentlyConnecting != isConnecting { + strongSelf.isCurrentlyConnecting = isConnecting + if isConnecting { + strongSelf.startCheckingCallIfNeeded() + } else { + strongSelf.checkCallDisposable?.dispose() + strongSelf.checkCallDisposable = nil + } + } })) self.memberStatesDisposable.set((callContext.memberStates @@ -343,6 +377,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } strongSelf.membersValue = result })) + + self.audioLevelsDisposable.set((callContext.audioLevels + |> deliverOnMainQueue).start(next: { [weak self] levels in + guard let strongSelf = self else { + return + } + var result: [(PeerId, Float)] = [] + for (ssrc, level) in levels { + if let peerId = strongSelf.ssrcMapping[ssrc] { + result.append((peerId, level)) + } + } + if !result.isEmpty { + strongSelf.audioLevelsPipe.putNext(result) + } + })) } } @@ -350,13 +400,47 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { case .estabilished: break default: - if case let .estabilished(_, clientParams, ssrcs, ssrcMapping) = internalState { + if case let .estabilished(_, clientParams, _, ssrcs, ssrcMapping) = internalState { self.ssrcMapping = ssrcMapping self.callContext?.setJoinResponse(payload: clientParams, ssrcs: ssrcs) + if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting { + self.startCheckingCallIfNeeded() + } } } } + private func startCheckingCallIfNeeded() { + if self.checkCallDisposable != nil { + return + } + if case let .estabilished(callInfo, _, ssrc, _, _) = self.internalState { + let checkSignal = checkGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, ssrc: Int32(bitPattern: ssrc)) + + self.checkCallDisposable = (( + checkSignal + |> castError(Bool.self) + |> delay(4.0, queue: .mainQueue()) + |> mapToSignal { result -> Signal in + if case .success = result { + return .fail(true) + } else { + return .single(true) + } + } + ) + |> restartIfError + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.checkCallDisposable = nil + strongSelf.requestCall() + }) + } + } + private func updateIsAudioSessionActive(_ value: Bool) { if self.isAudioSessionActive != value { self.isAudioSessionActive = value @@ -364,7 +448,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } public func leave() -> Signal { - if case let .estabilished(callInfo, _, _, _) = self.internalState { + if case let .estabilished(callInfo, _, _, _, _) = self.internalState { self.leaveDisposable.set((leaveGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash) |> deliverOnMainQueue).start(completed: { [weak self] in self?._canBeRemoved.set(.single(true)) @@ -402,7 +486,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } private func requestCall() { - self.internalState = .requesting + self.callContext?.stop() + self.callContext = nil + + self.updateSessionState(internalState: .requesting, audioSessionControl: self.audioSessionControl) enum CallError { case generic diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index fc98673e34..c625b1e63f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -127,7 +127,28 @@ public final class VoiceChatController: ViewController { } private final class Interaction { + private var audioLevels: [PeerId: ValuePipe] = [:] + init() { + } + + func getAudioLevel(_ peerId: PeerId) -> Signal? { + if let current = self.audioLevels[peerId] { + return current.signal() + } else { + let value = ValuePipe() + self.audioLevels[peerId] = value + return value.signal() + } + } + + func updateAudioLevels(levels: [(PeerId, Float)]) { + for (peerId, level) in levels { + if let pipe = self.audioLevels[peerId] { + pipe.putNext(level) + } + } + } } private struct PeerEntry: Comparable, Identifiable { @@ -173,7 +194,7 @@ public final class VoiceChatController: ViewController { //arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) }) }, removePeer: { id in //arguments.deleteIncludePeer(id) - }, noInsets: true) + }, noInsets: true, audioLevel: peer.id == context.account.peerId ? nil : interaction.getAudioLevel(peer.id)) } } @@ -225,6 +246,7 @@ public final class VoiceChatController: ViewController { private var audioOutputStateDisposable: Disposable? private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? + private var audioLevelsDisposable: Disposable? private var memberStatesDisposable: Disposable? private var itemInteraction: Interaction? @@ -347,12 +369,21 @@ public final class VoiceChatController: ViewController { self.audioOutputStateDisposable = (call.audioOutputState |> deliverOnMainQueue).start(next: { [weak self] state in - if let strongSelf = self { - strongSelf.audioOutputState = state - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - } + guard let strongSelf = self else { + return } + strongSelf.audioOutputState = state + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) + } + }) + + self.audioLevelsDisposable = (call.audioLevels + |> deliverOnMainQueue).start(next: { [weak self] levels in + guard let strongSelf = self else { + return + } + strongSelf.itemInteraction?.updateAudioLevels(levels: levels) }) self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside) @@ -370,6 +401,7 @@ public final class VoiceChatController: ViewController { self.callStateDisposable?.dispose() self.audioOutputStateDisposable?.dispose() self.memberStatesDisposable?.dispose() + self.audioLevelsDisposable?.dispose() } @objc private func leavePressed() { diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index f8a150310c..f8ff085d34 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -137,6 +137,44 @@ public func createGroupCall(account: Account, peerId: PeerId) -> Signal Signal { + return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), maxDate: maxDate, limit: limit)) + |> mapError { _ -> GetGroupCallParticipantsError in + return .generic + } + |> map { result -> GetGroupCallParticipantsResult in + var ssrcMapping: [UInt32: PeerId] = [:] + + switch result { + case let .groupParticipants(count, participants, users): + for participant in participants { + var peerId: PeerId? + var ssrc: UInt32? + switch participant { + case let .groupCallParticipant(flags, userId, date, source): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + ssrc = UInt32(bitPattern: source) + } + if let peerId = peerId, let ssrc = ssrc { + ssrcMapping[ssrc] = peerId + } + } + } + + return GetGroupCallParticipantsResult( + ssrcMapping: ssrcMapping + ) + } +} + public enum JoinGroupCallError { case generic } @@ -153,11 +191,17 @@ public func joinGroupCall(account: Account, callId: Int64, accessHash: Int64, jo return .generic } |> mapToSignal { updates -> Signal in - return account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) - |> mapError { _ -> JoinGroupCallError in - return .generic - } - |> mapToSignal { result -> Signal in + return combineLatest( + account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) + |> mapError { _ -> JoinGroupCallError in + return .generic + }, + getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, maxDate: 0, limit: 100) + |> mapError { _ -> JoinGroupCallError in + return .generic + } + ) + |> mapToSignal { result, participantsResult -> Signal in account.stateManager.addUpdates(updates) var maybeParsedCall: GroupCallInfo? @@ -180,7 +224,7 @@ public func joinGroupCall(account: Account, callId: Int64, accessHash: Int64, jo guard let _ = GroupCallInfo(call) else { return .fail(.generic) } - var ssrcMapping: [UInt32: PeerId] = [:] + var ssrcMapping: [UInt32: PeerId] = participantsResult.ssrcMapping for participant in participants { var peerId: PeerId? var ssrc: UInt32? @@ -233,3 +277,23 @@ public func stopGroupCall(account: Account, callId: Int64, accessHash: Int64) -> return .complete() } } + +public enum CheckGroupCallResult { + case success + case restart +} + +public func checkGroupCall(account: Account, callId: Int64, accessHash: Int64, ssrc: Int32) -> Signal { + return account.network.request(Api.functions.phone.checkGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), source: ssrc)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> map { result -> CheckGroupCallResult in + switch result { + case .boolTrue: + return .success + case .boolFalse: + return .restart + } + } +} diff --git a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift index 698fe5d4f8..7ca8b5af7b 100644 --- a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift @@ -188,7 +188,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionInviteToGroupCall: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)) diff --git a/submodules/TelegramCore/Sources/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/TelegramMediaAction.swift index eeb04af985..e0ee533e24 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/TelegramMediaAction.swift @@ -64,6 +64,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe case let .inputGroupCall(id, accessHash): return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, duration: duration)) } + case .messageActionInviteToGroupCall: + return nil } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 0cbe694529..f3b815faa3 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -210,6 +210,7 @@ swift_library( "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", "//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode", + "//submodules/AudioBlob:AudioBlob", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d4ca7f16d1..43c568ea24 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5979,11 +5979,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in - }, editMessageMedia: { [weak self] messageId, draw in - if let strongSelf = self { - strongSelf.controllerInteraction?.editMessageMedia(messageId, draw) - } - }, joinGroupCall: { [weak self] messageId in guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return } @@ -6011,6 +6006,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } + }, editMessageMedia: { [weak self] messageId, draw in + if let strongSelf = self { + strongSelf.controllerInteraction?.editMessageMedia(messageId, draw) + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) do { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index e894620c02..e51a148317 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -16,6 +16,7 @@ import SemanticStatusNode import FileMediaResourceStatus import CheckNode import MusicAlbumArtResources +import AudioBlob private struct FetchControls { let fetch: () -> Void diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index fbe9b3c273..72d8e1340a 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -207,7 +207,7 @@ final class ChatPanelInterfaceInteraction { scrollToTop: @escaping () -> Void, viewReplies: @escaping (MessageId?, ChatReplyThreadMessage) -> Void, activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void, - joinGroupCall: @escaping (MessageId) -> Void, + joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void, editMessageMedia: @escaping (MessageId, Bool) -> Void, statuses: ChatPanelInterfaceInteractionStatuses? ) { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index 3d77d119be..9398c9f4e9 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -132,8 +132,8 @@ final class ChatRecentActionsController: TelegramBaseController { }, scrollToTop: { }, viewReplies: { _, _ in }, activatePinnedListPreview: { _, _ in - }, editMessageMedia: { _, _ in }, joinGroupCall: { _ in + }, editMessageMedia: { _, _ in }, statuses: nil) self.navigationItem.titleView = self.titleView diff --git a/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift b/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift index 24283d889e..d476cb3509 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import LegacyComponents import AccountContext import ChatInterfaceState +import AudioBlob private let offsetThreshold: CGFloat = 10.0 private let dismissOffsetThreshold: CGFloat = 70.0 diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 9ada296a69..0edced3fa0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -438,8 +438,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, scrollToTop: { }, viewReplies: { _, _ in }, activatePinnedListPreview: { _, _ in - }, editMessageMedia: { _, _ in }, joinGroupCall: { _ in + }, editMessageMedia: { _, _ in }, statuses: nil) self.selectionPanel.interfaceInteraction = interfaceInteraction diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index f0c4336f96..72080355db 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -44,7 +44,7 @@ public final class OngoingGroupCallContext { var mainStreamAudioSsrc: UInt32? var otherSsrcs: [UInt32] = [] - let joinPayload = Promise() + let joinPayload = Promise<(String, UInt32)>() let networkState = ValuePromise(.connecting, ignoreRepeated: true) let isMuted = ValuePromise(true, ignoreRepeated: true) let memberStates = ValuePromise<[UInt32: MemberState]>([:], ignoreRepeated: true) @@ -105,7 +105,7 @@ public final class OngoingGroupCallContext { return } strongSelf.mainStreamAudioSsrc = ssrc - strongSelf.joinPayload.set(.single(payload)) + strongSelf.joinPayload.set(.single((payload, ssrc))) } }) } @@ -170,6 +170,10 @@ public final class OngoingGroupCallContext { } } + func stop() { + self.context.stop() + } + func setIsMuted(_ isMuted: Bool) { self.isMuted.set(isMuted) self.context.setIsMuted(isMuted) @@ -179,7 +183,7 @@ public final class OngoingGroupCallContext { private let queue = Queue() private let impl: QueueLocalObject - public var joinPayload: Signal { + public var joinPayload: Signal<(String, UInt32), NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in @@ -269,4 +273,10 @@ public final class OngoingGroupCallContext { impl.removeSsrcs(ssrcs: ssrcs) } } + + public func stop() { + self.impl.with { impl in + impl.stop() + } + } } diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index f48bd09e35..63d2ded655 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -160,6 +160,8 @@ typedef NS_ENUM(int32_t, GroupCallNetworkState) { - (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated; +- (void)stop; + - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; - (void)setJoinResponsePayload:(NSString * _Nonnull)payload; - (void)setSsrcs:(NSArray * _Nonnull)ssrcs; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 0e88cb7882..a38f9e4ed3 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -826,18 +826,26 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; networkStateUpdated(isConnected ? GroupCallNetworkStateConnected : GroupCallNetworkStateConnecting); }]; }, - .audioLevelsUpdated = [weakSelf, queue, audioLevelsUpdated](std::vector> const &levels) { + .audioLevelsUpdated = [audioLevelsUpdated](std::vector> const &levels) { NSMutableArray *result = [[NSMutableArray alloc] init]; for (auto &it : levels) { [result addObject:@(it.first)]; [result addObject:@(it.second)]; } + audioLevelsUpdated(result); } })); } return self; } +- (void)stop { + if (_instance) { + _instance->stop(); + _instance.reset(); + } +} + - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { if (_instance) { _instance->emitJoinPayload([completion](tgcalls::GroupJoinPayload payload) {