[WIP] Voice chats

This commit is contained in:
Ali 2020-11-20 22:32:45 +04:00
parent c14d93229c
commit c86ceafb5c
24 changed files with 372 additions and 49 deletions

View File

@ -198,6 +198,7 @@ public protocol PresentationGroupCall: class {
var canBeRemoved: Signal<Bool, NoError> { get }
var state: Signal<PresentationGroupCallState, NoError> { get }
var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get }
var audioLevels: Signal<[(PeerId, Float)], NoError> { get }
func leave() -> Signal<Bool, NoError>

View File

@ -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",
],
)

View File

@ -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

View File

@ -22,6 +22,7 @@ swift_library(
"//submodules/ContextUI:ContextUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AccountContext:AccountContext",
"//submodules/AudioBlob:AudioBlob",
],
visibility = [
"//visibility:public",

View File

@ -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<Float, NoError>?
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<Float, NoError>? = 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<Void, NoError>?, (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)

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 {

View File

@ -7310,6 +7310,21 @@ public extension Api {
})
}
public static func inviteToGroupCall(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
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<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(2054648117)

View File

@ -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<Bool>(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<Bool, Bool> 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<Bool, NoError> {
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

View File

@ -127,7 +127,28 @@ public final class VoiceChatController: ViewController {
}
private final class Interaction {
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
init() {
}
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError>? {
if let current = self.audioLevels[peerId] {
return current.signal()
} else {
let value = ValuePipe<Float>()
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() {

View File

@ -137,6 +137,44 @@ public func createGroupCall(account: Account, peerId: PeerId) -> Signal<GroupCal
}
}
public struct GetGroupCallParticipantsResult {
public var ssrcMapping: [UInt32: PeerId]
}
public enum GetGroupCallParticipantsError {
case generic
}
public func getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, maxDate: Int32, limit: Int32) -> Signal<GetGroupCallParticipantsResult, GetGroupCallParticipantsError> {
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<JoinGroupCallResult, JoinGroupCallError> in
return account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash)))
|> mapError { _ -> JoinGroupCallError in
return .generic
}
|> mapToSignal { result -> Signal<JoinGroupCallResult, JoinGroupCallError> 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<JoinGroupCallResult, JoinGroupCallError> 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<CheckGroupCallResult, NoError> {
return account.network.request(Api.functions.phone.checkGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), source: ssrc))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> map { result -> CheckGroupCallResult in
switch result {
case .boolTrue:
return .success
case .boolFalse:
return .restart
}
}
}

View File

@ -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))

View File

@ -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
}
}

View File

@ -210,6 +210,7 @@ swift_library(
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
"//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode",
"//submodules/AudioBlob:AudioBlob",
],
visibility = [
"//visibility:public",

View File

@ -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 {

View File

@ -16,6 +16,7 @@ import SemanticStatusNode
import FileMediaResourceStatus
import CheckNode
import MusicAlbumArtResources
import AudioBlob
private struct FetchControls {
let fetch: () -> Void

View File

@ -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?
) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -44,7 +44,7 @@ public final class OngoingGroupCallContext {
var mainStreamAudioSsrc: UInt32?
var otherSsrcs: [UInt32] = []
let joinPayload = Promise<String>()
let joinPayload = Promise<(String, UInt32)>()
let networkState = ValuePromise<NetworkState>(.connecting, ignoreRepeated: true)
let isMuted = ValuePromise<Bool>(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<Impl>
public var joinPayload: Signal<String, NoError> {
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()
}
}
}

View File

@ -160,6 +160,8 @@ typedef NS_ENUM(int32_t, GroupCallNetworkState) {
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated;
- (void)stop;
- (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion;
- (void)setJoinResponsePayload:(NSString * _Nonnull)payload;
- (void)setSsrcs:(NSArray<NSNumber *> * _Nonnull)ssrcs;

View File

@ -826,18 +826,26 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
networkStateUpdated(isConnected ? GroupCallNetworkStateConnected : GroupCallNetworkStateConnecting);
}];
},
.audioLevelsUpdated = [weakSelf, queue, audioLevelsUpdated](std::vector<std::pair<uint32_t, float>> const &levels) {
.audioLevelsUpdated = [audioLevelsUpdated](std::vector<std::pair<uint32_t, float>> 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) {