diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 3cff68fb62..d88b32285c 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -143,6 +143,9 @@ public protocol PresentationCall: AnyObject { var state: Signal { get } var audioLevel: Signal { get } + + var hasConference: Signal { get } + var conferenceCall: PresentationGroupCall? { get } var isMuted: Signal { get } @@ -164,7 +167,7 @@ public protocol PresentationCall: AnyObject { func setCurrentAudioOutput(_ output: AudioSessionOutput) func debugInfo() -> Signal<(String, String), NoError> - func createConferenceIfPossible() + func upgradeToConference(completion: @escaping (PresentationGroupCall) -> Void) -> Disposable func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) } diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 67cb22a211..9591a7142d 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -13,6 +13,7 @@ import AccountContext import TelegramNotices import AppBundle import TooltipUI +import CallScreen protocol CallControllerNodeProtocol: AnyObject { var isMuted: Bool { get set } @@ -41,193 +42,6 @@ protocol CallControllerNodeProtocol: AnyObject { } public final class CallController: ViewController { - public enum Call: Equatable { - case call(PresentationCall) - case groupCall(PresentationGroupCall) - - public static func ==(lhs: Call, rhs: Call) -> Bool { - switch lhs { - case let .call(lhsCall): - if case let .call(rhsCall) = rhs { - return lhsCall === rhsCall - } else { - return false - } - case let .groupCall(lhsGroupCall): - if case let .groupCall(rhsGroupCall) = rhs { - return lhsGroupCall === rhsGroupCall - } else { - return false - } - } - } - - public var context: AccountContext { - switch self { - case let .call(call): - return call.context - case let .groupCall(groupCall): - return groupCall.accountContext - } - } - - public var peerId: EnginePeer.Id? { - switch self { - case let .call(call): - return call.peerId - case let .groupCall(groupCall): - return groupCall.peerId - } - } - - public func requestVideo() { - switch self { - case let .call(call): - call.requestVideo() - case let .groupCall(groupCall): - groupCall.requestVideo() - } - } - - public func disableVideo() { - switch self { - case let .call(call): - call.disableVideo() - case let .groupCall(groupCall): - groupCall.disableVideo() - } - } - - public func disableScreencast() { - switch self { - case let .call(call): - (call as? PresentationCallImpl)?.disableScreencast() - case let .groupCall(groupCall): - groupCall.disableScreencast() - } - } - - public func switchVideoCamera() { - switch self { - case let .call(call): - call.switchVideoCamera() - case let .groupCall(groupCall): - groupCall.switchVideoCamera() - } - } - - public func toggleIsMuted() { - switch self { - case let .call(call): - call.toggleIsMuted() - case let .groupCall(groupCall): - groupCall.toggleIsMuted() - } - } - - public func setCurrentAudioOutput(_ output: AudioSessionOutput) { - switch self { - case let .call(call): - call.setCurrentAudioOutput(output) - case let .groupCall(groupCall): - groupCall.setCurrentAudioOutput(output) - } - } - - public var isMuted: Signal { - switch self { - case let .call(call): - return call.isMuted - case let .groupCall(groupCall): - return groupCall.isMuted - } - } - - public var audioLevel: Signal { - switch self { - case let .call(call): - return call.audioLevel - case let .groupCall(groupCall): - var audioLevelId: UInt32? - return groupCall.audioLevels |> map { audioLevels -> Float in - var result: Float = 0 - for item in audioLevels { - if let audioLevelId { - if item.1 == audioLevelId { - result = item.2 - break - } - } else { - if item.1 != 0 { - audioLevelId = item.1 - result = item.2 - break - } - } - } - - return result - } - } - } - - public var isOutgoing: Bool { - switch self { - case let .call(call): - return call.isOutgoing - case .groupCall: - return false - } - } - - public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) { - switch self { - case let .call(call): - call.makeOutgoingVideoView(completion: completion) - case let .groupCall(groupCall): - groupCall.makeOutgoingVideoView(requestClone: false, completion: { a, _ in - completion(a) - }) - } - } - - public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { - switch self { - case let .call(call): - return call.audioOutputState - case let .groupCall(groupCall): - return groupCall.audioOutputState - } - } - - public func debugInfo() -> Signal<(String, String), NoError> { - switch self { - case let .call(call): - return call.debugInfo() - case .groupCall: - return .single(("", "")) - } - } - - public func answer() { - switch self { - case let .call(call): - call.answer() - case .groupCall: - break - } - } - - public func hangUp() -> Signal { - switch self { - case let .call(call): - return call.hangUp() - case let .groupCall(groupCall): - return groupCall.leave(terminateIfPossible: false) - } - } - } - private var controllerNode: CallControllerNodeProtocol { return self.displayNode as! CallControllerNodeProtocol } @@ -242,7 +56,7 @@ public final class CallController: ViewController { private let sharedContext: SharedAccountContext private let account: Account - public let call: CallController.Call + public let call: PresentationCall private let easyDebugAccess: Bool private var presentationData: PresentationData @@ -268,7 +82,7 @@ public final class CallController: ViewController { public var onViewDidAppear: (() -> Void)? public var onViewDidDisappear: (() -> Void)? - public init(sharedContext: SharedAccountContext, account: Account, call: CallController.Call, easyDebugAccess: Bool) { + public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) { self.sharedContext = sharedContext self.account = account self.call = call @@ -293,84 +107,10 @@ public final class CallController: ViewController { self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) - switch call { - case let .call(call): - self.disposable = (call.state - |> deliverOnMainQueue).start(next: { [weak self] callState in - self?.callStateUpdated(callState) - }) - case let .groupCall(groupCall): - let accountPeerId = groupCall.account.peerId - let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = groupCall.members - |> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in - guard let members else { - return (nil, nil) - } - var local: String? - var remote: PresentationGroupCallRequestedVideo? - for participant in members.participants { - if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) { - if participant.peer.id == accountPeerId { - local = video.endpointId - } else { - if remote == nil { - remote = video - } - } - } - } - return (local, remote) - } - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs == rhs - }) - - var startTimestamp: Double? - self.disposable = (combineLatest(queue: .mainQueue(), - groupCall.state, - videoEndpoints - ) - |> deliverOnMainQueue).start(next: { [weak self] callState, videoEndpoints in - guard let self else { - return - } - let mappedState: PresentationCallState.State - switch callState.networkState { - case .connecting: - mappedState = .connecting(nil) - case .connected: - let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent() - startTimestamp = timestamp - mappedState = .active(timestamp, nil, Data()) - } - - var mappedLocalVideoState: PresentationCallState.VideoState = .inactive - var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive - - if let local = videoEndpoints.local { - mappedLocalVideoState = .active(isScreencast: false, endpointId: local) - } - if let remote = videoEndpoints.remote { - mappedRemoteVideoState = .active(endpointId: remote.endpointId) - } - - if case let .groupCall(groupCall) = self.call { - var requestedVideo: [PresentationGroupCallRequestedVideo] = [] - if let remote = videoEndpoints.remote { - requestedVideo.append(remote) - } - groupCall.setRequestedVideoList(items: requestedVideo) - } - - self.callStateUpdated(PresentationCallState( - state: mappedState, - videoState: mappedLocalVideoState, - remoteVideoState: mappedRemoteVideoState, - remoteAudioState: .active, - remoteBatteryLevel: .normal - )) - }) - } + self.disposable = (call.state + |> deliverOnMainQueue).start(next: { [weak self] callState in + self?.callStateUpdated(callState) + }) self.callMutedDisposable = (call.isMuted |> deliverOnMainQueue).start(next: { [weak self] value in @@ -605,11 +345,7 @@ public final class CallController: ViewController { } let callPeerView: Signal - if let peerId = self.call.peerId { - callPeerView = self.account.postbox.peerView(id: peerId) |> map(Optional.init) - } else { - callPeerView = .single(nil) - } + callPeerView = self.account.postbox.peerView(id: self.call.peerId) |> map(Optional.init) self.peerDisposable = (combineLatest(queue: .mainQueue(), self.account.postbox.peerView(id: self.account.peerId) |> take(1), @@ -659,6 +395,26 @@ public final class CallController: ViewController { self.onViewDidDisappear?() } + final class AnimateOutToGroupChat { + let incomingPeerId: EnginePeer.Id + let incomingVideoLayer: CALayer? + let incomingVideoPlaceholder: VideoSource.Output? + + init( + incomingPeerId: EnginePeer.Id, + incomingVideoLayer: CALayer?, + incomingVideoPlaceholder: VideoSource.Output? + ) { + self.incomingPeerId = incomingPeerId + self.incomingVideoLayer = incomingVideoLayer + self.incomingVideoPlaceholder = incomingVideoPlaceholder + } + } + + func animateOutToGroupChat(completion: @escaping () -> Void) -> AnimateOutToGroupChat? { + return (self.controllerNode as? CallControllerNodeV2)?.animateOutToGroupChat(completion: completion) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -674,7 +430,17 @@ public final class CallController: ViewController { }) } + public func dismissWithoutAnimation() { + self.presentingViewController?.dismiss(animated: false, completion: nil) + } + private func conferenceAddParticipant() { + if "".isEmpty { + let _ = self.call.upgradeToConference(completion: { _ in + }) + return + } + let controller = self.call.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( context: self.call.context, filter: [.onlyWriteable], @@ -690,17 +456,13 @@ public final class CallController: ViewController { guard let self else { return } - guard case let .call(call) = self.call else { - return - } - guard let call = call as? PresentationCallImpl else { + guard let call = self.call as? PresentationCallImpl else { return } let _ = call.requestAddToConference(peerId: peer.id) } - self.dismiss() - (self.call.context.sharedContext.mainWindow?.viewController as? NavigationController)?.pushViewController(controller) + self.present(controller, in: .current) } @objc private func backPressed() { diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index eb1d460a5d..8eff36aac0 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -32,7 +32,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private let account: Account private let presentationData: PresentationData private let statusBar: StatusBar - private let call: CallController.Call + private let call: PresentationCall private let containerView: UIView private let callScreen: PrivateCallScreen @@ -91,7 +91,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, easyDebugAccess: Bool, - call: CallController.Call + call: PresentationCall ) { self.sharedContext = sharedContext self.account = account @@ -131,13 +131,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } - #if DEBUG - if self.sharedContext.immediateExperimentalUISettings.conferenceCalls { - self.conferenceAddParticipant?() - return - } - #endif - self.call.toggleIsMuted() } self.callScreen.endCallAction = { [weak self] in @@ -321,11 +314,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP case .active: switch callState.videoState { case .active(let isScreencast, _), .paused(let isScreencast, _): - if isScreencast { - self.call.disableScreencast() - } else { - self.call.disableVideo() - } + let _ = isScreencast + self.call.disableVideo() default: DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: self.presentationData, present: { [weak self] c, a in if let strongSelf = self { @@ -501,22 +491,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.remoteVideo = nil default: switch callState.videoState { - case .active(let isScreencast, let endpointId), .paused(let isScreencast, let endpointId): + case .active(let isScreencast, _), .paused(let isScreencast, _): if isScreencast { self.localVideo = nil } else { if self.localVideo == nil { - switch self.call { - case let .call(call): - if let call = call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: false) { - self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - case let .groupCall(groupCall): - if let groupCall = groupCall as? PresentationGroupCallImpl { - if let videoStreamSignal = groupCall.video(endpointId: endpointId) { - self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - } + if let call = self.call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: false) { + self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) } } } @@ -525,19 +506,10 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } switch callState.remoteVideoState { - case .active(let endpointId), .paused(let endpointId): + case .active, .paused: if self.remoteVideo == nil { - switch self.call { - case let .call(call): - if let call = call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: true) { - self.remoteVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - case let .groupCall(groupCall): - if let groupCall = groupCall as? PresentationGroupCallImpl { - if let videoStreamSignal = groupCall.video(endpointId: endpointId) { - self.remoteVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) - } - } + if let call = self.call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: true) { + self.remoteVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal) } } case .inactive: @@ -710,6 +682,17 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } } + func animateOutToGroupChat(completion: @escaping () -> Void) -> CallController.AnimateOutToGroupChat { + self.callScreen.animateOutToGroupChat(completion: completion) + + let takenIncomingVideoLayer = self.callScreen.takeIncomingVideoLayer() + return CallController.AnimateOutToGroupChat( + incomingPeerId: self.call.peerId, + incomingVideoLayer: takenIncomingVideoLayer?.0, + incomingVideoPlaceholder: takenIncomingVideoLayer?.1 + ) + } + func expandFromPipIfPossible() { } diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 95003c0a57..04a82610a5 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -194,7 +194,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { private let audioLevelDisposable = MetaDisposable() private let stateDisposable = MetaDisposable() - private var didSetupData = false + private weak var didSetupDataForCall: AnyObject? private var currentSize: CGSize? private var currentContent: Content? @@ -277,8 +277,16 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { let wasEmpty = (self.titleNode.attributedText?.string ?? "").isEmpty - if !self.didSetupData { - self.didSetupData = true + let setupDataForCall: AnyObject? + switch content { + case let .call(_, _, call): + setupDataForCall = call + case let .groupCall(_, _, call): + setupDataForCall = call + } + + if self.didSetupDataForCall !== setupDataForCall { + self.didSetupDataForCall = setupDataForCall switch content { case let .call(sharedContext, account, call): self.presentationData = sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 8eea2892dd..8246e775f7 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -131,20 +131,33 @@ public final class PresentationCallImpl: PresentationCall { private let screencastAudioDataDisposable = MetaDisposable() private let screencastStateDisposable = MetaDisposable() - private var conferenceCall: PresentationGroupCallImpl? + private var conferenceCallImpl: PresentationGroupCallImpl? + public var conferenceCall: PresentationGroupCall? { + if !self.hasConferenceValue { + return nil + } + + return self.conferenceCallImpl + } private var conferenceCallDisposable: Disposable? + private var upgradedToConferenceCompletions = Bag<(PresentationGroupCall) -> Void>() + + private var waitForConferenceCallReadyDisposable: Disposable? + private let hasConferencePromise = ValuePromise(false) + private var hasConferenceValue: Bool = false { + didSet { + if self.hasConferenceValue != oldValue { + self.hasConferencePromise.set(self.hasConferenceValue) + } + } + } + public var hasConference: Signal { + return self.hasConferencePromise.get() + } private var localVideoEndpointId: String? private var remoteVideoEndpointId: String? - private var conferenceSignalingDataDisposable: Disposable? - private var conferenceIsConnected: Bool = false - private var notifyConferenceIsConnectedTimer: Foundation.Timer? - - private var remoteConferenceIsConnectedTimestamp: Double? - private let remoteConferenceIsConnected = ValuePromise(false, ignoreRepeated: true) - private var remoteConferenceIsConnectedTimer: Foundation.Timer? - init( context: AccountContext, audioSession: ManagedAudioSession, @@ -340,9 +353,7 @@ public final class PresentationCallImpl: PresentationCall { self.ongoingContextStateDisposable?.dispose() self.ongoingContextIsFailedDisposable?.dispose() self.ongoingContextIsDroppedDisposable?.dispose() - self.notifyConferenceIsConnectedTimer?.invalidate() - self.conferenceSignalingDataDisposable?.dispose() - self.remoteConferenceIsConnectedTimer?.invalidate() + self.waitForConferenceCallReadyDisposable?.dispose() if let dropCallKitCallTimer = self.dropCallKitCallTimer { dropCallKitCallTimer.invalidate() @@ -540,13 +551,13 @@ public final class PresentationCallImpl: PresentationCall { self.callWasActive = true var isConference = false - if case let .active(_, _, _, _, _, version, _, _, _) = sessionState.state { - isConference = version == "13.0.0" + if case let .active(_, _, _, _, _, _, _, _, conferenceCall) = sessionState.state { + isConference = conferenceCall != nil } else if case .switchedToConference = sessionState.state { isConference = true } - if let callContextState = callContextState { + if let callContextState = callContextState, !isConference { switch callContextState.state { case .initializing: presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) @@ -593,221 +604,290 @@ public final class PresentationCallImpl: PresentationCall { if let (key, keyVisualHash, conferenceCall) = conferenceCallData { if self.conferenceCallDisposable == nil { - presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + self.conferenceCallDisposable = EmptyDisposable + + self.ongoingContextStateDisposable?.dispose() + self.ongoingContextStateDisposable = nil + self.ongoingContext?.stop(debugLogValue: Promise()) + self.ongoingContext = nil - self.conferenceCallDisposable = (self.context.engine.calls.getCurrentGroupCall(callId: conferenceCall.id, accessHash: conferenceCall.accessHash) - |> delay(sessionState.isOutgoing ? 0.0 : 2.0, queue: .mainQueue()) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let self, let result else { + let conferenceCall = PresentationGroupCallImpl( + accountContext: self.context, + audioSession: self.audioSession, + callKitIntegration: self.callKitIntegration, + getDeviceAccessData: self.getDeviceAccessData, + initialCall: EngineGroupCallDescription( + id: conferenceCall.id, + accessHash: conferenceCall.accessHash, + title: nil, + scheduleTimestamp: nil, + subscribedToScheduled: false, + isStream: false + ), + internalId: CallSessionInternalId(), + peerId: nil, + isChannel: false, + invite: nil, + joinAsPeerId: nil, + isStream: false, + encryptionKey: (key, 1), + conferenceFromCallId: conferenceFromCallId, + isConference: true, + sharedAudioDevice: self.sharedAudioDevice + ) + self.conferenceCallImpl = conferenceCall + + conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted) + if let videoCapturer = self.videoCapturer { + conferenceCall.requestVideo(capturer: videoCapturer) + } + + let accountPeerId = conferenceCall.account.peerId + let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = conferenceCall.members + |> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in + guard let members else { + return (nil, nil) + } + var local: String? + var remote: PresentationGroupCallRequestedVideo? + for participant in members.participants { + if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) { + if participant.peer.id == accountPeerId { + local = video.endpointId + } else { + if remote == nil { + remote = video + } + } + } + } + return (local, remote) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) + + var startTimestamp: Double? + self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(), + conferenceCall.state, + videoEndpoints, + conferenceCall.signalBars, + conferenceCall.isFailed + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints, signalBars, isFailed in + guard let self else { return } - let conferenceCall = PresentationGroupCallImpl( - accountContext: self.context, - audioSession: self.audioSession, - callKitIntegration: self.callKitIntegration, - getDeviceAccessData: self.getDeviceAccessData, - initialCall: EngineGroupCallDescription( - id: result.info.id, - accessHash: result.info.accessHash, - title: nil, - scheduleTimestamp: nil, - subscribedToScheduled: false, - isStream: false - ), - internalId: CallSessionInternalId(), - peerId: nil, - isChannel: false, - invite: nil, - joinAsPeerId: nil, - isStream: false, - encryptionKey: (key, 1), - conferenceFromCallId: conferenceFromCallId, - isConference: true, - sharedAudioDevice: self.sharedAudioDevice - ) - self.conferenceCall = conferenceCall + var mappedLocalVideoState: PresentationCallState.VideoState = .inactive + var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive - conferenceCall.setIsMuted(action: .muted(isPushToTalkActive: !self.isMutedValue)) + if let local = videoEndpoints.local { + mappedLocalVideoState = .active(isScreencast: false, endpointId: local) + } + if let remote = videoEndpoints.remote { + mappedRemoteVideoState = .active(endpointId: remote.endpointId) + } - let accountPeerId = conferenceCall.account.peerId - let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = conferenceCall.members - |> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in - guard let members else { - return (nil, nil) + self.localVideoEndpointId = videoEndpoints.local + self.remoteVideoEndpointId = videoEndpoints.remote?.endpointId + + if let conferenceCall = self.conferenceCall { + var requestedVideo: [PresentationGroupCallRequestedVideo] = [] + if let remote = videoEndpoints.remote { + requestedVideo.append(remote) } - var local: String? - var remote: PresentationGroupCallRequestedVideo? - for participant in members.participants { - if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) { - if participant.peer.id == accountPeerId { - local = video.endpointId - } else { - if remote == nil { - remote = video + conferenceCall.setRequestedVideoList(items: requestedVideo) + } + + let mappedState: PresentationCallState.State + if isFailed { + mappedState = .terminating(.error(.disconnected)) + } else { + switch callState.networkState { + case .connecting: + mappedState = .connecting(keyVisualHash) + case .connected: + let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent() + startTimestamp = timestamp + mappedState = .active(timestamp, signalBars, keyVisualHash) + } + } + + if !self.didDropCall && !self.droppedCall { + /*let presentationState = PresentationCallState( + state: mappedState, + videoState: mappedLocalVideoState, + remoteVideoState: mappedRemoteVideoState, + remoteAudioState: .active, + remoteBatteryLevel: .normal + )*/ + let _ = mappedState + + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + + mappedLocalVideoState = .inactive + mappedRemoteVideoState = .inactive + if self.videoCapturer != nil { + mappedLocalVideoState = .active(isScreencast: false, endpointId: "local") + } + + if let callContextState = self.callContextState { + switch callContextState.remoteVideoState { + case .active, .paused: + mappedRemoteVideoState = .active(endpointId: "temp-\(self.peerId.toInt64())") + case .inactive: + break + } + } + + let presentationState = PresentationCallState( + state: .active(timestamp, signalBars, keyVisualHash), + videoState: mappedLocalVideoState, + remoteVideoState: mappedRemoteVideoState, + remoteAudioState: .active, + remoteBatteryLevel: .normal + ) + self.statePromise.set(presentationState) + self.updateTone(presentationState, callContextState: nil, previous: nil) + } + }) + + self.ongoingContextIsFailedDisposable = (conferenceCall.isFailed + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + if !self.didDropCall { + self.didDropCall = true + self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) + } + }) + + self.ongoingContextIsDroppedDisposable = (conferenceCall.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + if !self.didDropCall { + self.didDropCall = true + self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil)) + } + }) + + var audioLevelId: UInt32? + let audioLevel = conferenceCall.audioLevels |> map { audioLevels -> Float in + var result: Float = 0 + for item in audioLevels { + if let audioLevelId { + if item.1 == audioLevelId { + result = item.2 + break + } + } else { + if item.1 != 0 { + audioLevelId = item.1 + result = item.2 + break + } + } + } + + return result + } + + self.audioLevelDisposable = (audioLevel + |> deliverOnMainQueue).start(next: { [weak self] level in + if let strongSelf = self { + strongSelf.audioLevelPromise.set(level) + } + }) + + let upgradedToConferenceCompletions = self.upgradedToConferenceCompletions.copyItems() + self.upgradedToConferenceCompletions.removeAll() + for f in upgradedToConferenceCompletions { + f(conferenceCall) + } + + let waitForLocalVideo = self.videoCapturer != nil + + let waitForRemotePeerId: EnginePeer.Id? = self.peerId + var waitForRemoteVideo: EnginePeer.Id? + if let callContextState = self.callContextState { + switch callContextState.remoteVideoState { + case .active, .paused: + waitForRemoteVideo = self.peerId + case .inactive: + break + } + } + + self.waitForConferenceCallReadyDisposable?.dispose() + self.waitForConferenceCallReadyDisposable = (combineLatest(queue: .mainQueue(), + conferenceCall.state, + conferenceCall.members + ) + |> filter { state, members in + if state.networkState != .connected { + return false + } + if let waitForRemotePeerId { + var found = false + if let members { + for participant in members.participants { + if participant.peer.id == waitForRemotePeerId { + found = true + break + } + } + } + if !found { + return false + } + } + if waitForLocalVideo { + if let members { + for participant in members.participants { + if participant.peer.id == state.myPeerId { + if participant.videoDescription == nil { + return false } } } } - return (local, remote) } - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs == rhs - }) - - let remoteIsConnectedAggregated = combineLatest(queue: .mainQueue(), - self.remoteConferenceIsConnected.get(), - conferenceCall.hasActiveIncomingData - ) - |> map { remoteConferenceIsConnected, hasActiveIncomingData -> Bool in - return remoteConferenceIsConnected || hasActiveIncomingData - } - |> distinctUntilChanged - - var startTimestamp: Double? - self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(), - conferenceCall.state, - videoEndpoints, - conferenceCall.signalBars, - conferenceCall.isFailed, - remoteIsConnectedAggregated - ) - |> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints, signalBars, isFailed, remoteIsConnectedAggregated in - guard let self else { - return - } - - var mappedLocalVideoState: PresentationCallState.VideoState = .inactive - var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive - - if let local = videoEndpoints.local { - mappedLocalVideoState = .active(isScreencast: false, endpointId: local) - } - if let remote = videoEndpoints.remote { - mappedRemoteVideoState = .active(endpointId: remote.endpointId) - } - - self.localVideoEndpointId = videoEndpoints.local - self.remoteVideoEndpointId = videoEndpoints.remote?.endpointId - - if let conferenceCall = self.conferenceCall { - var requestedVideo: [PresentationGroupCallRequestedVideo] = [] - if let remote = videoEndpoints.remote { - requestedVideo.append(remote) - } - conferenceCall.setRequestedVideoList(items: requestedVideo) - } - - var isConnected = false - let mappedState: PresentationCallState.State - if isFailed { - mappedState = .terminating(.error(.disconnected)) - } else { - switch callState.networkState { - case .connecting: - mappedState = .connecting(keyVisualHash) - case .connected: - isConnected = true - if remoteIsConnectedAggregated { - let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent() - startTimestamp = timestamp - mappedState = .active(timestamp, signalBars, keyVisualHash) - } else { - mappedState = .connecting(keyVisualHash) + if let waitForRemoteVideo { + if let members { + for participant in members.participants { + if participant.peer.id == waitForRemoteVideo { + if participant.videoDescription == nil { + return false + } } } } - - self.updateConferenceIsConnected(isConnected: isConnected) - - if !self.didDropCall && !self.droppedCall { - let presentationState = PresentationCallState( - state: mappedState, - videoState: mappedLocalVideoState, - remoteVideoState: mappedRemoteVideoState, - remoteAudioState: .active, - remoteBatteryLevel: .normal - ) - self.statePromise.set(presentationState) - self.updateTone(presentationState, callContextState: nil, previous: nil) - } - }) - - self.ongoingContextIsFailedDisposable = (conferenceCall.isFailed - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] _ in - guard let self else { - return - } - if !self.didDropCall { - self.didDropCall = true - self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) - } - }) - - self.ongoingContextIsDroppedDisposable = (conferenceCall.canBeRemoved - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] _ in - guard let self else { - return - } - if !self.didDropCall { - self.didDropCall = true - self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil)) - } - }) - - var audioLevelId: UInt32? - let audioLevel = conferenceCall.audioLevels |> map { audioLevels -> Float in - var result: Float = 0 - for item in audioLevels { - if let audioLevelId { - if item.1 == audioLevelId { - result = item.2 - break - } - } else { - if item.1 != 0 { - audioLevelId = item.1 - result = item.2 - break - } - } - } - - return result } - - self.audioLevelDisposable = (audioLevel - |> deliverOnMainQueue).start(next: { [weak self] level in - if let strongSelf = self { - strongSelf.audioLevelPromise.set(level) - } - }) - - let localIsConnected = conferenceCall.state - |> map { state -> Bool in - switch state.networkState { - case .connected: - return true - default: - return false - } + return true + } + |> map { _, _ -> Void in + return Void() + } + |> take(1) + |> timeout(10.0, queue: .mainQueue(), alternate: .single(Void()))).start(next: { [weak self] _ in + guard let self else { + return } - |> distinctUntilChanged - - let bothLocalAndRemoteConnected = combineLatest(queue: .mainQueue(), - localIsConnected, - remoteIsConnectedAggregated - ) - |> map { localIsConnected, remoteIsConnectedAggregated -> Bool in - return localIsConnected && remoteIsConnectedAggregated - } - |> distinctUntilChanged - - conferenceCall.internal_isRemoteConnected.set(bothLocalAndRemoteConnected) + self.hasConferenceValue = true }) } } @@ -817,26 +897,10 @@ public final class PresentationCallImpl: PresentationCall { if let _ = audioSessionControl { self.audioSessionShouldBeActive.set(true) } - case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall): - if conferenceCall == nil, version == "13.0.0" { - self.createConferenceIfPossible() - } - + case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, _): self.audioSessionShouldBeActive.set(true) - if version == "13.0.0" && self.conferenceSignalingDataDisposable == nil { - self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in - Queue.mainQueue().async { - guard let self else { - return - } - - self.processConferenceSignalingData(dataList: dataList) - } - }) - } - - if version == "13.0.0" || conferenceCallData != nil { + if conferenceCallData != nil { if sessionState.isOutgoing { self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) } @@ -910,7 +974,7 @@ public final class PresentationCallImpl: PresentationCall { if wasActive { let debugLogValue = Promise() self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue) - let _ = self.conferenceCall?.leave(terminateIfPossible: false).start() + let _ = self.conferenceCallImpl?.leave(terminateIfPossible: false).start() } case .dropping: break @@ -918,7 +982,7 @@ public final class PresentationCallImpl: PresentationCall { self.audioSessionShouldBeActive.set(false) if wasActive { let debugLogValue = Promise() - if let conferenceCall = self.conferenceCall { + if let conferenceCall = self.conferenceCallImpl { debugLogValue.set(conferenceCall.debugLog.get()) let _ = conferenceCall.leave(terminateIfPossible: false).start() } else { @@ -1064,88 +1128,6 @@ public final class PresentationCallImpl: PresentationCall { } } - private func updateConferenceIsConnected(isConnected: Bool) { - if self.conferenceIsConnected != isConnected { - self.conferenceIsConnected = isConnected - self.sendConferenceIsConnectedState() - } - - if self.notifyConferenceIsConnectedTimer == nil { - self.notifyConferenceIsConnectedTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in - guard let self else { - return - } - self.sendConferenceIsConnectedState() - }) - } - } - - private func sendConferenceIsConnectedState() { - self.sendConferenceSignalingMessage(dict: ["_$": "s", "c": self.conferenceIsConnected]) - } - - private func processConferenceSignalingData(dataList: [Data]) { - for data in dataList { - if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - self.processConferenceSignalingMessage(dict: dict) - } - } - } - - private func processConferenceSignalingMessage(dict: [String: Any]) { - if let type = dict["_$"] as? String { - switch type { - case "s": - let isConnected = dict["c"] as? Bool ?? false - self.remoteConferenceIsConnected.set(isConnected) - - if isConnected { - self.remoteConferenceIsConnectedTimestamp = CFAbsoluteTimeGetCurrent() - } - - if self.remoteConferenceIsConnectedTimer == nil && isConnected { - self.remoteConferenceIsConnectedTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in - guard let self else { - return - } - let timestamp = CFAbsoluteTimeGetCurrent() - if let remoteConferenceIsConnectedTimestamp = self.remoteConferenceIsConnectedTimestamp { - if remoteConferenceIsConnectedTimestamp + 4.0 < timestamp { - self.remoteConferenceIsConnected.set(false) - } - - if remoteConferenceIsConnectedTimestamp + 10.0 < timestamp { - if !self.didDropCall { - self.didDropCall = true - - let presentationState = PresentationCallState( - state: .terminating(.error(.disconnected)), - videoState: .inactive, - remoteVideoState: .inactive, - remoteAudioState: .active, - remoteBatteryLevel: .normal - ) - self.statePromise.set(presentationState) - self.updateTone(presentationState, callContextState: nil, previous: nil) - - self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) - } - } - } - }) - } - default: - break - } - } - } - - private func sendConferenceSignalingMessage(dict: [String: Any]) { - if let data = try? JSONSerialization.data(withJSONObject: dict) { - self.context.account.callSessionManager.sendSignalingData(internalId: self.internalId, data: data) - } - } - private func updateIsAudioSessionActive(_ value: Bool) { if self.isAudioSessionActive != value { self.isAudioSessionActive = value @@ -1202,7 +1184,7 @@ public final class PresentationCallImpl: PresentationCall { public func hangUp() -> Signal { let debugLogValue = Promise() self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get()) - if let conferenceCall = self.conferenceCall { + if let conferenceCall = self.conferenceCallImpl { debugLogValue.set(conferenceCall.debugLog.get()) let _ = conferenceCall.leave(terminateIfPossible: false).start() } else { @@ -1215,7 +1197,7 @@ public final class PresentationCallImpl: PresentationCall { public func rejectBusy() { self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil)) let debugLog = Promise() - if let conferenceCall = self.conferenceCall { + if let conferenceCall = self.conferenceCallImpl { debugLog.set(conferenceCall.debugLog.get()) let _ = conferenceCall.leave(terminateIfPossible: false).start() } else { @@ -1231,7 +1213,6 @@ public final class PresentationCallImpl: PresentationCall { self.isMutedValue = value self.isMutedPromise.set(self.isMutedValue) self.ongoingContext?.setIsMuted(self.isMutedValue) - self.conferenceCall?.setIsMuted(action: .muted(isPushToTalkActive: !self.isMutedValue)) } public func requestVideo() { @@ -1242,7 +1223,7 @@ public final class PresentationCallImpl: PresentationCall { if let videoCapturer = self.videoCapturer { if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(videoCapturer) - } else if let conferenceCall = self.conferenceCall { + } else if let conferenceCall = self.conferenceCallImpl { conferenceCall.requestVideo(capturer: videoCapturer) } } @@ -1258,7 +1239,7 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer = nil if let ongoingContext = self.ongoingContext { ongoingContext.disableVideo() - } else if let conferenceCall = self.conferenceCall { + } else if let conferenceCall = self.conferenceCallImpl { conferenceCall.disableVideo() } } @@ -1308,7 +1289,7 @@ public final class PresentationCallImpl: PresentationCall { self.isScreencastActive = true if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(screencastCapturer) - } else if let conferenceCall = self.conferenceCall { + } else if let conferenceCall = self.conferenceCallImpl { conferenceCall.requestVideo(capturer: screencastCapturer) } } @@ -1321,7 +1302,7 @@ public final class PresentationCallImpl: PresentationCall { } self.isScreencastActive = false self.ongoingContext?.disableVideo() - self.conferenceCall?.disableVideo() + self.conferenceCallImpl?.disableVideo() if reset { self.resetScreencastContext() } @@ -1332,6 +1313,25 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer?.setIsVideoEnabled(!isPaused) } + public func upgradeToConference(completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { + if let conferenceCall = self.conferenceCall { + completion(conferenceCall) + return EmptyDisposable + } + + let index = self.upgradedToConferenceCompletions.add(completion) + self.callSessionManager.createConferenceIfNecessary(internalId: self.internalId) + + return ActionDisposable { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + self.upgradedToConferenceCompletions.remove(index) + } + } + } + public func requestAddToConference(peerId: EnginePeer.Id) -> Disposable { var conferenceCall: (conference: GroupCallReference, encryptionKey: Data)? if let sessionState = self.sessionState { @@ -1388,7 +1388,7 @@ public final class PresentationCallImpl: PresentationCall { if isIncoming { if let ongoingContext = self.ongoingContext { return ongoingContext.video(isIncoming: isIncoming) - } else if let conferenceCall = self.conferenceCall, let remoteVideoEndpointId = self.remoteVideoEndpointId { + } else if let conferenceCall = self.conferenceCallImpl, let remoteVideoEndpointId = self.remoteVideoEndpointId { return conferenceCall.video(endpointId: remoteVideoEndpointId) } else { return nil @@ -1400,10 +1400,6 @@ public final class PresentationCallImpl: PresentationCall { } } - public func createConferenceIfPossible() { - self.callSessionManager.createConferenceIfNecessary(internalId: self.internalId) - } - public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) { if self.videoCapturer == nil { let videoCapturer = OngoingCallVideoCapturer() diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 4ef10924e5..74d10bbcf9 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1887,6 +1887,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels) self.connectPendingVideoSubscribers() + if let videoCapturer = self.videoCapturer { + genericCallContext.requestVideo(videoCapturer) + } + if case let .call(callContext) = genericCallContext { var lastTimestamp: Double? self.hasActiveIncomingDataDisposable?.dispose() @@ -2336,6 +2340,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { peerView = .single(nil) } + self.updateLocalVideoState() + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), participantsContext.state, participantsContext.activeSpeakers, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index c3eea4b4eb..fd36252698 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -191,6 +191,7 @@ final class VideoChatParticipantVideoComponent: Component { private let pinchContainerNode: PinchSourceContainerNode private let extractedContainerView: ContextExtractedContentContainingView private var videoSource: AdaptedCallVideoSource? + private var videoPlaceholder: VideoSource.Output? private var videoDisposable: Disposable? private var videoBackgroundLayer: SimpleLayer? private var videoLayer: PrivateCallVideoLayer? @@ -263,6 +264,11 @@ final class VideoChatParticipantVideoComponent: Component { } } + func updatePlaceholder(placeholder: VideoSource.Output) { + self.videoPlaceholder = placeholder + self.componentState?.updated(transition: .immediate, isLocal: true) + } + func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -456,6 +462,46 @@ final class VideoChatParticipantVideoComponent: Component { videoBackgroundLayer.isHidden = true } + let videoUpdated: () -> Void = { [weak self] in + guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else { + return + } + + var videoOutput = videoSource.currentOutput + var isPlaceholder = false + if videoOutput == nil { + isPlaceholder = true + videoOutput = self.videoPlaceholder + } else { + self.videoPlaceholder = nil + } + + videoLayer.video = videoOutput + + if let videoOutput { + let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) + if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { + self.awaitingFirstVideoFrameForUnpause = false + + self.videoSpec = videoSpec + if !self.isUpdating { + var transition: ComponentTransition = .immediate + if !isPlaceholder { + transition = transition.withUserData(AnimationHint(kind: .videoAvailabilityChanged)) + } + self.componentState?.updated(transition: transition, isLocal: true) + } + } + } else { + if self.videoSpec != nil { + self.videoSpec = nil + if !self.isUpdating { + self.componentState?.updated(transition: .immediate, isLocal: true) + } + } + } + } + let videoLayer: PrivateCallVideoLayer if let current = self.videoLayer { videoLayer = current @@ -473,36 +519,16 @@ final class VideoChatParticipantVideoComponent: Component { self.videoSource = videoSource self.videoDisposable?.dispose() - self.videoDisposable = videoSource.addOnUpdated { [weak self] in - guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else { - return - } - - let videoOutput = videoSource.currentOutput - videoLayer.video = videoOutput - - if let videoOutput { - let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) - if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { - self.awaitingFirstVideoFrameForUnpause = false - - self.videoSpec = videoSpec - if !self.isUpdating { - self.componentState?.updated(transition: ComponentTransition.immediate.withUserData(AnimationHint(kind: .videoAvailabilityChanged)), isLocal: true) - } - } - } else { - if self.videoSpec != nil { - self.videoSpec = nil - if !self.isUpdating { - self.componentState?.updated(transition: .immediate, isLocal: true) - } - } - } + self.videoDisposable = videoSource.addOnUpdated { + videoUpdated() } } } + if let _ = self.videoPlaceholder, videoLayer.video == nil { + videoUpdated() + } + transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) if let videoSpec = self.videoSpec { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 7733798179..85cf39dffc 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -11,6 +11,7 @@ import MultilineTextComponent import TelegramPresentationData import PeerListItemComponent import ContextUI +import CallScreen final class VideoChatParticipantsComponent: Component { struct Layout: Equatable { @@ -1616,6 +1617,27 @@ final class VideoChatParticipantsComponent: Component { } } + func itemFrame(peerId: EnginePeer.Id, isPresentation: Bool) -> CGRect? { + for (key, itemView) in self.gridItemViews { + if key.id == peerId && key.isPresentation == isPresentation { + if let itemComponentView = itemView.view.view { + return itemComponentView.convert(itemComponentView.bounds, to: self) + } + } + } + return nil + } + + func updateItemPlaceholder(peerId: EnginePeer.Id, isPresentation: Bool, placeholder: VideoSource.Output) { + for (key, itemView) in self.gridItemViews { + if key.id == peerId && key.isPresentation == isPresentation { + if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View { + itemComponentView.updatePlaceholder(placeholder: placeholder) + } + } + } + } + func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -1854,7 +1876,7 @@ final class VideoChatParticipantsComponent: Component { return UIColor(white: 1.0, alpha: 1.0) } else { let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) - let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + let value: CGFloat = 1.0 - Display.bezierPoint(0.42, 0.0, 0.58, 1.0, step) return UIColor(white: 0.0, alpha: baseGradientAlpha * value) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index a50b442a57..70ebfc1ef5 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -114,6 +114,8 @@ final class VideoChatScreenComponent: Component { var focusedSpeakerAutoSwitchDeadline: Double = 0.0 var isTwoColumnSidebarHidden: Bool = false + var isAnimatedOutFromPrivateCall: Bool = false + let inviteDisposable = MetaDisposable() let currentAvatarMixin = Atomic(value: nil) let updateAvatarDisposable = MetaDisposable() @@ -164,6 +166,76 @@ final class VideoChatScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.5)) } + func animateIn(sourceCallController: CallController) { + let sourceCallControllerView = sourceCallController.view + var isAnimationFinished = false + let animateOutData = sourceCallController.animateOutToGroupChat(completion: { [weak sourceCallControllerView] in + isAnimationFinished = true + sourceCallControllerView?.removeFromSuperview() + }) + + var expandedPeer: (id: EnginePeer.Id, isPresentation: Bool)? + if let animateOutData, animateOutData.incomingVideoLayer != nil { + if let members = self.members, let participant = members.participants.first(where: { $0.peer.id == animateOutData.incomingPeerId }) { + if let _ = participant.videoDescription { + expandedPeer = (participant.peer.id, false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true) + } + } + } + + self.isAnimatedOutFromPrivateCall = true + self.verticalPanState = nil + + self.state?.updated(transition: .immediate) + + if !isAnimationFinished { + if let participantsView = self.participants.view { + self.containerView.insertSubview(sourceCallController.view, belowSubview: participantsView) + } else { + self.containerView.addSubview(sourceCallController.view) + } + } + + let transition: ComponentTransition = .spring(duration: 0.4) + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.25) + + self.isAnimatedOutFromPrivateCall = false + self.expandedParticipantsVideoState = nil + self.state?.updated(transition: transition) + + if let animateOutData, let expandedPeer, let incomingVideoLayer = animateOutData.incomingVideoLayer, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View, let targetFrame = participantsView.itemFrame(peerId: expandedPeer.id, isPresentation: expandedPeer.isPresentation) { + if let incomingVideoPlaceholder = animateOutData.incomingVideoPlaceholder { + participantsView.updateItemPlaceholder(peerId: expandedPeer.id, isPresentation: expandedPeer.isPresentation, placeholder: incomingVideoPlaceholder) + } + + let incomingVideoLayerFrame = incomingVideoLayer.convert(incomingVideoLayer.frame, to: sourceCallControllerView?.layer) + + let targetContainer = SimpleLayer() + targetContainer.masksToBounds = true + targetContainer.backgroundColor = UIColor.blue.cgColor + targetContainer.cornerRadius = 10.0 + + self.containerView.layer.insertSublayer(targetContainer, above: participantsView.layer) + + targetContainer.frame = incomingVideoLayerFrame + + targetContainer.addSublayer(incomingVideoLayer) + incomingVideoLayer.position = CGRect(origin: CGPoint(), size: incomingVideoLayerFrame.size).center + let sourceFitScale = max(incomingVideoLayerFrame.width / incomingVideoLayerFrame.width, incomingVideoLayerFrame.height / incomingVideoLayerFrame.height) + incomingVideoLayer.transform = CATransform3DMakeScale(sourceFitScale, sourceFitScale, 1.0) + + let targetFrame = participantsView.convert(targetFrame, to: self) + let targetFitScale = min(incomingVideoLayerFrame.width / targetFrame.width, incomingVideoLayerFrame.height / targetFrame.height) + + transition.setFrame(layer: targetContainer, frame: targetFrame, completion: { [weak targetContainer] _ in + targetContainer?.removeFromSuperlayer() + }) + transition.setTransform(layer: incomingVideoLayer, transform: CATransform3DMakeScale(targetFitScale, targetFitScale, 1.0)) + alphaTransition.setAlpha(layer: targetContainer, alpha: 0.0) + } + } + func animateOut(completion: @escaping () -> Void) { self.verticalPanState = PanState(fraction: 1.0, scrollView: nil) self.completionOnPanGestureApply = completion @@ -1027,30 +1099,32 @@ final class VideoChatScreenComponent: Component { self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) }) - self.memberEventsDisposable = (component.call.memberEvents - |> deliverOnMainQueue).start(next: { [weak self] event in - guard let self, let members = self.members, let component = self.component, let environment = self.environment else { - return - } - if event.joined { - var displayEvent = false - if case let .channel(channel) = self.peer, case .broadcast = channel.info { - displayEvent = false + if component.call.peerId != nil { + self.memberEventsDisposable = (component.call.memberEvents + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let self, let members = self.members, let component = self.component, let environment = self.environment else { + return } - if members.totalCount < 40 { - displayEvent = true - } else if event.peer.isVerified { - displayEvent = true - } else if event.isContact || event.isInChatList { - displayEvent = true + if event.joined { + var displayEvent = false + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + displayEvent = false + } + if members.totalCount < 40 { + displayEvent = true + } else if event.peer.isVerified { + displayEvent = true + } else if event.isContact || event.isInChatList { + displayEvent = true + } + + if displayEvent { + let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + } } - - if displayEvent { - let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string - self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - } - }) + }) + } } self.isPresentedValue.set(environment.isVisible) @@ -1210,6 +1284,7 @@ final class VideoChatScreenComponent: Component { self.containerView.addSubview(navigationLeftButtonView) } transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + alphaTransition.setAlpha(view: navigationLeftButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) @@ -1218,6 +1293,7 @@ final class VideoChatScreenComponent: Component { self.containerView.addSubview(navigationRightButtonView) } transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame) + alphaTransition.setAlpha(view: navigationRightButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } if isTwoColumnLayout { @@ -1300,10 +1376,11 @@ final class VideoChatScreenComponent: Component { maxTitleWidth -= 110.0 } + //TODO:localize let titleSize = self.title.update( transition: transition, component: AnyComponent(VideoChatTitleComponent( - title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? " ", + title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? "Group Call", status: idleTitleStatusText, isRecording: self.callState?.recordingStartTimestamp != nil, strings: environment.strings, @@ -1350,6 +1427,7 @@ final class VideoChatScreenComponent: Component { self.containerView.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) + alphaTransition.setAlpha(view: titleView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } let areButtonsCollapsed: Bool @@ -1411,6 +1489,10 @@ final class VideoChatScreenComponent: Component { let actionMicrophoneButtonSpacing = min(effectiveMaxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter)) + if self.isAnimatedOutFromPrivateCall { + collapsedMicrophoneButtonFrame.origin.y = availableSize.height + 48.0 + } + var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter)) var isMainColumnHidden = false @@ -1617,6 +1699,9 @@ final class VideoChatScreenComponent: Component { if let callState = self.callState, callState.scheduleTimestamp != nil { participantsAlpha = 0.0 } + if self.isAnimatedOutFromPrivateCall { + participantsAlpha = 0.0 + } alphaTransition.setAlpha(view: participantsView, alpha: participantsAlpha) } @@ -1919,12 +2004,16 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo private var isAnimatingDismiss: Bool = false private var idleTimerExtensionDisposable: Disposable? + + private var sourceCallController: CallController? public init( initialData: InitialData, - call: PresentationGroupCall + call: PresentationGroupCall, + sourceCallController: CallController? ) { self.call = call + self.sourceCallController = sourceCallController let theme = customizeDefaultDarkPresentationTheme( theme: defaultDarkPresentationTheme, @@ -1964,7 +2053,12 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo self.isDismissed = false if let componentView = self.node.hostView.componentView as? VideoChatScreenComponent.View { - componentView.animateIn() + if let sourceCallController = self.sourceCallController { + self.sourceCallController = nil + componentView.animateIn(sourceCallController: sourceCallController) + } else { + componentView.animateIn() + } } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 651b385172..8033762d00 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7148,12 +7148,6 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte } } -public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController { - let useV2 = shouldUseV2VideoChatImpl(context: accountContext) - - if useV2 { - return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call) - } else { - return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call) - } +public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any, sourceCallController: CallController?) -> VoiceChatController { + return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call, sourceCallController: sourceCallController) } diff --git a/submodules/TelegramCore/BUILD b/submodules/TelegramCore/BUILD index 2d8a1a61bc..2b09a36713 100644 --- a/submodules/TelegramCore/BUILD +++ b/submodules/TelegramCore/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/Utils/RangeSet:RangeSet", "//submodules/Utils/DarwinDirStat", "//submodules/Emoji", + "//submodules/TelegramCore/FlatSerialization", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCore/FlatBuffers/BUILD b/submodules/TelegramCore/FlatBuffers/BUILD new file mode 100644 index 0000000000..e2d518aa50 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/BUILD @@ -0,0 +1,16 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "FlatBuffers", + module_name = "FlatBuffers", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramCore/FlatBuffers/Sources/ByteBuffer.swift b/submodules/TelegramCore/FlatBuffers/Sources/ByteBuffer.swift new file mode 100644 index 0000000000..9442c855a6 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/ByteBuffer.swift @@ -0,0 +1,542 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// `ByteBuffer` is the interface that stores the data for a `Flatbuffers` object +/// it allows users to write and read data directly from memory thus the use of its +/// functions should be used +@frozen +public struct ByteBuffer { + + /// Storage is a container that would hold the memory pointer to solve the issue of + /// deallocating the memory that was held by (memory: UnsafeMutableRawPointer) + @usableFromInline + final class Storage { + // This storage doesn't own the memory, therefore, we won't deallocate on deinit. + private let unowned: Bool + /// pointer to the start of the buffer object in memory + var memory: UnsafeMutableRawPointer + /// Capacity of UInt8 the buffer can hold + var capacity: Int + + @usableFromInline + init(count: Int, alignment: Int) { + memory = UnsafeMutableRawPointer.allocate( + byteCount: count, + alignment: alignment) + capacity = count + unowned = false + } + + @usableFromInline + init(memory: UnsafeMutableRawPointer, capacity: Int, unowned: Bool) { + self.memory = memory + self.capacity = capacity + self.unowned = unowned + } + + deinit { + if !unowned { + memory.deallocate() + } + } + + @usableFromInline + func copy(from ptr: UnsafeRawPointer, count: Int) { + assert( + !unowned, + "copy should NOT be called on a buffer that is built by assumingMemoryBound") + memory.copyMemory(from: ptr, byteCount: count) + } + + @usableFromInline + func initialize(for size: Int) { + assert( + !unowned, + "initalize should NOT be called on a buffer that is built by assumingMemoryBound") + memset(memory, 0, size) + } + + /// Reallocates the buffer incase the object to be written doesnt fit in the current buffer + /// - Parameter size: Size of the current object + @usableFromInline + func reallocate(_ size: Int, writerSize: Int, alignment: Int) { + let currentWritingIndex = capacity &- writerSize + while capacity <= writerSize &+ size { + capacity = capacity << 1 + } + + /// solution take from Apple-NIO + capacity = capacity.convertToPowerofTwo + + let newData = UnsafeMutableRawPointer.allocate( + byteCount: capacity, + alignment: alignment) + memset(newData, 0, capacity &- writerSize) + memcpy( + newData.advanced(by: capacity &- writerSize), + memory.advanced(by: currentWritingIndex), + writerSize) + memory.deallocate() + memory = newData + } + } + + @usableFromInline var _storage: Storage + + /// The size of the elements written to the buffer + their paddings + private var _writerSize: Int = 0 + /// Alignment of the current memory being written to the buffer + var alignment = 1 + /// Current Index which is being used to write to the buffer, it is written from the end to the start of the buffer + var writerIndex: Int { _storage.capacity &- _writerSize } + + /// Reader is the position of the current Writer Index (capacity - size) + public var reader: Int { writerIndex } + /// Current size of the buffer + public var size: UOffset { UOffset(_writerSize) } + /// Public Pointer to the buffer object in memory. This should NOT be modified for any reason + public var memory: UnsafeMutableRawPointer { _storage.memory } + /// Current capacity for the buffer + public var capacity: Int { _storage.capacity } + /// Crash if the trying to read an unaligned buffer instead of allowing users to read them. + public let allowReadingUnalignedBuffers: Bool + + /// Constructor that creates a Flatbuffer object from a UInt8 + /// - Parameter + /// - bytes: Array of UInt8 + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + bytes: [UInt8], + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + var b = bytes + _storage = Storage(count: bytes.count, alignment: alignment) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + b.withUnsafeMutableBytes { bufferPointer in + _storage.copy(from: bufferPointer.baseAddress!, count: bytes.count) + } + } + + #if !os(WASI) + /// Constructor that creates a Flatbuffer from the Swift Data type object + /// - Parameter + /// - data: Swift data Object + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + data: Data, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + var b = data + _storage = Storage(count: data.count, alignment: alignment) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + b.withUnsafeMutableBytes { bufferPointer in + _storage.copy(from: bufferPointer.baseAddress!, count: data.count) + } + } + #endif + + /// Constructor that creates a Flatbuffer instance with a size + /// - Parameter: + /// - size: Length of the buffer + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + init(initialSize size: Int) { + let size = size.convertToPowerofTwo + _storage = Storage(count: size, alignment: alignment) + _storage.initialize(for: size) + allowReadingUnalignedBuffers = false + } + + #if swift(>=5.0) && !os(WASI) + /// Constructor that creates a Flatbuffer object from a ContiguousBytes + /// - Parameters: + /// - contiguousBytes: Binary stripe to use as the buffer + /// - count: amount of readable bytes + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + contiguousBytes: Bytes, + count: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(count: count, alignment: alignment) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + contiguousBytes.withUnsafeBytes { buf in + _storage.copy(from: buf.baseAddress!, count: buf.count) + } + } + #endif + + /// Constructor that creates a Flatbuffer from unsafe memory region without copying + /// - Parameter: + /// - assumingMemoryBound: The unsafe memory region + /// - capacity: The size of the given memory region + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + public init( + assumingMemoryBound memory: UnsafeMutableRawPointer, + capacity: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(memory: memory, capacity: capacity, unowned: true) + _writerSize = capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + } + + /// Creates a copy of the buffer that's being built by calling sizedBuffer + /// - Parameters: + /// - memory: Current memory of the buffer + /// - count: count of bytes + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + init( + memory: UnsafeMutableRawPointer, + count: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(count: count, alignment: alignment) + _storage.copy(from: memory, count: count) + _writerSize = _storage.capacity + allowReadingUnalignedBuffers = allowUnalignedBuffers + } + + /// Creates a copy of the existing flatbuffer, by copying it to a different memory. + /// - Parameters: + /// - memory: Current memory of the buffer + /// - count: count of bytes + /// - removeBytes: Removes a number of bytes from the current size + /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer + init( + memory: UnsafeMutableRawPointer, + count: Int, + removing removeBytes: Int, + allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false) + { + _storage = Storage(count: count, alignment: alignment) + _storage.copy(from: memory, count: count) + _writerSize = removeBytes + allowReadingUnalignedBuffers = allowUnalignedBuffers + } + + /// Fills the buffer with padding by adding to the writersize + /// - Parameter padding: Amount of padding between two to be serialized objects + @inline(__always) + @usableFromInline + mutating func fill(padding: Int) { + assert(padding >= 0, "Fill should be larger than or equal to zero") + ensureSpace(size: padding) + _writerSize = _writerSize &+ (MemoryLayout.size &* padding) + } + + /// Adds an array of type Scalar to the buffer memory + /// - Parameter elements: An array of Scalars + @inline(__always) + @usableFromInline + mutating func push(elements: [T]) { + elements.withUnsafeBytes { ptr in + ensureSpace(size: ptr.count) + memcpy( + _storage.memory.advanced(by: writerIndex &- ptr.count), + ptr.baseAddress!, + ptr.count) + _writerSize = _writerSize &+ ptr.count + } + } + + /// Adds an array of type Scalar to the buffer memory + /// - Parameter elements: An array of Scalars + @inline(__always) + @usableFromInline + mutating func push(elements: [T]) { + elements.withUnsafeBytes { ptr in + ensureSpace(size: ptr.count) + memcpy( + _storage.memory.advanced(by: writerIndex &- ptr.count), + ptr.baseAddress!, + ptr.count) + _writerSize = _writerSize &+ ptr.count + } + } + + /// Adds a `ContiguousBytes` to buffer memory + /// - Parameter value: bytes to copy + #if swift(>=5.0) && !os(WASI) + @inline(__always) + @usableFromInline + mutating func push(bytes: ContiguousBytes) { + bytes.withUnsafeBytes { ptr in + ensureSpace(size: ptr.count) + memcpy( + _storage.memory.advanced(by: writerIndex &- ptr.count), + ptr.baseAddress!, + ptr.count) + _writerSize = _writerSize &+ ptr.count + } + } + #endif + + /// Adds an object of type NativeStruct into the buffer + /// - Parameters: + /// - value: Object that will be written to the buffer + /// - size: size to subtract from the WriterIndex + @usableFromInline + @inline(__always) + mutating func push(struct value: T, size: Int) { + ensureSpace(size: size) + withUnsafePointer(to: value) { + memcpy( + _storage.memory.advanced(by: writerIndex &- size), + $0, + size) + _writerSize = _writerSize &+ size + } + } + + /// Adds an object of type Scalar into the buffer + /// - Parameters: + /// - value: Object that will be written to the buffer + /// - len: Offset to subtract from the WriterIndex + @inline(__always) + @usableFromInline + mutating func push(value: T, len: Int) { + ensureSpace(size: len) + withUnsafePointer(to: value) { + memcpy( + _storage.memory.advanced(by: writerIndex &- len), + $0, + len) + _writerSize = _writerSize &+ len + } + } + + /// Adds a string to the buffer using swift.utf8 object + /// - Parameter str: String that will be added to the buffer + /// - Parameter len: length of the string + @inline(__always) + @usableFromInline + mutating func push(string str: String, len: Int) { + ensureSpace(size: len) + if str.utf8 + .withContiguousStorageIfAvailable({ self.push(bytes: $0, len: len) }) != + nil + { + } else { + let utf8View = str.utf8 + for c in utf8View.reversed() { + push(value: c, len: 1) + } + } + } + + /// Writes a string to Bytebuffer using UTF8View + /// - Parameters: + /// - bytes: Pointer to the view + /// - len: Size of string + @usableFromInline + @inline(__always) + mutating func push( + bytes: UnsafeBufferPointer, + len: Int) -> Bool + { + memcpy( + _storage.memory.advanced(by: writerIndex &- len), + bytes.baseAddress!, + len) + _writerSize = _writerSize &+ len + return true + } + + /// Write stores an object into the buffer directly or indirectly. + /// + /// Direct: ignores the capacity of buffer which would mean we are referring to the direct point in memory + /// indirect: takes into respect the current capacity of the buffer (capacity - index), writing to the buffer from the end + /// - Parameters: + /// - value: Value that needs to be written to the buffer + /// - index: index to write to + /// - direct: Should take into consideration the capacity of the buffer + @inline(__always) + func write(value: T, index: Int, direct: Bool = false) { + var index = index + if !direct { + index = _storage.capacity &- index + } + assert(index < _storage.capacity, "Write index is out of writing bound") + assert(index >= 0, "Writer index should be above zero") + withUnsafePointer(to: value) { + memcpy( + _storage.memory.advanced(by: index), + $0, + MemoryLayout.size) + } + } + + /// Makes sure that buffer has enouch space for each of the objects that will be written into it + /// - Parameter size: size of object + @discardableResult + @usableFromInline + @inline(__always) + mutating func ensureSpace(size: Int) -> Int { + if size &+ _writerSize > _storage.capacity { + _storage.reallocate(size, writerSize: _writerSize, alignment: alignment) + } + assert(size < FlatBufferMaxSize, "Buffer can't grow beyond 2 Gigabytes") + return size + } + + /// pops the written VTable if it's already written into the buffer + /// - Parameter size: size of the `VTable` + @usableFromInline + @inline(__always) + mutating func pop(_ size: Int) { + assert( + (_writerSize &- size) > 0, + "New size should NOT be a negative number") + memset(_storage.memory.advanced(by: writerIndex), 0, _writerSize &- size) + _writerSize = size + } + + /// Clears the current size of the buffer + @inline(__always) + mutating public func clearSize() { + _writerSize = 0 + } + + /// Clears the current instance of the buffer, replacing it with new memory + @inline(__always) + mutating public func clear() { + _writerSize = 0 + alignment = 1 + _storage.initialize(for: _storage.capacity) + } + + /// Reads an object from the buffer + /// - Parameters: + /// - def: Type of the object + /// - position: the index of the object in the buffer + @inline(__always) + public func read(def: T.Type, position: Int) -> T { + if allowReadingUnalignedBuffers { + return _storage.memory.advanced(by: position).loadUnaligned(as: T.self) + } + return _storage.memory.advanced(by: position).load(as: T.self) + } + + /// Reads a slice from the memory assuming a type of T + /// - Parameters: + /// - index: index of the object to be read from the buffer + /// - count: count of bytes in memory + @inline(__always) + public func readSlice( + index: Int, + count: Int) -> [T] + { + assert( + index + count <= _storage.capacity, + "Reading out of bounds is illegal") + let start = _storage.memory.advanced(by: index) + .assumingMemoryBound(to: T.self) + let array = UnsafeBufferPointer(start: start, count: count) + return Array(array) + } + + #if !os(WASI) + /// Reads a string from the buffer and encodes it to a swift string + /// - Parameters: + /// - index: index of the string in the buffer + /// - count: length of the string + /// - type: Encoding of the string + @inline(__always) + public func readString( + at index: Int, + count: Int, + type: String.Encoding = .utf8) -> String? + { + assert( + index + count <= _storage.capacity, + "Reading out of bounds is illegal") + let start = _storage.memory.advanced(by: index) + .assumingMemoryBound(to: UInt8.self) + let bufprt = UnsafeBufferPointer(start: start, count: count) + return String(bytes: Array(bufprt), encoding: type) + } + #else + /// Reads a string from the buffer and encodes it to a swift string + /// - Parameters: + /// - index: index of the string in the buffer + /// - count: length of the string + @inline(__always) + public func readString( + at index: Int, + count: Int) -> String? + { + assert( + index + count <= _storage.capacity, + "Reading out of bounds is illegal") + let start = _storage.memory.advanced(by: index) + .assumingMemoryBound(to: UInt8.self) + let bufprt = UnsafeBufferPointer(start: start, count: count) + return String(cString: bufprt.baseAddress!) + } + #endif + + /// Creates a new Flatbuffer object that's duplicated from the current one + /// - Parameter removeBytes: the amount of bytes to remove from the current Size + @inline(__always) + public func duplicate(removing removeBytes: Int = 0) -> ByteBuffer { + assert(removeBytes > 0, "Can NOT remove negative bytes") + assert( + removeBytes < _storage.capacity, + "Can NOT remove more bytes than the ones allocated") + return ByteBuffer( + memory: _storage.memory, + count: _storage.capacity, + removing: _writerSize &- removeBytes) + } + + /// Returns the written bytes into the ``ByteBuffer`` + public var underlyingBytes: [UInt8] { + let cp = capacity &- writerIndex + let start = memory.advanced(by: writerIndex) + .bindMemory(to: UInt8.self, capacity: cp) + + let ptr = UnsafeBufferPointer(start: start, count: cp) + return Array(ptr) + } + + /// SkipPrefix Skips the first 4 bytes in case one of the following + /// functions are called `getPrefixedSizeCheckedRoot` & `getPrefixedSizeRoot` + /// which allows us to skip the first 4 bytes instead of recreating the buffer + @discardableResult + @usableFromInline + @inline(__always) + mutating func skipPrefix() -> Int32 { + _writerSize = _writerSize &- MemoryLayout.size + return read(def: Int32.self, position: 0) + } + +} + +extension ByteBuffer: CustomDebugStringConvertible { + + public var debugDescription: String { + """ + buffer located at: \(_storage.memory), with capacity of \(_storage.capacity) + { writerSize: \(_writerSize), readerSize: \(reader), writerIndex: \( + writerIndex) } + """ + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Constants.swift b/submodules/TelegramCore/FlatBuffers/Sources/Constants.swift new file mode 100644 index 0000000000..35912a50ca --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Constants.swift @@ -0,0 +1,114 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// A boolean to see if the system is littleEndian +let isLitteEndian: Bool = { + let number: UInt32 = 0x12345678 + return number == number.littleEndian +}() +/// Constant for the file id length +let FileIdLength = 4 +/// Type aliases +public typealias Byte = UInt8 +public typealias UOffset = UInt32 +public typealias SOffset = Int32 +public typealias VOffset = UInt16 +/// Maximum size for a buffer +public let FlatBufferMaxSize = UInt32 + .max << ((MemoryLayout.size * 8 - 1) - 1) + +/// Protocol that All Scalars should conform to +/// +/// Scalar is used to conform all the numbers that can be represented in a FlatBuffer. It's used to write/read from the buffer. +public protocol Scalar: Equatable { + associatedtype NumericValue + var convertedEndian: NumericValue { get } +} + +extension Scalar where Self: Verifiable {} + +extension Scalar where Self: FixedWidthInteger { + /// Converts the value from BigEndian to LittleEndian + /// + /// Converts values to little endian on machines that work with BigEndian, however this is NOT TESTED yet. + public var convertedEndian: NumericValue { + self as! Self.NumericValue + } +} + +extension Double: Scalar, Verifiable { + public typealias NumericValue = UInt64 + + public var convertedEndian: UInt64 { + bitPattern.littleEndian + } +} + +extension Float32: Scalar, Verifiable { + public typealias NumericValue = UInt32 + + public var convertedEndian: UInt32 { + bitPattern.littleEndian + } +} + +extension Bool: Scalar, Verifiable { + public var convertedEndian: UInt8 { + self == true ? 1 : 0 + } + + public typealias NumericValue = UInt8 +} + +extension Int: Scalar, Verifiable { + public typealias NumericValue = Int +} + +extension Int8: Scalar, Verifiable { + public typealias NumericValue = Int8 +} + +extension Int16: Scalar, Verifiable { + public typealias NumericValue = Int16 +} + +extension Int32: Scalar, Verifiable { + public typealias NumericValue = Int32 +} + +extension Int64: Scalar, Verifiable { + public typealias NumericValue = Int64 +} + +extension UInt8: Scalar, Verifiable { + public typealias NumericValue = UInt8 +} + +extension UInt16: Scalar, Verifiable { + public typealias NumericValue = UInt16 +} + +extension UInt32: Scalar, Verifiable { + public typealias NumericValue = UInt32 +} + +extension UInt64: Scalar, Verifiable { + public typealias NumericValue = UInt64 +} + +public func FlatBuffersVersion_24_12_23() {} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Enum.swift b/submodules/TelegramCore/FlatBuffers/Sources/Enum.swift new file mode 100644 index 0000000000..29b382247a --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Enum.swift @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Enum is a protocol that all flatbuffers enums should conform to +/// Since it allows us to get the actual `ByteSize` and `Value` from +/// a swift enum. +public protocol Enum { + /// associatedtype that the type of the enum should conform to + associatedtype T: Scalar & Verifiable + /// Size of the current associatedtype in the enum + static var byteSize: Int { get } + /// The current value the enum hosts + var value: T { get } +} + +extension Enum where Self: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + try verifier.inBuffer(position: position, of: type.self) + } + +} + +/// UnionEnum is a Protocol that allows us to create Union type of enums +/// and their value initializers. Since an `init` was required by +/// the verifier +public protocol UnionEnum: Enum { + init?(value: T) throws +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferBuilder.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferBuilder.swift new file mode 100644 index 0000000000..26ae634915 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferBuilder.swift @@ -0,0 +1,925 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// ``FlatBufferBuilder`` builds a `FlatBuffer` through manipulating its internal state. +/// +/// This is done by creating a ``ByteBuffer`` that hosts the incoming data and +/// has a hardcoded growth limit of `2GiB` which is set by the Flatbuffers standards. +/// +/// ```swift +/// var builder = FlatBufferBuilder() +/// ``` +/// The builder should be always created as a variable, since it would be passed into the writers +/// +@frozen +public struct FlatBufferBuilder { + + /// Storage for the Vtables used in the buffer are stored in here, so they would be written later in EndTable + @usableFromInline internal var _vtableStorage = VTableStorage() + /// Flatbuffer data will be written into + @usableFromInline internal var _bb: ByteBuffer + + /// Reference Vtables that were already written to the buffer + private var _vtables: [UOffset] = [] + /// A check if the buffer is being written into by a different table + private var isNested = false + /// Dictonary that stores a map of all the strings that were written to the buffer + private var stringOffsetMap: [String: Offset] = [:] + /// A check to see if finish(::) was ever called to retreive data object + private var finished = false + /// A check to see if the buffer should serialize Default values + private var serializeDefaults: Bool + + /// Current alignment for the buffer + var _minAlignment: Int = 0 { + didSet { + _bb.alignment = _minAlignment + } + } + + /// Gives a read access to the buffer's size + public var size: UOffset { _bb.size } + + #if !os(WASI) + /// Data representation of the buffer + /// + /// Should only be used after ``finish(offset:addPrefix:)`` is called + public var data: Data { + assert(finished, "Data shouldn't be called before finish()") + return Data( + bytes: _bb.memory.advanced(by: _bb.writerIndex), + count: _bb.capacity &- _bb.writerIndex) + } + #endif + + /// Returns the underlying bytes in the ``ByteBuffer`` + /// + /// Note: This should be used with caution. + public var fullSizedByteArray: [UInt8] { + let ptr = UnsafeBufferPointer( + start: _bb.memory.assumingMemoryBound(to: UInt8.self), + count: _bb.capacity) + return Array(ptr) + } + + /// Returns the written bytes into the ``ByteBuffer`` + /// + /// Should only be used after ``finish(offset:addPrefix:)`` is called + public var sizedByteArray: [UInt8] { + assert(finished, "Data shouldn't be called before finish()") + return _bb.underlyingBytes + } + + /// Returns the original ``ByteBuffer`` + /// + /// Returns the current buffer that was just created + /// with the offsets, and data written to it. + public var buffer: ByteBuffer { _bb } + + /// Returns a newly created sized ``ByteBuffer`` + /// + /// returns a new buffer that is sized to the data written + /// to the main buffer + public var sizedBuffer: ByteBuffer { + assert(finished, "Data shouldn't be called before finish()") + return ByteBuffer( + memory: _bb.memory.advanced(by: _bb.reader), + count: Int(_bb.size)) + } + + // MARK: - Init + + /// Initialize the buffer with a size + /// - Parameters: + /// - initialSize: Initial size for the buffer + /// - force: Allows default to be serialized into the buffer + /// + /// This initializes a new builder with an initialSize that would initialize + /// a new ``ByteBuffer``. ``FlatBufferBuilder`` by default doesnt serialize defaults + /// however the builder can be force by passing true for `serializeDefaults` + public init( + initialSize: Int32 = 1024, + serializeDefaults force: Bool = false) + { + assert(initialSize > 0, "Size should be greater than zero!") + guard isLitteEndian else { + fatalError( + "Reading/Writing a buffer in big endian machine is not supported on swift") + } + serializeDefaults = force + _bb = ByteBuffer(initialSize: Int(initialSize)) + } + + /// Clears the builder and the buffer from the written data. + mutating public func clear() { + _minAlignment = 0 + isNested = false + stringOffsetMap.removeAll(keepingCapacity: true) + _vtables.removeAll(keepingCapacity: true) + _vtableStorage.clear() + _bb.clear() + } + + // MARK: - Create Tables + + /// Checks if the required fields were serialized into the buffer + /// - Parameters: + /// - table: offset for the table + /// - fields: Array of all the important fields to be serialized + /// + /// *NOTE: Never call this function, this is only supposed to be called + /// by the generated code* + @inline(__always) + mutating public func require(table: Offset, fields: [Int32]) { + for index in stride(from: 0, to: fields.count, by: 1) { + let start = _bb.capacity &- Int(table.o) + let startTable = start &- Int(_bb.read(def: Int32.self, position: start)) + let isOkay = _bb.read( + def: VOffset.self, + position: startTable &+ Int(fields[index])) != 0 + assert(isOkay, "Flatbuffers requires the following field") + } + } + + /// Finished the buffer by adding the file id and then calling finish + /// - Parameters: + /// - offset: Offset of the table + /// - fileId: Takes the fileId + /// - prefix: if false it wont add the size of the buffer + /// + /// ``finish(offset:fileId:addPrefix:)`` should be called at the end of creating + /// a table + /// ```swift + /// var root = SomeObject + /// .createObject(&builder, + /// name: nameOffset) + /// builder.finish( + /// offset: root, + /// fileId: "ax1a", + /// addPrefix: true) + /// ``` + /// File id would append a file id name at the end of the written bytes before, + /// finishing the buffer. + /// + /// Whereas, if `addPrefix` is true, the written bytes would + /// include the size of the current buffer. + mutating public func finish( + offset: Offset, + fileId: String, + addPrefix prefix: Bool = false) + { + let size = MemoryLayout.size + preAlign( + len: size &+ (prefix ? size : 0) &+ FileIdLength, + alignment: _minAlignment) + assert(fileId.count == FileIdLength, "Flatbuffers requires file id to be 4") + _bb.push(string: fileId, len: 4) + finish(offset: offset, addPrefix: prefix) + } + + /// Finished the buffer by adding the file id, offset, and prefix to it. + /// - Parameters: + /// - offset: Offset of the table + /// - prefix: if false it wont add the size of the buffer + /// + /// ``finish(offset:addPrefix:)`` should be called at the end of creating + /// a table + /// ```swift + /// var root = SomeObject + /// .createObject(&builder, + /// name: nameOffset) + /// builder.finish( + /// offset: root, + /// addPrefix: true) + /// ``` + /// If `addPrefix` is true, the written bytes would + /// include the size of the current buffer. + mutating public func finish( + offset: Offset, + addPrefix prefix: Bool = false) + { + notNested() + let size = MemoryLayout.size + preAlign(len: size &+ (prefix ? size : 0), alignment: _minAlignment) + push(element: refer(to: offset.o)) + if prefix { push(element: _bb.size) } + _vtableStorage.clear() + finished = true + } + + /// ``startTable(with:)`` will let the builder know, that a new object is being serialized. + /// + /// The function will fatalerror if called while there is another object being serialized. + /// ```swift + /// let start = Monster + /// .startMonster(&fbb) + /// ``` + /// - Parameter numOfFields: Number of elements to be written to the buffer + /// - Returns: Offset of the newly started table + @inline(__always) + mutating public func startTable(with numOfFields: Int) -> UOffset { + notNested() + isNested = true + _vtableStorage.start(count: numOfFields) + return _bb.size + } + + /// ``endTable(at:)`` will let the ``FlatBufferBuilder`` know that the + /// object that's written to it is completed + /// + /// This would be called after all the elements are serialized, + /// it will add the current vtable into the ``ByteBuffer``. + /// The functions will `fatalError` in case the object is called + /// without ``startTable(with:)``, or the object has exceeded the limit of 2GB. + /// + /// - Parameter startOffset:Start point of the object written + /// - returns: The root of the table + mutating public func endTable(at startOffset: UOffset) -> UOffset { + assert(isNested, "Calling endtable without calling starttable") + let sizeofVoffset = MemoryLayout.size + let vTableOffset = push(element: SOffset(0)) + + let tableObjectSize = vTableOffset &- startOffset + assert(tableObjectSize < 0x10000, "Buffer can't grow beyond 2 Gigabytes") + let _max = Int(_vtableStorage.maxOffset) &+ sizeofVoffset + + _bb.fill(padding: _max) + _bb.write( + value: VOffset(tableObjectSize), + index: _bb.writerIndex &+ sizeofVoffset, + direct: true) + _bb.write(value: VOffset(_max), index: _bb.writerIndex, direct: true) + + var itr = 0 + while itr < _vtableStorage.writtenIndex { + let loaded = _vtableStorage.load(at: itr) + itr = itr &+ _vtableStorage.size + guard loaded.offset != 0 else { continue } + let _index = (_bb.writerIndex &+ Int(loaded.position)) + _bb.write( + value: VOffset(vTableOffset &- loaded.offset), + index: _index, + direct: true) + } + + _vtableStorage.clear() + let vt_use = _bb.size + + var isAlreadyAdded: Int? + + let vt2 = _bb.memory.advanced(by: _bb.writerIndex) + let len2 = vt2.load(fromByteOffset: 0, as: Int16.self) + + for index in stride(from: 0, to: _vtables.count, by: 1) { + let position = _bb.capacity &- Int(_vtables[index]) + let vt1 = _bb.memory.advanced(by: position) + let len1 = _bb.read(def: Int16.self, position: position) + if len2 != len1 || 0 != memcmp(vt1, vt2, Int(len2)) { continue } + + isAlreadyAdded = Int(_vtables[index]) + break + } + + if let offset = isAlreadyAdded { + let vTableOff = Int(vTableOffset) + let space = _bb.capacity &- vTableOff + _bb.write(value: Int32(offset &- vTableOff), index: space, direct: true) + _bb.pop(_bb.capacity &- space) + } else { + _bb.write(value: Int32(vt_use &- vTableOffset), index: Int(vTableOffset)) + _vtables.append(_bb.size) + } + isNested = false + return vTableOffset + } + + // MARK: - Builds Buffer + + /// Asserts to see if the object is not nested + @inline(__always) + @usableFromInline + mutating internal func notNested() { + assert(!isNested, "Object serialization must not be nested") + } + + /// Changes the minimuim alignment of the buffer + /// - Parameter size: size of the current alignment + @inline(__always) + @usableFromInline + mutating internal func minAlignment(size: Int) { + if size > _minAlignment { + _minAlignment = size + } + } + + /// Gets the padding for the current element + /// - Parameters: + /// - bufSize: Current size of the buffer + the offset of the object to be written + /// - elementSize: Element size + @inline(__always) + @usableFromInline + mutating internal func padding( + bufSize: UInt32, + elementSize: UInt32) -> UInt32 + { + ((~bufSize) &+ 1) & (elementSize - 1) + } + + /// Prealigns the buffer before writting a new object into the buffer + /// - Parameters: + /// - len:Length of the object + /// - alignment: Alignment type + @inline(__always) + @usableFromInline + mutating internal func preAlign(len: Int, alignment: Int) { + minAlignment(size: alignment) + _bb.fill(padding: Int(padding( + bufSize: _bb.size &+ UOffset(len), + elementSize: UOffset(alignment)))) + } + + /// Prealigns the buffer before writting a new object into the buffer + /// - Parameters: + /// - len: Length of the object + /// - type: Type of the object to be written + @inline(__always) + @usableFromInline + mutating internal func preAlign(len: Int, type: T.Type) { + preAlign(len: len, alignment: MemoryLayout.size) + } + + /// Refers to an object that's written in the buffer + /// - Parameter off: the objects index value + @inline(__always) + @usableFromInline + mutating internal func refer(to off: UOffset) -> UOffset { + let size = MemoryLayout.size + preAlign(len: size, alignment: size) + return _bb.size &- off &+ UInt32(size) + } + + /// Tracks the elements written into the buffer + /// - Parameters: + /// - offset: The offset of the element witten + /// - position: The position of the element + @inline(__always) + @usableFromInline + mutating internal func track(offset: UOffset, at position: VOffset) { + _vtableStorage.add(loc: (offset: offset, position: position)) + } + + // MARK: - Inserting Vectors + + /// ``startVector(_:elementSize:)`` creates a new vector within buffer + /// + /// The function checks if there is a current object being written, if + /// the check passes it creates a buffer alignment of `length * elementSize` + /// ```swift + /// builder.startVector( + /// int32Values.count, elementSize: 4) + /// ``` + /// + /// - Parameters: + /// - len: Length of vector to be created + /// - elementSize: Size of object type to be written + @inline(__always) + mutating public func startVector(_ len: Int, elementSize: Int) { + notNested() + isNested = true + preAlign(len: len &* elementSize, type: UOffset.self) + preAlign(len: len &* elementSize, alignment: elementSize) + } + + /// ``endVector(len:)`` ends the currently created vector + /// + /// Calling ``endVector(len:)`` requires the length, of the current + /// vector. The length would be pushed to indicate the count of numbers + /// within the vector. If ``endVector(len:)`` is called without + /// ``startVector(_:elementSize:)`` it asserts. + /// + /// ```swift + /// let vectorOffset = builder. + /// endVector(len: int32Values.count) + /// ``` + /// + /// - Parameter len: Length of the buffer + /// - Returns: Returns the current ``Offset`` in the ``ByteBuffer`` + @inline(__always) + mutating public func endVector(len: Int) -> Offset { + assert(isNested, "Calling endVector without calling startVector") + isNested = false + return Offset(offset: push(element: Int32(len))) + } + + /// Creates a vector of type ``Scalar`` into the ``ByteBuffer`` + /// + /// ``createVector(_:)-4swl0`` writes a vector of type Scalars into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([1, 2, 3, 4]) + /// ``` + /// + /// The underlying implementation simply calls ``createVector(_:size:)-4lhrv`` + /// + /// - Parameter elements: elements to be written into the buffer + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(_ elements: [T]) -> Offset { + createVector(elements, size: elements.count) + } + + /// Creates a vector of type Scalar in the buffer + /// + /// ``createVector(_:)-4swl0`` writes a vector of type Scalars into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([1, 2, 3, 4], size: 4) + /// ``` + /// + /// - Parameter elements: Elements to be written into the buffer + /// - Parameter size: Count of elements + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector( + _ elements: [T], + size: Int) -> Offset + { + let size = size + startVector(size, elementSize: MemoryLayout.size) + _bb.push(elements: elements) + return endVector(len: size) + } + + #if swift(>=5.0) && !os(WASI) + @inline(__always) + /// Creates a vector of bytes in the buffer. + /// + /// Allows creating a vector from `Data` without copying to a `[UInt8]` + /// + /// - Parameter bytes: bytes to be written into the buffer + /// - Returns: ``Offset`` of the vector + mutating public func createVector(bytes: ContiguousBytes) -> Offset { + let size = bytes.withUnsafeBytes { ptr in ptr.count } + startVector(size, elementSize: MemoryLayout.size) + _bb.push(bytes: bytes) + return endVector(len: size) + } + #endif + + /// Creates a vector of type ``Enum`` into the ``ByteBuffer`` + /// + /// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([.swift, .cpp]) + /// ``` + /// + /// The underlying implementation simply calls ``createVector(_:size:)-7cx6z`` + /// + /// - Parameter elements: elements to be written into the buffer + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(_ elements: [T]) -> Offset { + createVector(elements, size: elements.count) + } + + /// Creates a vector of type ``Enum`` into the ``ByteBuffer`` + /// + /// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// ```swift + /// let vectorOffset = builder. + /// createVector([.swift, .cpp]) + /// ``` + /// + /// - Parameter elements: Elements to be written into the buffer + /// - Parameter size: Count of elements + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector( + _ elements: [T], + size: Int) -> Offset + { + let size = size + startVector(size, elementSize: T.byteSize) + for index in stride(from: elements.count, to: 0, by: -1) { + _bb.push(value: elements[index &- 1].value, len: T.byteSize) + } + return endVector(len: size) + } + + /// Creates a vector of already written offsets + /// + /// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)``. + /// + /// The underlying implementation simply calls ``createVector(ofOffsets:len:)`` + /// + /// ```swift + /// let namesOffsets = builder. + /// createVector(ofOffsets: [name1, name2]) + /// ``` + /// - Parameter offsets: Array of offsets of type ``Offset`` + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(ofOffsets offsets: [Offset]) -> Offset { + createVector(ofOffsets: offsets, len: offsets.count) + } + + /// Creates a vector of already written offsets + /// + /// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into + /// ``ByteBuffer``. This is a convenient method instead of calling, + /// ``startVector(_:elementSize:)`` and then ``endVector(len:)`` + /// + /// ```swift + /// let namesOffsets = builder. + /// createVector(ofOffsets: [name1, name2]) + /// ``` + /// + /// - Parameter offsets: Array of offsets of type ``Offset`` + /// - Parameter size: Count of elements + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector( + ofOffsets offsets: [Offset], + len: Int) -> Offset + { + startVector(len, elementSize: MemoryLayout.size) + for index in stride(from: offsets.count, to: 0, by: -1) { + push(element: offsets[index &- 1]) + } + return endVector(len: len) + } + + /// Creates a vector of strings + /// + /// ``createVector(ofStrings:)`` creates a vector of `String` into + /// ``ByteBuffer``. This is a convenient method instead of manually + /// creating the string offsets, you simply pass it to this function + /// and it would write the strings into the ``ByteBuffer``. + /// After that it calls ``createVector(ofOffsets:)`` + /// + /// ```swift + /// let namesOffsets = builder. + /// createVector(ofStrings: ["Name", "surname"]) + /// ``` + /// + /// - Parameter str: Array of string + /// - returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(ofStrings str: [String]) -> Offset { + var offsets: [Offset] = [] + for index in stride(from: 0, to: str.count, by: 1) { + offsets.append(create(string: str[index])) + } + return createVector(ofOffsets: offsets) + } + + /// Creates a vector of type ``NativeStruct``. + /// + /// Any swift struct in the generated code, should confirm to + /// ``NativeStruct``. Since the generated swift structs are padded + /// to the `FlatBuffers` standards. + /// + /// ```swift + /// let offsets = builder. + /// createVector(ofStructs: [NativeStr(num: 1), NativeStr(num: 2)]) + /// ``` + /// + /// - Parameter structs: A vector of ``NativeStruct`` + /// - Returns: ``Offset`` of the vector + @inline(__always) + mutating public func createVector(ofStructs structs: [T]) + -> Offset + { + startVector( + structs.count * MemoryLayout.size, + elementSize: MemoryLayout.alignment) + _bb.push(elements: structs) + return endVector(len: structs.count) + } + + // MARK: - Inserting Structs + + /// Writes a ``NativeStruct`` into the ``ByteBuffer`` + /// + /// Adds a native struct that's build and padded according + /// to `FlatBuffers` standards. with a predefined position. + /// + /// ```swift + /// let offset = builder.create( + /// struct: NativeStr(num: 1), + /// position: 10) + /// ``` + /// + /// - Parameters: + /// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer`` + /// - position: The predefined position of the object + /// - Returns: ``Offset`` of written struct + @inline(__always) + @discardableResult + mutating public func create( + struct s: T, position: VOffset) -> Offset + { + let offset = create(struct: s) + _vtableStorage.add( + loc: (offset: _bb.size, position: VOffset(position))) + return offset + } + + /// Writes a ``NativeStruct`` into the ``ByteBuffer`` + /// + /// Adds a native struct that's build and padded according + /// to `FlatBuffers` standards, directly into the buffer without + /// a predefined position. + /// + /// ```swift + /// let offset = builder.create( + /// struct: NativeStr(num: 1)) + /// ``` + /// + /// - Parameters: + /// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer`` + /// - Returns: ``Offset`` of written struct + @inline(__always) + @discardableResult + mutating public func create( + struct s: T) -> Offset + { + let size = MemoryLayout.size + preAlign(len: size, alignment: MemoryLayout.alignment) + _bb.push(struct: s, size: size) + return Offset(offset: _bb.size) + } + + // MARK: - Inserting Strings + + /// Insets a string into the buffer of type `UTF8` + /// + /// Adds a swift string into ``ByteBuffer`` by encoding it + /// using `UTF8` + /// + /// ```swift + /// let nameOffset = builder + /// .create(string: "welcome") + /// ``` + /// + /// - Parameter str: String to be serialized + /// - returns: ``Offset`` of inserted string + @inline(__always) + mutating public func create(string str: String?) -> Offset { + guard let str = str else { return Offset() } + let len = str.utf8.count + notNested() + preAlign(len: len &+ 1, type: UOffset.self) + _bb.fill(padding: 1) + _bb.push(string: str, len: len) + push(element: UOffset(len)) + return Offset(offset: _bb.size) + } + + /// Insets a shared string into the buffer of type `UTF8` + /// + /// Adds a swift string into ``ByteBuffer`` by encoding it + /// using `UTF8`. The function will check if the string, + /// is already written to the ``ByteBuffer`` + /// + /// ```swift + /// let nameOffset = builder + /// .createShared(string: "welcome") + /// + /// + /// let secondOffset = builder + /// .createShared(string: "welcome") + /// + /// assert(nameOffset.o == secondOffset.o) + /// ``` + /// + /// - Parameter str: String to be serialized + /// - returns: ``Offset`` of inserted string + @inline(__always) + mutating public func createShared(string str: String?) -> Offset { + guard let str = str else { return Offset() } + if let offset = stringOffsetMap[str] { + return offset + } + let offset = create(string: str) + stringOffsetMap[str] = offset + return offset + } + + // MARK: - Inseting offsets + + /// Writes the ``Offset`` of an already written table + /// + /// Writes the ``Offset`` of a table if not empty into the + /// ``ByteBuffer`` + /// + /// - Parameters: + /// - offset: ``Offset`` of another object to be written + /// - position: The predefined position of the object + @inline(__always) + mutating public func add(offset: Offset, at position: VOffset) { + if offset.isEmpty { return } + add(element: refer(to: offset.o), def: 0, at: position) + } + + /// Pushes a value of type ``Offset`` into the ``ByteBuffer`` + /// - Parameter o: ``Offset`` + /// - returns: Current position of the ``Offset`` + @inline(__always) + @discardableResult + mutating public func push(element o: Offset) -> UOffset { + push(element: refer(to: o.o)) + } + + // MARK: - Inserting Scalars to Buffer + + /// Writes a ``Scalar`` value into ``ByteBuffer`` + /// + /// ``add(element:def:at:)`` takes in a default value, and current value + /// and the position within the `VTable`. The default value would not + /// be serialized if the value is the same as the current value or + /// `serializeDefaults` is equal to false. + /// + /// If serializing defaults is important ``init(initialSize:serializeDefaults:)``, + /// passing true for `serializeDefaults` would do the job. + /// + /// ```swift + /// // Adds 10 to the buffer + /// builder.add(element: Int(10), def: 1, position 12) + /// ``` + /// + /// *NOTE: Never call this manually* + /// + /// - Parameters: + /// - element: Element to insert + /// - def: Default value for that element + /// - position: The predefined position of the element + @inline(__always) + mutating public func add( + element: T, + def: T, + at position: VOffset) + { + if element == def && !serializeDefaults { return } + track(offset: push(element: element), at: position) + } + + /// Writes a optional ``Scalar`` value into ``ByteBuffer`` + /// + /// Takes an optional value to be written into the ``ByteBuffer`` + /// + /// *NOTE: Never call this manually* + /// + /// - Parameters: + /// - element: Optional element of type scalar + /// - position: The predefined position of the element + @inline(__always) + mutating public func add(element: T?, at position: VOffset) { + guard let element = element else { return } + track(offset: push(element: element), at: position) + } + + /// Pushes a values of type ``Scalar`` into the ``ByteBuffer`` + /// + /// *NOTE: Never call this manually* + /// + /// - Parameter element: Element to insert + /// - returns: position of the Element + @inline(__always) + @discardableResult + mutating public func push(element: T) -> UOffset { + let size = MemoryLayout.size + preAlign( + len: size, + alignment: size) + _bb.push(value: element, len: size) + return _bb.size + } + +} + +extension FlatBufferBuilder: CustomDebugStringConvertible { + + public var debugDescription: String { + """ + buffer debug: + \(_bb) + builder debug: + { finished: \(finished), serializeDefaults: \( + serializeDefaults), isNested: \(isNested) } + """ + } + + typealias FieldLoc = (offset: UOffset, position: VOffset) + + /// VTableStorage is a class to contain the VTable buffer that would be serialized into buffer + @usableFromInline + internal class VTableStorage { + /// Memory check since deallocating each time we want to clear would be expensive + /// and memory leaks would happen if we dont deallocate the first allocated memory. + /// memory is promised to be available before adding `FieldLoc` + private var memoryInUse = false + /// Size of FieldLoc in memory + let size = MemoryLayout.stride + /// Memeory buffer + var memory: UnsafeMutableRawBufferPointer! + /// Capacity of the current buffer + var capacity: Int = 0 + /// Maximuim offset written to the class + var maxOffset: VOffset = 0 + /// number of fields written into the buffer + var numOfFields: Int = 0 + /// Last written Index + var writtenIndex: Int = 0 + + /// Creates the memory to store the buffer in + @usableFromInline + @inline(__always) + init() { + memory = UnsafeMutableRawBufferPointer.allocate( + byteCount: 0, + alignment: 0) + } + + @inline(__always) + deinit { + memory.deallocate() + } + + /// Builds a buffer with byte count of fieldloc.size * count of field numbers + /// - Parameter count: number of fields to be written + @inline(__always) + func start(count: Int) { + assert(count >= 0, "number of fields should NOT be negative") + let capacity = count &* size + ensure(space: capacity) + } + + /// Adds a FieldLoc into the buffer, which would track how many have been written, + /// and max offset + /// - Parameter loc: Location of encoded element + @inline(__always) + func add(loc: FieldLoc) { + memory.baseAddress?.advanced(by: writtenIndex).storeBytes( + of: loc, + as: FieldLoc.self) + writtenIndex = writtenIndex &+ size + numOfFields = numOfFields &+ 1 + maxOffset = max(loc.position, maxOffset) + } + + /// Clears the data stored related to the encoded buffer + @inline(__always) + func clear() { + maxOffset = 0 + numOfFields = 0 + writtenIndex = 0 + } + + /// Ensure that the buffer has enough space instead of recreating the buffer each time. + /// - Parameter space: space required for the new vtable + @inline(__always) + func ensure(space: Int) { + guard space &+ writtenIndex > capacity else { return } + memory.deallocate() + memory = UnsafeMutableRawBufferPointer.allocate( + byteCount: space, + alignment: size) + capacity = space + } + + /// Loads an object of type `FieldLoc` from buffer memory + /// - Parameter index: index of element + /// - Returns: a FieldLoc at index + @inline(__always) + func load(at index: Int) -> FieldLoc { + memory.load(fromByteOffset: index, as: FieldLoc.self) + } + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferObject.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferObject.swift new file mode 100644 index 0000000000..e836e6120c --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatBufferObject.swift @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// NativeStruct is a protocol that indicates if the struct is a native `swift` struct +/// since now we will be serializing native structs into the buffer. +public protocol NativeStruct {} + +/// FlatbuffersInitializable is a protocol that allows any object to be +/// Initialized from a ByteBuffer +public protocol FlatbuffersInitializable { + /// Any flatbuffers object that confirms to this protocol is going to be + /// initializable through this initializer + init(_ bb: ByteBuffer, o: Int32) +} + +/// FlatbufferObject structures all the Flatbuffers objects +public protocol FlatBufferObject: FlatbuffersInitializable { + var __buffer: ByteBuffer! { get } +} + +/// ``ObjectAPIPacker`` is a protocol that allows object to pack and unpack from a +/// ``NativeObject`` to a flatbuffers Object and vice versa. +public protocol ObjectAPIPacker { + /// associatedtype to the object that should be unpacked. + associatedtype T + + /// ``pack(_:obj:)-3ptws`` tries to pacs the variables of a native Object into the `ByteBuffer` by using + /// a FlatBufferBuilder + /// - Parameters: + /// - builder: FlatBufferBuilder that will host incoming data + /// - obj: Object of associatedtype to the current implementer + /// + /// ``pack(_:obj:)-3ptws`` can be called by passing through an already initialized ``FlatBufferBuilder`` + /// or it can be called by using the public API that will create a new ``FlatBufferBuilder`` + static func pack(_ builder: inout FlatBufferBuilder, obj: inout T?) -> Offset + + /// ``pack(_:obj:)-20ipk`` packs the variables of a native Object into the `ByteBuffer` by using + /// the FlatBufferBuilder + /// - Parameters: + /// - builder: FlatBufferBuilder that will host incoming data + /// - obj: Object of associatedtype to the current implementer + /// + /// ``pack(_:obj:)-20ipk`` can be called by passing through an already initialized ``FlatBufferBuilder`` + /// or it can be called by using the public API that will create a new ``FlatBufferBuilder`` + static func pack(_ builder: inout FlatBufferBuilder, obj: inout T) -> Offset + + /// ``unpack()`` unpacks a ``FlatBuffers`` object into a Native swift object. + mutating func unpack() -> T +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatBuffersUtils.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatBuffersUtils.swift new file mode 100644 index 0000000000..18c130f5a2 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatBuffersUtils.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// FlatBuffersUtils hosts some utility functions that might be useful +public enum FlatBuffersUtils { + + /// Gets the size of the prefix + /// - Parameter bb: Flatbuffer object + public static func getSizePrefix(bb: ByteBuffer) -> Int32 { + bb.read(def: Int32.self, position: bb.reader) + } + + /// Removes the prefix by duplicating the Flatbuffer this call is expensive since its + /// creates a new buffer use `readPrefixedSizeCheckedRoot` instead + /// unless a completely new buffer is required + /// - Parameter bb: Flatbuffer object + /// + /// + public static func removeSizePrefix(bb: ByteBuffer) -> ByteBuffer { + bb.duplicate(removing: MemoryLayout.size) + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/FlatbuffersErrors.swift b/submodules/TelegramCore/FlatBuffers/Sources/FlatbuffersErrors.swift new file mode 100644 index 0000000000..13207b53a9 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/FlatbuffersErrors.swift @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Collection of thrown from the Flatbuffer verifier +public enum FlatbuffersErrors: Error, Equatable { + + /// Thrown when trying to verify a buffer that doesnt have the length of an ID + case bufferDoesntContainID + /// Thrown when verifying a file id that doesnt match buffer id + case bufferIdDidntMatchPassedId + /// Prefixed size doesnt match the current (readable) buffer size + case prefixedSizeNotEqualToBufferSize + /// Thrown when buffer is bigger than the allowed 2GiB + case exceedsMaxSizeAllowed + /// Thrown when there is an missaligned pointer at position + /// of type + case missAlignedPointer(position: Int, type: String) + /// Thrown when trying to read a value that goes out of the + /// current buffer bounds + case outOfBounds(position: UInt, end: Int) + /// Thrown when the signed offset is out of the bounds of the + /// current buffer + case signedOffsetOutOfBounds(offset: Int, position: Int) + /// Thrown when a required field doesnt exist within the buffer + case requiredFieldDoesntExist(position: VOffset, name: String) + /// Thrown when a string is missing its NULL Terminator `\0`, + /// this can be disabled in the `VerifierOptions` + case missingNullTerminator(position: Int, str: String?) + /// Thrown when the verifier has reached the maximum tables allowed, + /// this can be disabled in the `VerifierOptions` + case maximumTables + /// Thrown when the verifier has reached the maximum depth allowed, + /// this can be disabled in the `VerifierOptions` + case maximumDepth + /// Thrown when the verifier is presented with an unknown union case + case unknownUnionCase + /// thrown when a value for a union is not found within the buffer + case valueNotFound(key: Int?, keyName: String, field: Int?, fieldName: String) + /// thrown when the size of the keys vector doesnt match fields vector + case unionVectorSize( + keyVectorSize: Int, + fieldVectorSize: Int, + unionKeyName: String, + fieldName: String) + case apparentSizeTooLarge + +} + +#if !os(WASI) + +extension FlatbuffersErrors { + public static func == ( + lhs: FlatbuffersErrors, + rhs: FlatbuffersErrors) -> Bool + { + lhs.localizedDescription == rhs.localizedDescription + } +} + +#endif diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Int+extension.swift b/submodules/TelegramCore/FlatBuffers/Sources/Int+extension.swift new file mode 100644 index 0000000000..62b5cd5cd1 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Int+extension.swift @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +extension Int { + + /// Moves the current int into the nearest power of two + /// + /// This is used since the UnsafeMutableRawPointer will face issues when writing/reading + /// if the buffer alignment exceeds that actual size of the buffer + var convertToPowerofTwo: Int { + guard self > 0 else { return 1 } + var n = UOffset(self) + + #if arch(arm) || arch(i386) + let max = UInt32(Int.max) + #else + let max = UInt32.max + #endif + + n -= 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + if n != max { + n += 1 + } + + return Int(n) + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Message.swift b/submodules/TelegramCore/FlatBuffers/Sources/Message.swift new file mode 100644 index 0000000000..8ccfca4186 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Message.swift @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// FlatBufferGRPCMessage protocol that should allow us to invoke +/// initializers directly from the GRPC generated code +public protocol FlatBufferGRPCMessage { + + /// Raw pointer which would be pointing to the beginning of the readable bytes + var rawPointer: UnsafeMutableRawPointer { get } + + /// Size of readable bytes in the buffer + var size: Int { get } + + init(byteBuffer: ByteBuffer) +} + +/// Message is a wrapper around Buffers to to able to send Flatbuffers `Buffers` through the +/// GRPC library +public struct Message: FlatBufferGRPCMessage { + internal var buffer: ByteBuffer + + /// Returns the an object of type T that would be read from the buffer + public var object: T { + T.init( + buffer, + o: Int32(buffer.read(def: UOffset.self, position: buffer.reader)) + + Int32(buffer.reader)) + } + + public var rawPointer: UnsafeMutableRawPointer { + buffer.memory.advanced(by: buffer.reader) } + + public var size: Int { Int(buffer.size) } + + /// Initializes the message with the type Flatbuffer.Bytebuffer that is transmitted over + /// GRPC + /// - Parameter byteBuffer: Flatbuffer ByteBuffer object + public init(byteBuffer: ByteBuffer) { + buffer = byteBuffer + } + + /// Initializes the message by copying the buffer to the message to be sent. + /// from the builder + /// - Parameter builder: FlatbufferBuilder that has the bytes created in + /// - Note: Use `builder.finish(offset)` before passing the builder without prefixing anything to it + public init(builder: inout FlatBufferBuilder) { + buffer = builder.sizedBuffer + builder.clear() + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Mutable.swift b/submodules/TelegramCore/FlatBuffers/Sources/Mutable.swift new file mode 100644 index 0000000000..307e9a927c --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Mutable.swift @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Mutable is a protocol that allows us to mutate Scalar values within a ``ByteBuffer`` +public protocol Mutable { + /// makes Flatbuffer accessed within the Protocol + var bb: ByteBuffer { get } + /// makes position of the ``Table``/``Struct`` accessed within the Protocol + var position: Int32 { get } +} + +extension Mutable { + + /// Mutates the memory in the buffer, this is only called from the access function of ``Table`` and ``struct`` + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + func mutate(value: T, o: Int32) -> Bool { + guard o != 0 else { return false } + bb.write(value: value, index: Int(o), direct: true) + return true + } +} + +extension Mutable where Self == Table { + + /// Mutates a value by calling mutate with respect to the position in a ``Table`` + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func mutate(_ value: T, index: Int32) -> Bool { + guard index != 0 else { return false } + return mutate(value: value, o: index + position) + } + + /// Directly mutates the element by calling mutate + /// + /// Mutates the Element at index ignoring the current position by calling mutate + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func directMutate(_ value: T, index: Int32) -> Bool { + mutate(value: value, o: index) + } +} + +extension Mutable where Self == Struct { + + /// Mutates a value by calling mutate with respect to the position in the struct + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func mutate(_ value: T, index: Int32) -> Bool { + mutate(value: value, o: index + position) + } + + /// Directly mutates the element by calling mutate + /// + /// Mutates the Element at index ignoring the current position by calling mutate + /// - Parameters: + /// - value: New value to be inserted to the buffer + /// - index: index of the Element + public func directMutate(_ value: T, index: Int32) -> Bool { + mutate(value: value, o: index) + } +} + +extension Struct: Mutable {} +extension Table: Mutable {} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/NativeObject.swift b/submodules/TelegramCore/FlatBuffers/Sources/NativeObject.swift new file mode 100644 index 0000000000..2ed83970ff --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/NativeObject.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// NativeObject is a protocol that all of the `Object-API` generated code should be +/// conforming to since it allows developers the ease of use to pack and unpack their +/// Flatbuffers objects +public protocol NativeObject {} + +extension NativeObject { + + /// Serialize is a helper function that serializes the data from the Object API to a bytebuffer directly th + /// - Parameter type: Type of the Flatbuffer object + /// - Returns: returns the encoded sized ByteBuffer + public func serialize(type: T.Type) -> ByteBuffer + where T.T == Self + { + var builder = FlatBufferBuilder(initialSize: 1024) + return serialize(builder: &builder, type: type.self) + } + + /// Serialize is a helper function that serializes the data from the Object API to a bytebuffer directly. + /// + /// - Parameters: + /// - builder: A FlatBufferBuilder + /// - type: Type of the Flatbuffer object + /// - Returns: returns the encoded sized ByteBuffer + /// - Note: The `serialize(builder:type)` can be considered as a function that allows you to create smaller builder instead of the default `1024`. + /// It can be considered less expensive in terms of memory allocation + public func serialize( + builder: inout FlatBufferBuilder, + type: T.Type) -> ByteBuffer where T.T == Self + { + var s = self + let root = type.pack(&builder, obj: &s) + builder.finish(offset: root) + return builder.sizedBuffer + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Offset.swift b/submodules/TelegramCore/FlatBuffers/Sources/Offset.swift new file mode 100644 index 0000000000..95ef9df993 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Offset.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Offset object for all the Objects that are written into the buffer +public struct Offset { + /// Offset of the object in the buffer + public var o: UOffset + /// Returns false if the offset is equal to zero + public var isEmpty: Bool { o == 0 } + + public init(offset: UOffset) { o = offset } + public init() { o = 0 } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Root.swift b/submodules/TelegramCore/FlatBuffers/Sources/Root.swift new file mode 100644 index 0000000000..8e606e6ccf --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Root.swift @@ -0,0 +1,116 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Takes in a prefixed sized buffer, where the prefixed size would be skipped. +/// And would verify that the buffer passed is a valid `Flatbuffers` Object. +/// - Parameters: +/// - byteBuffer: Buffer that needs to be checked and read +/// - options: Verifier options +/// - Throws: FlatbuffersErrors +/// - Returns: Returns a valid, checked Flatbuffers object +/// +/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in +/// the ``ByteBuffer`` and verifies the buffer by calling ``getCheckedRoot(byteBuffer:options:)`` +public func getPrefixedSizeCheckedRoot( + byteBuffer: inout ByteBuffer, + fileId: String? = nil, + options: VerifierOptions = .init()) throws -> T +{ + byteBuffer.skipPrefix() + return try getCheckedRoot( + byteBuffer: &byteBuffer, + fileId: fileId, + options: options) +} + +/// Takes in a prefixed sized buffer, where we check if the sized buffer is equal to prefix size. +/// And would verify that the buffer passed is a valid `Flatbuffers` Object. +/// - Parameters: +/// - byteBuffer: Buffer that needs to be checked and read +/// - options: Verifier options +/// - Throws: FlatbuffersErrors +/// - Returns: Returns a valid, checked Flatbuffers object +/// +/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in +/// the ``ByteBuffer`` and verifies the buffer by calling ``getCheckedRoot(byteBuffer:options:)`` +public func getCheckedPrefixedSizeRoot( + byteBuffer: inout ByteBuffer, + fileId: String? = nil, + options: VerifierOptions = .init()) throws -> T +{ + let prefix = byteBuffer.skipPrefix() + if prefix != byteBuffer.size { + throw FlatbuffersErrors.prefixedSizeNotEqualToBufferSize + } + return try getCheckedRoot( + byteBuffer: &byteBuffer, + fileId: fileId, + options: options) +} + +/// Takes in a prefixed sized buffer, where the prefixed size would be skipped. +/// Returns a `NON-Checked` flatbuffers object +/// - Parameter byteBuffer: Buffer that contains data +/// - Returns: Returns a Flatbuffers object +/// +/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in +/// the ``ByteBuffer`` and then calls ``getRoot(byteBuffer:)`` +public func getPrefixedSizeRoot( + byteBuffer: inout ByteBuffer) + -> T +{ + byteBuffer.skipPrefix() + return getRoot(byteBuffer: &byteBuffer) + +} + +/// Verifies that the buffer passed is a valid `Flatbuffers` Object. +/// - Parameters: +/// - byteBuffer: Buffer that needs to be checked and read +/// - options: Verifier options +/// - Throws: FlatbuffersErrors +/// - Returns: Returns a valid, checked Flatbuffers object +/// +/// ``getCheckedRoot(byteBuffer:options:)`` Takes in a ``ByteBuffer`` and verifies +/// that by creating a ``Verifier`` and checkes if all the `Bytes` and correctly aligned +/// and within the ``ByteBuffer`` range. +public func getCheckedRoot( + byteBuffer: inout ByteBuffer, + fileId: String? = nil, + options: VerifierOptions = .init()) throws -> T +{ + var verifier = try Verifier(buffer: &byteBuffer, options: options) + if let fileId = fileId { + try verifier.verify(id: fileId) + } + try ForwardOffset.verify(&verifier, at: 0, of: T.self) + return T.init( + byteBuffer, + o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) + + Int32(byteBuffer.reader)) +} + +/// Returns a `NON-Checked` flatbuffers object +/// - Parameter byteBuffer: Buffer that contains data +/// - Returns: Returns a Flatbuffers object +public func getRoot(byteBuffer: inout ByteBuffer) -> T { + T.init( + byteBuffer, + o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) + + Int32(byteBuffer.reader)) +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/String+extension.swift b/submodules/TelegramCore/FlatBuffers/Sources/String+extension.swift new file mode 100644 index 0000000000..de4f5f91f0 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/String+extension.swift @@ -0,0 +1,109 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +extension String: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer`, `missingNullTerminator` and `outOfBounds` + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + + let range = try String.verifyRange(&verifier, at: position, of: UInt8.self) + /// Safe &+ since we already check for overflow in verify range + let stringLen = range.start &+ range.count + + if stringLen >= verifier.capacity { + throw FlatbuffersErrors.outOfBounds( + position: UInt(clamping: stringLen.magnitude), + end: verifier.capacity) + } + + let isNullTerminated = verifier._buffer.read( + def: UInt8.self, + position: stringLen) == 0 + + if !verifier._options._ignoreMissingNullTerminators && !isNullTerminated { + let str = verifier._buffer.readString(at: range.start, count: range.count) + throw FlatbuffersErrors.missingNullTerminator( + position: position, + str: str) + } + } +} + +extension String: FlatbuffersInitializable { + + /// Initailizes a string from a Flatbuffers ByteBuffer + /// - Parameters: + /// - bb: ByteBuffer containing the readable string + /// - o: Current position + public init(_ bb: ByteBuffer, o: Int32) { + let v = Int(o) + let count = bb.read(def: Int32.self, position: v) + self = bb.readString( + at: MemoryLayout.size + v, + count: Int(count)) ?? "" + } +} + +extension String: ObjectAPIPacker { + + public static func pack( + _ builder: inout FlatBufferBuilder, + obj: inout String?) -> Offset + { + guard var obj = obj else { return Offset() } + return pack(&builder, obj: &obj) + } + + public static func pack( + _ builder: inout FlatBufferBuilder, + obj: inout String) -> Offset + { + builder.create(string: obj) + } + + public mutating func unpack() -> String { + self + } + +} + +extension String: NativeObject { + + public func serialize(type: T.Type) -> ByteBuffer + where T.T == Self + { + fatalError("serialize should never be called from string directly") + } + + public func serialize( + builder: inout FlatBufferBuilder, + type: T.Type) -> ByteBuffer where T.T == Self + { + fatalError("serialize should never be called from string directly") + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Struct.swift b/submodules/TelegramCore/FlatBuffers/Sources/Struct.swift new file mode 100644 index 0000000000..bbce8f978c --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Struct.swift @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Struct is a representation of a mutable `Flatbuffers` struct +/// since native structs are value types and cant be mutated +@frozen +public struct Struct { + + /// Hosting Bytebuffer + public private(set) var bb: ByteBuffer + /// Current position of the struct + public private(set) var position: Int32 + + /// Initializer for a mutable flatbuffers struct + /// - Parameters: + /// - bb: Current hosting Bytebuffer + /// - position: Current position for the struct in the ByteBuffer + public init(bb: ByteBuffer, position: Int32 = 0) { + self.bb = bb + self.position = position + } + + /// Reads data from the buffer directly at offset O + /// - Parameters: + /// - type: Type of data to be read + /// - o: Current offset of the data + /// - Returns: Data of Type T that conforms to type Scalar + public func readBuffer(of type: T.Type, at o: Int32) -> T { + let r = bb.read(def: T.self, position: Int(o + position)) + return r + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Table.swift b/submodules/TelegramCore/FlatBuffers/Sources/Table.swift new file mode 100644 index 0000000000..02a2e6f2cd --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Table.swift @@ -0,0 +1,236 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// `Table` is a Flatbuffers object that can read, +/// mutate scalar fields within a valid flatbuffers buffer +@frozen +public struct Table { + + /// Hosting Bytebuffer + public private(set) var bb: ByteBuffer + /// Current position of the table within the buffer + public private(set) var position: Int32 + + /// Initializer for the table interface to allow generated code to read + /// data from memory + /// - Parameters: + /// - bb: ByteBuffer that stores data + /// - position: Current table position + /// - Note: This will `CRASH` if read on a big endian machine + public init(bb: ByteBuffer, position: Int32 = 0) { + guard isLitteEndian else { + fatalError( + "Reading/Writing a buffer in big endian machine is not supported on swift") + } + self.bb = bb + self.position = position + } + + /// Gets the offset of the current field within the buffer by reading + /// the vtable + /// - Parameter o: current offset + /// - Returns: offset of field within buffer + public func offset(_ o: Int32) -> Int32 { + let vtable = position - bb.read(def: Int32.self, position: Int(position)) + return o < bb + .read(def: VOffset.self, position: Int(vtable)) ? Int32(bb.read( + def: Int16.self, + position: Int(vtable + o))) : 0 + } + + /// Gets the indirect offset of the current stored object + /// (applicable only for object arrays) + /// - Parameter o: current offset + /// - Returns: offset of field within buffer + public func indirect(_ o: Int32) -> Int32 { + o + bb.read(def: Int32.self, position: Int(o)) + } + + /// String reads from the buffer with respect to position of the current table. + /// - Parameter offset: Offset of the string + public func string(at offset: Int32) -> String? { + directString(at: offset + position) + } + + /// Direct string reads from the buffer disregarding the position of the table. + /// It would be preferable to use string unless the current position of the table + /// is not needed + /// - Parameter offset: Offset of the string + public func directString(at offset: Int32) -> String? { + var offset = offset + offset += bb.read(def: Int32.self, position: Int(offset)) + let count = bb.read(def: Int32.self, position: Int(offset)) + let position = Int(offset) + MemoryLayout.size + return bb.readString(at: position, count: Int(count)) + } + + /// Reads from the buffer with respect to the position in the table. + /// - Parameters: + /// - type: Type of Element that needs to be read from the buffer + /// - o: Offset of the Element + public func readBuffer(of type: T.Type, at o: Int32) -> T { + directRead(of: T.self, offset: o + position) + } + + /// Reads from the buffer disregarding the position of the table. + /// It would be used when reading from an + /// ``` + /// let offset = __t.offset(10) + /// //Only used when the we already know what is the + /// // position in the table since __t.vector(at:) + /// // returns the index with respect to the position + /// __t.directRead(of: Byte.self, + /// offset: __t.vector(at: offset) + index * 1) + /// ``` + /// - Parameters: + /// - type: Type of Element that needs to be read from the buffer + /// - o: Offset of the Element + public func directRead(of type: T.Type, offset o: Int32) -> T { + let r = bb.read(def: T.self, position: Int(o)) + return r + } + + /// Returns that current `Union` object at a specific offset + /// by adding offset to the current position of table + /// - Parameter o: offset + /// - Returns: A flatbuffers object + public func union(_ o: Int32) -> T { + let o = o + position + return directUnion(o) + } + + /// Returns a direct `Union` object at a specific offset + /// - Parameter o: offset + /// - Returns: A flatbuffers object + public func directUnion(_ o: Int32) -> T { + T.init(bb, o: o + bb.read(def: Int32.self, position: Int(o))) + } + + /// Returns a vector of type T at a specific offset + /// This should only be used by `Scalars` + /// - Parameter off: Readable offset + /// - Returns: Returns a vector of type [T] + public func getVector(at off: Int32) -> [T]? { + let o = offset(off) + guard o != 0 else { return nil } + return bb.readSlice(index: Int(vector(at: o)), count: Int(vector(count: o))) + } + + /// Vector count gets the count of Elements within the array + /// - Parameter o: start offset of the vector + /// - returns: Count of elements + public func vector(count o: Int32) -> Int32 { + var o = o + o += position + o += bb.read(def: Int32.self, position: Int(o)) + return bb.read(def: Int32.self, position: Int(o)) + } + + /// Vector start index in the buffer + /// - Parameter o:start offset of the vector + /// - returns: the start index of the vector + public func vector(at o: Int32) -> Int32 { + var o = o + o += position + return o + bb.read(def: Int32.self, position: Int(o)) + 4 + } + + /// Reading an indirect offset of a table. + /// - Parameters: + /// - o: position within the buffer + /// - fbb: ByteBuffer + /// - Returns: table offset + static public func indirect(_ o: Int32, _ fbb: ByteBuffer) -> Int32 { + o + fbb.read(def: Int32.self, position: Int(o)) + } + + /// Gets a vtable value according to an table Offset and a field offset + /// - Parameters: + /// - o: offset relative to entire buffer + /// - vOffset: Field offset within a vtable + /// - fbb: ByteBuffer + /// - Returns: an position of a field + static public func offset( + _ o: Int32, + vOffset: Int32, + fbb: ByteBuffer) -> Int32 + { + let vTable = Int32(fbb.capacity) - o + return vTable + Int32(fbb.read( + def: Int16.self, + position: Int(vTable + vOffset - fbb.read( + def: Int32.self, + position: Int(vTable))))) + } + + /// Compares two objects at offset A and offset B within a ByteBuffer + /// - Parameters: + /// - off1: first offset to compare + /// - off2: second offset to compare + /// - fbb: Bytebuffer + /// - Returns: returns the difference between + static public func compare( + _ off1: Int32, + _ off2: Int32, + fbb: ByteBuffer) -> Int32 + { + let memorySize = Int32(MemoryLayout.size) + let _off1 = off1 + fbb.read(def: Int32.self, position: Int(off1)) + let _off2 = off2 + fbb.read(def: Int32.self, position: Int(off2)) + let len1 = fbb.read(def: Int32.self, position: Int(_off1)) + let len2 = fbb.read(def: Int32.self, position: Int(_off2)) + let startPos1 = _off1 + memorySize + let startPos2 = _off2 + memorySize + let minValue = min(len1, len2) + for i in 0...minValue { + let b1 = fbb.read(def: Int8.self, position: Int(i + startPos1)) + let b2 = fbb.read(def: Int8.self, position: Int(i + startPos2)) + if b1 != b2 { + return Int32(b2 - b1) + } + } + return len1 - len2 + } + + /// Compares two objects at offset A and array of `Bytes` within a ByteBuffer + /// - Parameters: + /// - off1: Offset to compare to + /// - key: bytes array to compare to + /// - fbb: Bytebuffer + /// - Returns: returns the difference between + static public func compare( + _ off1: Int32, + _ key: [Byte], + fbb: ByteBuffer) -> Int32 + { + let memorySize = Int32(MemoryLayout.size) + let _off1 = off1 + fbb.read(def: Int32.self, position: Int(off1)) + let len1 = fbb.read(def: Int32.self, position: Int(_off1)) + let len2 = Int32(key.count) + let startPos1 = _off1 + memorySize + let minValue = min(len1, len2) + for i in 0.. Int? { + if field >= _vtableLength { + return nil + } + + /// Reading the offset for the field needs to be read. + let offset: VOffset = try _verifier.getValue( + at: Int(clamping: _vtable &+ Int(field))) + + if offset > 0 { + return Int(clamping: _position &+ Int(offset)) + } + return nil + } + + /// Visits all the fields within the table to validate the integrity + /// of the data + /// - Parameters: + /// - field: voffset of the current field to be read + /// - fieldName: fieldname to report data Errors. + /// - required: If the field has to be available in the buffer + /// - type: Type of field to be read + /// - Throws: A `FlatbuffersErrors` where the field is corrupt + public mutating func visit( + field: VOffset, + fieldName: String, + required: Bool, + type: T.Type) throws where T: Verifiable + { + let derefValue = try dereference(field) + + if let value = derefValue { + try T.verify(&_verifier, at: value, of: T.self) + return + } + if required { + throw FlatbuffersErrors.requiredFieldDoesntExist( + position: field, + name: fieldName) + } + } + + /// Visits all the fields for a union object within the table to + /// validate the integrity of the data + /// - Parameters: + /// - key: Current Key Voffset + /// - field: Current field Voffset + /// - unionKeyName: Union key name + /// - fieldName: Field key name + /// - required: indicates if an object is required to be present + /// - completion: Completion is a handler that WILL be called in the generated + /// - Throws: A `FlatbuffersErrors` where the field is corrupt + public mutating func visit( + unionKey key: VOffset, + unionField field: VOffset, + unionKeyName: String, + fieldName: String, + required: Bool, + completion: @escaping (inout Verifier, T, Int) throws -> Void) throws + where T: UnionEnum + { + let keyPos = try dereference(key) + let valPos = try dereference(field) + + if keyPos == nil && valPos == nil { + if required { + throw FlatbuffersErrors.requiredFieldDoesntExist( + position: key, + name: unionKeyName) + } + return + } + + if let _key = keyPos, + let _val = valPos + { + /// verifiying that the key is within the buffer + try T.T.verify(&_verifier, at: _key, of: T.T.self) + guard let _enum = try T.init(value: _verifier._buffer.read( + def: T.T.self, + position: _key)) else + { + throw FlatbuffersErrors.unknownUnionCase + } + /// we are assuming that Unions will always be of type Uint8 + try completion( + &_verifier, + _enum, + _val) + return + } + throw FlatbuffersErrors.valueNotFound( + key: keyPos, + keyName: unionKeyName, + field: valPos, + fieldName: fieldName) + } + + /// Visits and validates all the objects within a union vector + /// - Parameters: + /// - key: Current Key Voffset + /// - field: Current field Voffset + /// - unionKeyName: Union key name + /// - fieldName: Field key name + /// - required: indicates if an object is required to be present + /// - completion: Completion is a handler that WILL be called in the generated + /// - Throws: A `FlatbuffersErrors` where the field is corrupt + public mutating func visitUnionVector( + unionKey key: VOffset, + unionField field: VOffset, + unionKeyName: String, + fieldName: String, + required: Bool, + completion: @escaping (inout Verifier, T, Int) throws -> Void) throws + where T: UnionEnum + { + let keyVectorPosition = try dereference(key) + let offsetVectorPosition = try dereference(field) + + if let keyPos = keyVectorPosition, + let valPos = offsetVectorPosition + { + try UnionVector.verify( + &_verifier, + keyPosition: keyPos, + fieldPosition: valPos, + unionKeyName: unionKeyName, + fieldName: fieldName, + completion: completion) + return + } + if required { + throw FlatbuffersErrors.requiredFieldDoesntExist( + position: field, + name: fieldName) + } + } + + /// Finishs the current Table verifier, and subtracts the current + /// table from the incremented depth. + public mutating func finish() { + _verifier.finish() + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/VeriferOptions.swift b/submodules/TelegramCore/FlatBuffers/Sources/VeriferOptions.swift new file mode 100644 index 0000000000..a7f11e243d --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/VeriferOptions.swift @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// `VerifierOptions` is a set of options to verify a flatbuffer +public struct VerifierOptions { + + /// Maximum `Apparent` size if the buffer can be expanded into a DAG tree + internal var _maxApparentSize: UOffset + + /// Maximum table count allowed in a buffer + internal var _maxTableCount: UOffset + + /// Maximum depth allowed in a buffer + internal var _maxDepth: UOffset + + /// Ignoring missing null terminals in strings + internal var _ignoreMissingNullTerminators: Bool + + /// initializes the set of options for the verifier + /// - Parameters: + /// - maxDepth: Maximum depth allowed in a buffer + /// - maxTableCount: Maximum table count allowed in a buffer + /// - maxApparentSize: Maximum `Apparent` size if the buffer can be expanded into a DAG tree + /// - ignoreMissingNullTerminators: Ignoring missing null terminals in strings *Currently not supported in swift* + public init( + maxDepth: UOffset = 64, + maxTableCount: UOffset = 1000000, + maxApparentSize: UOffset = 1 << 31, + ignoreMissingNullTerminators: Bool = false) + { + _maxDepth = maxDepth + _maxTableCount = maxTableCount + _maxApparentSize = maxApparentSize + _ignoreMissingNullTerminators = ignoreMissingNullTerminators + } + +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Verifiable.swift b/submodules/TelegramCore/FlatBuffers/Sources/Verifiable.swift new file mode 100644 index 0000000000..3d3e08f0f8 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Verifiable.swift @@ -0,0 +1,213 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Verifiable is a protocol all swift flatbuffers object should conform to, +/// since swift is similar to `cpp` and `rust` where the data is read directly +/// from `unsafeMemory` thus the need to verify if the buffer received is a valid one +public protocol Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable +} + +extension Verifiable { + + /// Verifies if the current range to be read is within the bounds of the buffer, + /// and if the range is properly aligned + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Erros thrown from `isAligned` & `rangeInBuffer` + /// - Returns: a tuple of the start position and the count of objects within the range + @discardableResult + public static func verifyRange( + _ verifier: inout Verifier, + at position: Int, of type: T.Type) throws -> (start: Int, count: Int) + { + let len: UOffset = try verifier.getValue(at: position) + let intLen = Int(len) + let start = Int(clamping: (position &+ MemoryLayout.size).magnitude) + try verifier.isAligned(position: start, type: type.self) + try verifier.rangeInBuffer(position: start, size: intLen) + return (start, intLen) + } +} + +extension Verifiable where Self: Scalar { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + try verifier.inBuffer(position: position, of: type.self) + } +} + +// MARK: - ForwardOffset + +/// ForwardOffset is a container to wrap around the Generic type to be verified +/// from the flatbuffers object. +public enum ForwardOffset: Verifiable where U: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + let offset: UOffset = try verifier.getValue(at: position) + let nextOffset = Int(clamping: (Int(offset) &+ position).magnitude) + try U.verify(&verifier, at: nextOffset, of: U.self) + } +} + +// MARK: - Vector + +/// Vector is a container to wrap around the Generic type to be verified +/// from the flatbuffers object. +public enum Vector: Verifiable where U: Verifiable, S: Verifiable { + + /// Verifies that the current value is which the bounds of the buffer, and if + /// the current `Value` is aligned properly + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - position: Current position within the buffer + /// - type: The type of the object to be verified + /// - Throws: Errors coming from `inBuffer` function + public static func verify( + _ verifier: inout Verifier, + at position: Int, + of type: T.Type) throws where T: Verifiable + { + /// checks if the next verification type S is equal to U of type forwardOffset + /// This had to be done since I couldnt find a solution for duplicate call functions + /// A fix will be appreciated + if U.self is ForwardOffset.Type { + let range = try verifyRange(&verifier, at: position, of: UOffset.self) + for index in stride( + from: range.start, + to: Int( + clamping: range + .start &+ (range.count &* MemoryLayout.size)), + by: MemoryLayout.size) + { + try U.verify(&verifier, at: index, of: U.self) + } + } else { + try S.verifyRange(&verifier, at: position, of: S.self) + } + } +} + +// MARK: - UnionVector + +/// UnionVector is a container to wrap around the Generic type to be verified +/// from the flatbuffers object. +public enum UnionVector where S: UnionEnum { + + /// Completion handler for the function Verify, that passes the verifier + /// enum type and position of union field + public typealias Completion = (inout Verifier, S, Int) throws -> Void + + /// Verifies if the current range to be read is within the bounds of the buffer, + /// and if the range is properly aligned. It also verifies if the union type is a + /// *valid/supported* union type. + /// - Parameters: + /// - verifier: Verifier that hosts the buffer + /// - keyPosition: Current union key position within the buffer + /// - fieldPosition: Current union field position within the buffer + /// - unionKeyName: Name of key to written if error is presented + /// - fieldName: Name of field to written if error is presented + /// - completion: Completion is a handler that WILL be called in the generated + /// code to verify the actual objects + /// - Throws: FlatbuffersErrors + public static func verify( + _ verifier: inout Verifier, + keyPosition: Int, + fieldPosition: Int, + unionKeyName: String, + fieldName: String, + completion: @escaping Completion) throws + { + /// Get offset for union key vectors and offset vectors + let keyOffset: UOffset = try verifier.getValue(at: keyPosition) + let fieldOffset: UOffset = try verifier.getValue(at: fieldPosition) + + /// Check if values are within the buffer, returns the start position of vectors, and vector counts + /// Using &+ is safe since we already verified that the value is within the buffer, where the max is + /// going to be 2Gib and swift supports Int64 by default + let keysRange = try S.T.verifyRange( + &verifier, + at: Int(keyOffset) &+ keyPosition, + of: S.T.self) + let offsetsRange = try UOffset.verifyRange( + &verifier, + at: Int(fieldOffset) &+ fieldPosition, + of: UOffset.self) + + guard keysRange.count == offsetsRange.count else { + throw FlatbuffersErrors.unionVectorSize( + keyVectorSize: keysRange.count, + fieldVectorSize: offsetsRange.count, + unionKeyName: unionKeyName, + fieldName: fieldName) + } + + var count = 0 + /// Iterate over the vector of keys and offsets. + while count < keysRange.count { + + /// index of readable enum value in array + let keysIndex = MemoryLayout.size * count + guard let _enum = try S.init(value: verifier._buffer.read( + def: S.T.self, + position: keysRange.start + keysIndex)) else + { + throw FlatbuffersErrors.unknownUnionCase + } + /// index of readable offset value in array + let fieldIndex = MemoryLayout.size * count + try completion(&verifier, _enum, offsetsRange.start + fieldIndex) + count += 1 + } + } +} diff --git a/submodules/TelegramCore/FlatBuffers/Sources/Verifier.swift b/submodules/TelegramCore/FlatBuffers/Sources/Verifier.swift new file mode 100644 index 0000000000..0d52ccd8a8 --- /dev/null +++ b/submodules/TelegramCore/FlatBuffers/Sources/Verifier.swift @@ -0,0 +1,238 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/// Verifier that check if the buffer passed into it is a valid, +/// safe, aligned Flatbuffers object since swift read from `unsafeMemory` +public struct Verifier { + + /// Flag to check for alignment if true + fileprivate let _checkAlignment: Bool + /// Storage for all changing values within the verifier + private let storage: Storage + /// Current verifiable ByteBuffer + internal var _buffer: ByteBuffer + /// Options for verification + internal let _options: VerifierOptions + + /// Current stored capacity within the verifier + var capacity: Int { + storage.capacity + } + + /// Current depth of verifier + var depth: Int { + storage.depth + } + + /// Current table count + var tableCount: Int { + storage.tableCount + } + + + /// Initializer for the verifier + /// - Parameters: + /// - buffer: Bytebuffer that is required to be verified + /// - options: `VerifierOptions` that set the rule for some of the verification done + /// - checkAlignment: If alignment check is required to be preformed + /// - Throws: `exceedsMaxSizeAllowed` if capacity of the buffer is more than 2GiB + public init( + buffer: inout ByteBuffer, + options: VerifierOptions = .init(), + checkAlignment: Bool = true) throws + { + guard buffer.capacity < FlatBufferMaxSize else { + throw FlatbuffersErrors.exceedsMaxSizeAllowed + } + + _buffer = buffer + _checkAlignment = checkAlignment + _options = options + storage = Storage(capacity: buffer.capacity) + } + + /// Resets the verifier to initial state + public func reset() { + storage.depth = 0 + storage.tableCount = 0 + } + + /// Checks if the value of type `T` is aligned properly in the buffer + /// - Parameters: + /// - position: Current position + /// - type: Type of value to check + /// - Throws: `missAlignedPointer` if the pointer is not aligned properly + public func isAligned(position: Int, type: T.Type) throws { + + /// If check alignment is false this mutating function doesnt continue + if !_checkAlignment { return } + + /// advance pointer to position X + let ptr = _buffer._storage.memory.advanced(by: position) + /// Check if the pointer is aligned + if Int(bitPattern: ptr) & (MemoryLayout.alignment &- 1) == 0 { + return + } + + throw FlatbuffersErrors.missAlignedPointer( + position: position, + type: String(describing: T.self)) + } + + /// Checks if the value of Size "X" is within the range of the buffer + /// - Parameters: + /// - position: Current position to be read + /// - size: `Byte` Size of readable object within the buffer + /// - Throws: `outOfBounds` if the value is out of the bounds of the buffer + /// and `apparentSizeTooLarge` if the apparent size is bigger than the one specified + /// in `VerifierOptions` + public func rangeInBuffer(position: Int, size: Int) throws { + let end = UInt(clamping: (position &+ size).magnitude) + if end > _buffer.capacity { + throw FlatbuffersErrors.outOfBounds(position: end, end: storage.capacity) + } + storage.apparentSize = storage.apparentSize &+ UInt32(size) + if storage.apparentSize > _options._maxApparentSize { + throw FlatbuffersErrors.apparentSizeTooLarge + } + } + + /// Validates if a value of type `T` is aligned and within the bounds of + /// the buffer + /// - Parameters: + /// - position: Current readable position + /// - type: Type of value to check + /// - Throws: FlatbuffersErrors + public func inBuffer(position: Int, of type: T.Type) throws { + try isAligned(position: position, type: type) + try rangeInBuffer(position: position, size: MemoryLayout.size) + } + + /// Visits a table at the current position and validates if the table meets + /// the rules specified in the `VerifierOptions` + /// - Parameter position: Current position to be read + /// - Throws: FlatbuffersErrors + /// - Returns: A `TableVerifier` at the current readable table + public mutating func visitTable(at position: Int) throws -> TableVerifier { + let vtablePosition = try derefOffset(position: position) + let vtableLength: VOffset = try getValue(at: vtablePosition) + + let length = Int(vtableLength) + try isAligned( + position: Int(clamping: (vtablePosition + length).magnitude), + type: VOffset.self) + try rangeInBuffer(position: vtablePosition, size: length) + + storage.tableCount += 1 + + if storage.tableCount > _options._maxTableCount { + throw FlatbuffersErrors.maximumTables + } + + storage.depth += 1 + + if storage.depth > _options._maxDepth { + throw FlatbuffersErrors.maximumDepth + } + + return TableVerifier( + position: position, + vtable: vtablePosition, + vtableLength: length, + verifier: &self) + } + + /// Validates if a value of type `T` is within the buffer and returns it + /// - Parameter position: Current position to be read + /// - Throws: `inBuffer` errors + /// - Returns: a value of type `T` usually a `VTable` or a table offset + internal func getValue(at position: Int) throws -> T { + try inBuffer(position: position, of: T.self) + return _buffer.read(def: T.self, position: position) + } + + /// derefrences an offset within a vtable to get the position of the field + /// in the bytebuffer + /// - Parameter position: Current readable position + /// - Throws: `inBuffer` errors & `signedOffsetOutOfBounds` + /// - Returns: Current readable position for a field + @inline(__always) + internal func derefOffset(position: Int) throws -> Int { + try inBuffer(position: position, of: Int32.self) + + let offset = _buffer.read(def: Int32.self, position: position) + // switching to int32 since swift's default Int is int64 + // this should be safe since we already checked if its within + // the buffer + let _int32Position = UInt32(position) + + let reportedOverflow: (partialValue: UInt32, overflow: Bool) + if offset > 0 { + reportedOverflow = _int32Position + .subtractingReportingOverflow(offset.magnitude) + } else { + reportedOverflow = _int32Position + .addingReportingOverflow(offset.magnitude) + } + + /// since `subtractingReportingOverflow` & `addingReportingOverflow` returns true, + /// if there is overflow we return failure + if reportedOverflow.overflow || reportedOverflow.partialValue > _buffer + .capacity + { + throw FlatbuffersErrors.signedOffsetOutOfBounds( + offset: Int(offset), + position: position) + } + + return Int(reportedOverflow.partialValue) + } + + /// finishes the current iteration of verification on an object + internal func finish() { + storage.depth -= 1 + } + + @inline(__always) + func verify(id: String) throws { + let size = MemoryLayout.size + guard storage.capacity >= (size * 2) else { + throw FlatbuffersErrors.bufferDoesntContainID + } + let str = _buffer.readString(at: size, count: size) + if id == str { + return + } + throw FlatbuffersErrors.bufferIdDidntMatchPassedId + } + + final private class Storage { + /// Current ApparentSize + fileprivate var apparentSize: UOffset = 0 + /// Amount of tables present within a buffer + fileprivate var tableCount = 0 + /// Capacity of the current buffer + fileprivate let capacity: Int + /// Current reached depth within the buffer + fileprivate var depth = 0 + + init(capacity: Int) { + self.capacity = capacity + } + } +} diff --git a/submodules/TelegramCore/FlatSerialization/BUILD b/submodules/TelegramCore/FlatSerialization/BUILD new file mode 100644 index 0000000000..e18b8eb401 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/BUILD @@ -0,0 +1,56 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +models = glob([ + "Models/*.fbs", +]) + +model_names = [ + f[7:-4] for f in models +] + +generated_models = [ "{}_generated.swift".format(name) for name in model_names ] +flatc_input = " ".join([ "$(location Models/{}.fbs)".format(name) for name in model_names ]) + +genrule( + name = "GenerateModels", + srcs = models, + tools = [ + "//third-party/flatc:flatc_bin" + ], + cmd_bash = + """ + set -ex + FLATC="$$(pwd)/$(location //third-party/flatc:flatc_bin)" + + BUILD_DIR="$(RULEDIR)/build" + rm -rf "$$BUILD_DIR" + mkdir -p "$$BUILD_DIR" + + "$$FLATC" --swift -o "$$BUILD_DIR" {flatc_input} + """.format( + flatc_input=flatc_input + ) + "\n" + "\n".join([ + """ + cp "$$BUILD_DIR/{name}_generated.swift" "$(location {name}_generated.swift)" + """.format(name=name) for name in model_names + ]), + outs = generated_models, + visibility = [ + "//visibility:public", + ] +) + +swift_library( + name = "FlatSerialization", + module_name = "FlatSerialization", + srcs = generated_models, + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/TelegramCore/FlatBuffers", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramCore/FlatSerialization/Models/MediaId.fbs b/submodules/TelegramCore/FlatSerialization/Models/MediaId.fbs new file mode 100644 index 0000000000..5792a89655 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/MediaId.fbs @@ -0,0 +1,6 @@ +namespace TelegramCore; + +struct MediaId { + namespace: int; + id: int64; +} diff --git a/submodules/TelegramCore/FlatSerialization/Models/PixelDimensions.fbs b/submodules/TelegramCore/FlatSerialization/Models/PixelDimensions.fbs new file mode 100644 index 0000000000..dc1be6bcc0 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/PixelDimensions.fbs @@ -0,0 +1,6 @@ +namespace TelegramCore; + +struct PixelDimensions { + width: int; + height: int; +} diff --git a/submodules/TelegramCore/FlatSerialization/Models/TelegramMediaFile.fbs b/submodules/TelegramCore/FlatSerialization/Models/TelegramMediaFile.fbs new file mode 100644 index 0000000000..0eefc28bfb --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/TelegramMediaFile.fbs @@ -0,0 +1,11 @@ +include "MediaId.fbs"; +include "VideoThumbnail.fbs"; + +namespace TelegramCore; + +table TelegramMediaFile { + id: MediaId; + videoThumbnails: [VideoThumbnail]; +} + +root_type TelegramMediaFile; diff --git a/submodules/TelegramCore/FlatSerialization/Models/VideoThumbnail.fbs b/submodules/TelegramCore/FlatSerialization/Models/VideoThumbnail.fbs new file mode 100644 index 0000000000..62a84090f9 --- /dev/null +++ b/submodules/TelegramCore/FlatSerialization/Models/VideoThumbnail.fbs @@ -0,0 +1,7 @@ +include "PixelDimensions.fbs"; + +namespace TelegramCore; + +struct VideoThumbnail { + dimensions: PixelDimensions; +} diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 65fe389de6..c21584858e 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -1005,8 +1005,11 @@ private final class CallSessionManagerContext { if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { switch context.state { - case .accepting, .active, .dropping, .requesting, .ringing, .terminated, .requested, .switchedToConference: + case .accepting, .dropping, .requesting, .ringing, .terminated, .requested, .switchedToConference: break + case let .active(id, accessHash, beginTimestamp, key, keyId, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, _): + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: beginTimestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) + self.contextUpdated(internalId: internalId) case let .awaitingConfirmation(_, accessHash, gAHash, b, config): if let (key, calculatedKeyId, keyVisualHash) = self.makeSessionEncryptionKey(config: config, gAHash: gAHash, b: b, gA: gAOrB.makeData()) { if keyFingerprint == calculatedKeyId { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 4e0c73441d..0d478915d6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -1,4 +1,4 @@ - import Foundation +import Foundation import Postbox private let typeFileName: Int32 = 0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index e085be0fa4..9001c25b2d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -1117,10 +1117,15 @@ public final class GroupCallParticipantsContext { var pendingMuteStateChanges: [PeerId: MuteStateChange] = [:] + var hasLocalVideo: PeerId? = nil + var isEmpty: Bool { if !self.pendingMuteStateChanges.isEmpty { return false } + if self.hasLocalVideo != nil { + return false + } return true } } @@ -1254,6 +1259,12 @@ public final class GroupCallParticipantsContext { publicState.participants[i].raiseHandRating = nil sortAgain = true } + + if let hasLocalVideoPeerId = state.overlayState.hasLocalVideo, hasLocalVideoPeerId == publicState.participants[i].peer.id { + if publicState.participants[i].videoDescription == nil { + publicState.participants[i].videoDescription = GroupCallParticipantsContext.Participant.VideoDescription(endpointId: "_local", ssrcGroups: [], audioSsrc: nil, isPaused: false) + } + } } if sortAgain { publicState.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: publicState.sortAscending) }) @@ -1943,6 +1954,10 @@ public final class GroupCallParticipantsContext { self.localIsVideoPaused = isVideoPaused self.localIsPresentationPaused = isPresentationPaused + if let isVideoMuted { + self.stateValue.overlayState.hasLocalVideo = isVideoMuted ? nil : peerId + } + let disposable = MetaDisposable() let account = self.account diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 4c731b68f8..bb8cb19604 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -86,7 +86,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: ComponentTransition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], isAnimatedOutToGroupCall: Bool, transition: ComponentTransition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -95,7 +95,9 @@ final class ButtonGroupView: OverlayMaskContainerView { let buttonNoticeSpacing: CGFloat = 16.0 let controlsHiddenNoticeSpacing: CGFloat = 0.0 var nextNoticeY: CGFloat - if controlsHidden { + if isAnimatedOutToGroupCall { + nextNoticeY = size.height + 4.0 + } else if controlsHidden { nextNoticeY = size.height - insets.bottom - 4.0 } else { nextNoticeY = size.height - insets.bottom - 52.0 - buttonSize - buttonNoticeSpacing @@ -130,9 +132,11 @@ final class ButtonGroupView: OverlayMaskContainerView { } } let noticeSize = noticeView.update(icon: notice.icon, text: notice.text, constrainedWidth: size.width - insets.left * 2.0 - 16.0 * 2.0, transition: noticeTransition) - let noticeFrame = CGRect(origin: CGPoint(x: floor((size.width - noticeSize.width) * 0.5), y: nextNoticeY - noticeSize.height), size: noticeSize) + let noticeFrame = CGRect(origin: CGPoint(x: floor((size.width - noticeSize.width) * 0.5), y: isAnimatedOutToGroupCall ? nextNoticeY : (nextNoticeY - noticeSize.height)), size: noticeSize) noticesHeight += noticeSize.height - nextNoticeY -= noticeSize.height + noticeSpacing + if !isAnimatedOutToGroupCall { + nextNoticeY -= noticeSize.height + noticeSpacing + } noticeTransition.setFrame(view: noticeView, frame: noticeFrame) if animateIn, !transition.animation.isImmediate { @@ -142,6 +146,9 @@ final class ButtonGroupView: OverlayMaskContainerView { if noticesHeight != 0.0 { noticesHeight += 5.0 } + if isAnimatedOutToGroupCall { + noticesHeight = 0.0 + } var removedNoticeIds: [AnyHashable] = [] for (id, noticeView) in self.noticeViews { if !validNoticeIds.contains(id) { @@ -161,7 +168,7 @@ final class ButtonGroupView: OverlayMaskContainerView { let buttonY: CGFloat let resultHeight: CGFloat - if controlsHidden { + if controlsHidden || isAnimatedOutToGroupCall { buttonY = size.height + 12.0 resultHeight = insets.bottom + 4.0 + noticesHeight } else { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift index 68addda2d2..4eb3db1ff5 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -29,7 +29,7 @@ public func resolveCallVideoRotationAngle(angle: Float, followsDeviceOrientation return (angle + interfaceAngle).truncatingRemainder(dividingBy: Float.pi * 2.0) } -private final class VideoContainerLayer: SimpleLayer { +final class VideoContainerLayer: SimpleLayer { let contentsLayer: SimpleLayer override init() { @@ -129,11 +129,16 @@ final class VideoContainerView: HighlightTrackingButton { let key: Key - private let videoContainerLayer: VideoContainerLayer + let videoContainerLayer: VideoContainerLayer + var videoContainerLayerTaken: Bool = false private var videoLayer: PrivateCallVideoLayer private var disappearingVideoLayer: DisappearingVideo? + var currentVideoOutput: VideoSource.Output? { + return self.videoLayer.video + } + let blurredContainerLayer: SimpleLayer private let shadowContainer: SimpleLayer @@ -245,7 +250,7 @@ final class VideoContainerView: HighlightTrackingButton { self.layer.addSublayer(self.shadowContainer) self.highligthedChanged = { [weak self] highlighted in - guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty else { + guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty, !self.videoContainerLayerTaken else { return } var highlightedState = false @@ -316,6 +321,10 @@ final class VideoContainerView: HighlightTrackingButton { } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + if self.videoContainerLayerTaken { + return + } + switch recognizer.state { case .began, .changed: self.dragVelocity = CGPoint() @@ -549,6 +558,9 @@ final class VideoContainerView: HighlightTrackingButton { } private func update(previousParams: Params?, params: Params, transition: ComponentTransition) { + if self.videoContainerLayerTaken { + return + } guard let videoMetrics = self.videoMetrics else { return } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 8073195d30..5205aba59e 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -199,6 +199,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private var isUpdating: Bool = false + private var isAnimatedOutToGroupCall: Bool = false + private var animateOutToGroupCallCompletion: (() -> Void)? + private var canAnimateAudioLevel: Bool = false private var displayEmojiTooltip: Bool = false private var isEmojiKeyExpanded: Bool = false @@ -233,8 +236,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu private var pipVideoCallViewController: UIViewController? private var pipController: AVPictureInPictureController? - private var snowEffectView: SnowEffectView? - public override init(frame: CGRect) { self.overlayContentsView = UIView() self.overlayContentsView.isUserInteractionEnabled = false @@ -489,6 +490,32 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } + public func animateOutToGroupChat(completion: @escaping () -> Void) { + self.isAnimatedOutToGroupCall = true + self.animateOutToGroupCallCompletion = completion + self.update(transition: .easeInOut(duration: 0.25)) + } + + public func takeIncomingVideoLayer() -> (CALayer, VideoSource.Output?)? { + var remoteVideoContainerKey: VideoContainerView.Key? + if self.swapLocalAndRemoteVideo { + if let _ = self.activeRemoteVideoSource { + remoteVideoContainerKey = .foreground + } + } else { + if let _ = self.activeRemoteVideoSource { + remoteVideoContainerKey = .background + } + } + + if let remoteVideoContainerKey, let videoContainerView = self.videoContainerViews.first(where: { $0.key == remoteVideoContainerKey }) { + videoContainerView.videoContainerLayerTaken = true + return (videoContainerView.videoContainerLayer, videoContainerView.currentVideoOutput) + } + + return nil + } + public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: ComponentTransition) { let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state) if self.params == params { @@ -717,6 +744,16 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } self.backgroundLayer.update(stateIndex: backgroundStateIndex, isEnergySavingEnabled: params.state.isEnergySavingEnabled, transition: transition) + genericAlphaTransition.setAlpha(layer: self.backgroundLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0, completion: { [weak self] _ in + guard let self else { + return + } + if let animateOutToGroupCallCompletion = self.animateOutToGroupCallCompletion { + self.animateOutToGroupCallCompletion = nil + animateOutToGroupCallCompletion() + } + }) + transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size)) var isVideoButtonEnabled = false @@ -793,7 +830,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu }*/ let displayClose = false - let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, transition: transition) + let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, isAnimatedOutToGroupCall: self.isAnimatedOutToGroupCall, transition: transition) var expandedEmojiKeyRect: CGRect? if self.isEmojiKeyExpanded { @@ -836,7 +873,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height)) emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size)) - alphaTransition.setAlpha(view: emojiExpandedInfoView, alpha: 1.0) + alphaTransition.setAlpha(view: emojiExpandedInfoView, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) transition.setScale(view: emojiExpandedInfoView, scale: 1.0) expandedEmojiKeyRect = emojiExpandedInfoFrame @@ -868,7 +905,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: backButtonSize) transition.setFrame(view: self.backButtonView, frame: backButtonFrame) - transition.setAlpha(view: self.backButtonView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(view: self.backButtonView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) if case let .active(activeState) = params.state.lifecycleState { let emojiView: KeyEmojiView @@ -886,9 +923,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu return } if !self.isEmojiKeyExpanded { + #if DEBUG + self.conferenceAddParticipant?() + #else self.isEmojiKeyExpanded = true self.displayEmojiTooltip = false self.update(transition: .spring(duration: 0.4)) + #endif } } } @@ -915,6 +956,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center) } emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size)) + if self.isAnimatedOutToGroupCall { + emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) + } if let emojiTooltipView = self.emojiTooltipView { self.emojiTooltipView = nil @@ -940,7 +984,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center) } emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size)) - emojiAlphaTransition.setAlpha(view: emojiView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) if self.displayEmojiTooltip { let emojiTooltipView: EmojiTooltipView @@ -1261,6 +1305,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } + genericAlphaTransition.setAlpha(layer: self.avatarTransformLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(layer: self.blobTransformLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) + let titleSize = self.titleView.update( string: titleString, fontSize: !havePrimaryVideo ? 28.0 : 17.0, @@ -1335,7 +1382,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu size: titleSize ) transition.setFrame(view: self.titleView, frame: titleFrame) - genericAlphaTransition.setAlpha(view: self.titleView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(view: self.titleView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) let statusFrame = CGRect( origin: CGPoint( @@ -1354,7 +1401,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } else { transition.setFrame(view: self.statusView, frame: statusFrame) - genericAlphaTransition.setAlpha(view: self.statusView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + genericAlphaTransition.setAlpha(view: self.statusView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0) } if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2, !self.isEmojiKeyExpanded, (!self.displayEmojiTooltip || !havePrimaryVideo) { @@ -1380,11 +1427,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu ComponentTransition.immediate.setScale(view: weakSignalView, scale: 0.001) weakSignalView.alpha = 0.0 transition.setScaleWithSpring(view: weakSignalView, scale: 1.0) - transition.setAlpha(view: weakSignalView, alpha: 1.0) } } else { transition.setFrame(view: weakSignalView, frame: weakSignalFrame) } + transition.setAlpha(view: weakSignalView, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0) } else { if let weakSignalView = self.weakSignalView { self.weakSignalView = nil @@ -1396,55 +1443,3 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } } } - -final class SnowEffectView: UIView { - private let particlesLayer: CAEmitterLayer - - override init(frame: CGRect) { - let particlesLayer = CAEmitterLayer() - self.particlesLayer = particlesLayer - self.particlesLayer.backgroundColor = nil - self.particlesLayer.isOpaque = false - - particlesLayer.emitterShape = .circle - particlesLayer.emitterMode = .surface - particlesLayer.renderMode = .oldestLast - - let image1 = UIImage(named: "Call/Snow")?.cgImage - - let cell1 = CAEmitterCell() - cell1.contents = image1 - cell1.name = "Snow" - cell1.birthRate = 92.0 - cell1.lifetime = 20.0 - cell1.velocity = 59.0 - cell1.velocityRange = -15.0 - cell1.xAcceleration = 5.0 - cell1.yAcceleration = 40.0 - cell1.emissionRange = 90.0 * (.pi / 180.0) - cell1.spin = -28.6 * (.pi / 180.0) - cell1.spinRange = 57.2 * (.pi / 180.0) - cell1.scale = 0.06 - cell1.scaleRange = 0.3 - cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor - - particlesLayer.emitterCells = [cell1] - - super.init(frame: frame) - - self.layer.addSublayer(particlesLayer) - self.clipsToBounds = true - self.backgroundColor = nil - self.isOpaque = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(size: CGSize) { - self.particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) - self.particlesLayer.emitterSize = CGSize(width: size.width * 3.0, height: size.height * 2.0) - self.particlesLayer.emitterPosition = CGPoint(x: size.width * 0.5, y: -325.0) - } -} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b16eeaf750..c5c4672dbf 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -176,6 +176,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let callState = Promise(nil) private var awaitingCallConnectionDisposable: Disposable? private var callPeerDisposable: Disposable? + private var callIsConferenceDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -811,18 +812,41 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callController?.dismiss() self.callController = nil self.hasOngoingCall.set(false) + self.callState.set(.single(nil)) self.notificationController?.setBlocking(nil) self.callPeerDisposable?.dispose() self.callPeerDisposable = nil + self.callIsConferenceDisposable?.dispose() + self.callIsConferenceDisposable = nil if let call { - self.callState.set(call.state - |> map(Optional.init)) + if call.conferenceCall == nil { + self.callState.set(call.state + |> map(Optional.init)) + } + self.hasOngoingCall.set(true) setNotificationCall(call) + self.callIsConferenceDisposable = (call.hasConference + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + guard let call = self.call else { + return + } + guard let callController = self.callController, callController.call === call else { + return + } + if call.conferenceCall != nil { + self.callState.set(.single(nil)) + self.presentControllerWithCurrentCall() + } + }) + if call.isOutgoing { self.presentControllerWithCurrentCall() } else { @@ -921,7 +945,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return } - let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData) + let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData, sourceCallController: nil) groupCallController.onViewDidAppear = { [weak strongSelf] in if let strongSelf { strongSelf.hasGroupCallOnScreenPromise.set(true) @@ -947,31 +971,34 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) - let callSignal: Signal = .single(nil) + let callSignal: Signal<(PresentationCall?, PresentationGroupCall?), NoError> = .single((nil, nil)) |> then( callManager.currentCallSignal |> deliverOnMainQueue - |> mapToSignal { call -> Signal in + |> mapToSignal { call -> Signal<(PresentationCall?, PresentationGroupCall?), NoError> in guard let call else { - return .single(nil) + return .single((nil, nil)) } - return call.state - |> map { [weak call] state -> PresentationCall? in + return combineLatest(call.state, call.hasConference) + |> map { [weak call] state, _ -> (PresentationCall?, PresentationGroupCall?) in guard let call else { - return nil + return (nil, nil) + } + if let conferenceCall = call.conferenceCall { + return (nil, conferenceCall) } switch state.state { case .ringing: - return nil + return (nil, nil) case .terminating, .terminated: - return nil + return (nil, nil) default: - return call + return (call, nil) } } } |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs === rhs + return lhs.0 === rhs.0 && lhs.1 === rhs.1 }) ) let groupCallSignal: Signal = .single(nil) @@ -985,8 +1012,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.hasGroupCallOnScreenPromise.get() ).start(next: { [weak self] call, groupCall, hasGroupCallOnScreen in if let strongSelf = self { + var (call, conferenceCall) = call + var groupCall = groupCall + if let conferenceCall { + call = nil + groupCall = conferenceCall + } + let statusBarContent: CallStatusBarNodeImpl.Content? - if let call = call { + if let call { statusBarContent = .call(strongSelf, call.context.account, call) } else if let groupCall = groupCall, !hasGroupCallOnScreen { statusBarContent = .groupCall(strongSelf, groupCall.account, groupCall) @@ -1179,30 +1213,75 @@ public final class SharedAccountContextImpl: SharedAccountContext { return } - if let currentCallController = self.callController { - if currentCallController.call == .call(call) { - self.navigateToCurrentCall() - return - } else { + if let conferenceCall = call.conferenceCall { + if let groupCallController = self.groupCallController { + if groupCallController.call === conferenceCall { + return + } + groupCallController.dismiss(closing: true, manual: false) + self.groupCallController = nil + } + var transitioniongCallController: CallController? + if let callController = self.callController { + transitioniongCallController = callController + callController.dismissWithoutAnimation() self.callController = nil - currentCallController.dismiss() } + + let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: conferenceCall.accountContext, call: conferenceCall) + |> deliverOnMainQueue).start(next: { [weak self, weak transitioniongCallController] initialData in + guard let self else { + return + } + guard let navigationController = self.mainWindow?.viewController as? NavigationController else { + return + } + guard let call = self.call, let conferenceCall = call.conferenceCall else { + return + } + + let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: conferenceCall.accountContext, call: conferenceCall, initialData: initialData, sourceCallController: transitioniongCallController) + groupCallController.onViewDidAppear = { [weak self] in + if let self { + self.hasGroupCallOnScreenPromise.set(true) + } + } + groupCallController.onViewDidDisappear = { [weak self] in + if let self { + self.hasGroupCallOnScreenPromise.set(false) + } + } + groupCallController.navigationPresentation = .flatModal + groupCallController.parentNavigationController = navigationController + self.groupCallController = groupCallController + navigationController.pushViewController(groupCallController) + }) + } else { + if let currentCallController = self.callController { + if currentCallController.call === call { + self.navigateToCurrentCall() + return + } else { + self.callController = nil + currentCallController.dismiss() + } + } + + self.mainWindow?.hostView.containerView.endEditing(true) + let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) + self.callController = callController + callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in + guard let self, let callController else { + completion(false) + return + } + if callController.window == nil { + self.mainWindow?.present(callController, on: .calls) + } + completion(true) + } + self.mainWindow?.present(callController, on: .calls) } - - self.mainWindow?.hostView.containerView.endEditing(true) - let callController = CallController(sharedContext: self, account: call.context.account, call: .call(call), easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) - self.callController = callController - callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in - guard let self, let callController else { - completion(false) - return - } - if callController.window == nil { - self.mainWindow?.present(callController, on: .calls) - } - completion(true) - } - self.mainWindow?.present(callController, on: .calls) } public func updateNotificationTokensRegistration() { diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index ab815d0e7f..843dee34df 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/TgVoipWebrtc:TgVoipWebrtc", "//submodules/FFMpegBinding", "//submodules/ManagedFile", + "//submodules/AppBundle", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 94b9cd8825..72d5365a1a 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -7,6 +7,12 @@ import TelegramUIPreferences import TgVoip import TgVoipWebrtc +#if os(iOS) +import UIKit +import AppBundle +import Accelerate +#endif + private func debugUseLegacyVersionForReflectors() -> Bool { #if DEBUG && false return true @@ -407,8 +413,179 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { } } +#if targetEnvironment(simulator) +private extension UIImage { + @available(iOS 13.0, *) + func toBiplanarYUVPixelBuffer() -> CVPixelBuffer? { + guard let cgImage = self.cgImage else { + return nil + } + + // Dimensions + let width = Int(self.size.width * self.scale) + let height = Int(self.size.height * self.scale) + + // 1) Create an ARGB8888 vImage buffer from the UIImage (CGImage). + // We will first allocate a buffer for ARGB pixels, then use + // vImage to copy cgImage → argbBuffer. + + // Each ARGB pixel is 4 bytes + let argbBytesPerPixel = 4 + let argbRowBytes = width * argbBytesPerPixel + + // Allocate contiguous memory for ARGB data + let argbData = malloc(argbRowBytes * height) + defer { + free(argbData) + } + + // Create a vImage buffer for ARGB + var argbBuffer = vImage_Buffer( + data: argbData, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: argbRowBytes + ) + + // Initialize the ARGB buffer from our CGImage + // This helper function can fail, so check the result: + var format = vImage_CGImageFormat( + bitsPerComponent: 8, + bitsPerPixel: 32, + colorSpace: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue), + renderingIntent: CGColorRenderingIntent.defaultIntent + )! + + if vImageBuffer_InitWithCGImage( + &argbBuffer, + &format, + nil, + cgImage, + vImage_Flags(kvImageNoFlags) + ) != kvImageNoError { + return nil + } + + // 2) Create a CVPixelBuffer in YUV 420 (bi-planar) format. + // Typically, you’d choose either kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + // or kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange. + + let pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + let attrs: [CFString: Any] = [ + kCVPixelBufferIOSurfacePropertiesKey: [:], + // Optionally, specify other attributes if needed. + ] + + var cvPixelBufferOut: CVPixelBuffer? + guard CVPixelBufferCreate( + kCFAllocatorDefault, + width, + height, + pixelFormat, + attrs as CFDictionary, + &cvPixelBufferOut + ) == kCVReturnSuccess, + let pixelBuffer = cvPixelBufferOut + else { + return nil + } + + // 3) Lock the CVPixelBuffer to get direct access to its planes. + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + // Plane 0: Y-plane + guard let yBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return nil + } + let yPitch = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) + + // Plane 1: CbCr-plane + guard let cbcrBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return nil + } + let cbcrPitch = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) + + // 4) Create vImage buffers for each plane. + + // Y plane is full size (width x height) + var yBuffer = vImage_Buffer( + data: yBaseAddress, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: yPitch + ) + + // CbCr plane is half height, but each row has interleaved Cb/Cr + // so the plane is (width/2) * 2 bytes = width bytes wide, and height/2. + var cbcrBuffer = vImage_Buffer( + data: cbcrBaseAddress, + height: vImagePixelCount(height / 2), + width: vImagePixelCount(width), + rowBytes: cbcrPitch + ) + + var info = vImage_ARGBToYpCbCr() + var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 0, CbCr_bias: 128, YpRangeMax: 255, CbCrRangeMax: 255, YpMax: 255, YpMin: 1, CbCrMax: 255, CbCrMin: 0) + vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0) + + let error = vImageConvert_ARGB8888To420Yp8_CbCr8( + &argbBuffer, + &yBuffer, + &cbcrBuffer, + &info, + nil, + UInt32(kvImageDoNotTile) + ) + + if error != kvImageNoError { + return nil + } + + return pixelBuffer + } + + @available(iOS 13.0, *) + var cmSampleBuffer: CMSampleBuffer? { + guard let pixelBuffer = self.toBiplanarYUVPixelBuffer() else { + return nil + } + var newSampleBuffer: CMSampleBuffer? = nil + + var timingInfo = CMSampleTimingInfo( + duration: CMTimeMake(value: 1, timescale: 30), + presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), + decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) + ) + + var videoInfo: CMVideoFormatDescription? = nil + CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &videoInfo) + guard let videoInfo = videoInfo else { + return nil + } + CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: videoInfo, sampleTiming: &timingInfo, sampleBufferOut: &newSampleBuffer) + + if let newSampleBuffer = newSampleBuffer { + let attachments = CMSampleBufferGetSampleAttachmentsArray(newSampleBuffer, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + } + + return newSampleBuffer + } +} +#endif + public final class OngoingCallVideoCapturer { internal let impl: OngoingCallThreadLocalContextVideoCapturer + + #if targetEnvironment(simulator) + private var simulatedVideoTimer: Foundation.Timer? + #endif private let isActivePromise = ValuePromise(true, ignoreRepeated: true) public var isActive: Signal { @@ -419,13 +596,47 @@ public final class OngoingCallVideoCapturer { if isCustom { self.impl = OngoingCallThreadLocalContextVideoCapturer.withExternalSampleBufferProvider() } else { + #if targetEnvironment(simulator) && false + self.impl = OngoingCallThreadLocalContextVideoCapturer.withExternalSampleBufferProvider() + let imageSize = CGSize(width: 600.0, height: 800.0) + UIGraphicsBeginImageContextWithOptions(imageSize, true, 1.0) + let sourceImage: UIImage? + let imagePath = NSTemporaryDirectory() + "frontCameraImage.jpg" + if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) { + sourceImage = image + } else { + sourceImage = UIImage(bundleImageName: "Camera/SelfiePlaceholder")! + } + if let sourceImage { + sourceImage.draw(in: CGRect(origin: CGPoint(), size: imageSize)) + } + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + self.simulatedVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + if #available(iOS 13.0, *) { + if let image, let sampleBuffer = image.cmSampleBuffer { + self.injectSampleBuffer(sampleBuffer, rotation: .up, completion: {}) + } + } + }) + #else self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: "", keepLandscape: keepLandscape) + #endif } let isActivePromise = self.isActivePromise self.impl.setOnIsActiveUpdated({ value in isActivePromise.set(value) }) } + + deinit { + #if targetEnvironment(simulator) + self.simulatedVideoTimer?.invalidate() + #endif + } public func switchVideoInput(isFront: Bool) { self.impl.switchVideoInput(isFront ? "" : "back") diff --git a/third-party/flatc/BUILD b/third-party/flatc/BUILD index 503240e9ea..e3d54ce41c 100644 --- a/third-party/flatc/BUILD +++ b/third-party/flatc/BUILD @@ -22,14 +22,14 @@ set -x pushd "$$BUILD_DIR/flatbuffers-24.12.23" mkdir build cd build - PATH="$$PATH:$$CMAKE_DIR/cmake-3.23.1-macos-universal/CMake.app/Contents/bin" cmake .. -DCMAKE_BUILD_TYPE=Release" + PATH="$$PATH:$$CMAKE_DIR/cmake-3.23.1-macos-universal/CMake.app/Contents/bin" cmake .. -DCMAKE_BUILD_TYPE=Release -DFLATBUFFERS_BUILD_TESTS=0 -DFLATBUFFERS_INSTALL=0 -DFLATBUFFERS_BUILD_FLATLIB=0 -DFLATBUFFERS_STATIC_FLATC=0 make -j $$core_count popd - tar -cf "$(location flatc.tar)" -C "$$BUILD_DIR/flatbuffers-24.12.23/build" . + cp "$$BUILD_DIR/flatbuffers-24.12.23/build/flatc" "$(location flatc_bin)" """, outs = [ - "flatc.tar", + "flatc_bin", ], visibility = [ "//visibility:public",