import Foundation import SwiftSignalKit import TgVoipWebrtc import TelegramCore final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc { private let queue: Queue init(queue: Queue) { self.queue = queue super.init() } func dispatch(_ f: @escaping () -> Void) { self.queue.async { f() } } func dispatch(after seconds: Double, block f: @escaping () -> Void) { self.queue.after(seconds, f) } func isCurrent() -> Bool { return self.queue.isCurrent() } } enum BroadcastPartSubject { case audio case video(channelId: Int32, quality: OngoingGroupCallContext.VideoChannel.Quality) } protocol BroadcastPartSource: AnyObject { func requestTime(completion: @escaping (Int64) -> Void) -> Disposable func requestPart(timestampMilliseconds: Int64, durationMilliseconds: Int64, subject: BroadcastPartSubject, completion: @escaping (OngoingGroupCallBroadcastPart) -> Void, rejoinNeeded: @escaping () -> Void) -> Disposable } final class NetworkBroadcastPartSource: BroadcastPartSource { private let queue: Queue private let engine: TelegramEngine private let callId: Int64 private let accessHash: Int64 private let isExternalStream: Bool private var dataSource: AudioBroadcastDataSource? #if DEBUG private let debugDumpDirectory: EngineTempBox.Directory? #endif init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) { self.queue = queue self.engine = engine self.callId = callId self.accessHash = accessHash self.isExternalStream = isExternalStream #if DEBUG && true self.debugDumpDirectory = EngineTempBox.shared.tempDirectory() #endif } func requestTime(completion: @escaping (Int64) -> Void) -> Disposable { if self.isExternalStream { let dataSource: Signal if let dataSourceValue = self.dataSource { dataSource = .single(dataSourceValue) } else { dataSource = self.engine.calls.getAudioBroadcastDataSource(callId: self.callId, accessHash: self.accessHash) } let engine = self.engine let callId = self.callId let accessHash = self.accessHash return (dataSource |> deliverOn(self.queue) |> mapToSignal { [weak self] dataSource -> Signal in if let dataSource = dataSource { self?.dataSource = dataSource return engine.calls.requestStreamState(dataSource: dataSource, callId: callId, accessHash: accessHash) } else { return .single(nil) } } |> deliverOn(self.queue)).start(next: { result in if let channel = result?.channels.first { completion(channel.latestTimestamp) } else { completion(0) } }) } else { return self.engine.calls.serverTime().start(next: { result in completion(result) }) } } func requestPart(timestampMilliseconds: Int64, durationMilliseconds: Int64, subject: BroadcastPartSubject, completion: @escaping (OngoingGroupCallBroadcastPart) -> Void, rejoinNeeded: @escaping () -> Void) -> Disposable { let timestampIdMilliseconds: Int64 if timestampMilliseconds != 0 { timestampIdMilliseconds = timestampMilliseconds } else { timestampIdMilliseconds = (Int64((Date().timeIntervalSince1970) * 1000.0) / durationMilliseconds) * durationMilliseconds } let dataSource: Signal if let dataSourceValue = self.dataSource { dataSource = .single(dataSourceValue) } else { dataSource = self.engine.calls.getAudioBroadcastDataSource(callId: self.callId, accessHash: self.accessHash) } let callId = self.callId let accessHash = self.accessHash let engine = self.engine let queue = self.queue let signal = dataSource |> deliverOn(self.queue) |> mapToSignal { [weak self] dataSource -> Signal in if let dataSource = dataSource { self?.dataSource = dataSource switch subject { case .audio: return engine.calls.getAudioBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds) |> map(Optional.init) case let .video(channelId, quality): let mappedQuality: Int32 switch quality { case .thumbnail: mappedQuality = 0 case .medium: mappedQuality = 1 case .full: mappedQuality = 2 } return engine.calls.getVideoBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds, channelId: channelId, quality: mappedQuality) |> map(Optional.init) } } else { return .single(nil) |> delay(2.0, queue: queue) } } |> deliverOn(self.queue) #if DEBUG let debugDumpDirectory = self.debugDumpDirectory #endif return signal.start(next: { result in guard let result = result else { completion(OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: Double(timestampIdMilliseconds), status: .notReady, oggData: Data())) return } let part: OngoingGroupCallBroadcastPart switch result.status { case let .data(dataValue): #if DEBUG if let debugDumpDirectory { let tempFilePath = debugDumpDirectory.path + "/\(timestampMilliseconds).mp4" let _ = try? dataValue.subdata(in: 32 ..< dataValue.count).write(to: URL(fileURLWithPath: tempFilePath)) print("Dump stream part: \(tempFilePath)") } #endif part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .success, oggData: dataValue) case .notReady: part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .notReady, oggData: Data()) case .resyncNeeded: part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .resyncNeeded, oggData: Data()) case .rejoinNeeded: rejoinNeeded() return } completion(part) }) } } final class OngoingGroupCallBroadcastPartTaskImpl: NSObject, OngoingGroupCallBroadcastPartTask { private let disposable: Disposable? init(disposable: Disposable?) { self.disposable = disposable } func cancel() { self.disposable?.dispose() } } public final class OngoingGroupCallContext { public struct AudioStreamData { public var engine: TelegramEngine public var callId: Int64 public var accessHash: Int64 public var isExternalStream: Bool public init(engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) { self.engine = engine self.callId = callId self.accessHash = accessHash self.isExternalStream = isExternalStream } } public enum ConnectionMode { case none case rtc case broadcast } public enum VideoContentType { case none case generic case screencast } public struct NetworkState: Equatable { public var isConnected: Bool public var isTransitioningFromBroadcastToRtc: Bool public init(isConnected: Bool, isTransitioningFromBroadcastToRtc: Bool) { self.isConnected = isConnected self.isTransitioningFromBroadcastToRtc = isTransitioningFromBroadcastToRtc } } public enum AudioLevelKey: Hashable { case local case source(UInt32) } public struct MediaChannelDescription { public enum Kind { case audio case video } public var kind: Kind public var audioSsrc: UInt32 public var videoDescription: String? public init(kind: Kind, audioSsrc: UInt32, videoDescription: String?) { self.kind = kind self.audioSsrc = audioSsrc self.videoDescription = videoDescription } } public struct VideoChannel: Equatable { public enum Quality { case thumbnail case medium case full } public struct SsrcGroup: Equatable { public var semantics: String public var ssrcs: [UInt32] public init(semantics: String, ssrcs: [UInt32]) { self.semantics = semantics self.ssrcs = ssrcs } } public var audioSsrc: UInt32 public var endpointId: String public var ssrcGroups: [SsrcGroup] public var minQuality: Quality public var maxQuality: Quality public init(audioSsrc: UInt32, endpointId: String, ssrcGroups: [SsrcGroup], minQuality: Quality, maxQuality: Quality) { self.audioSsrc = audioSsrc self.endpointId = endpointId self.ssrcGroups = ssrcGroups self.minQuality = minQuality self.maxQuality = maxQuality } } public final class VideoFrameData { public final class NativeBuffer { public let pixelBuffer: CVPixelBuffer init(pixelBuffer: CVPixelBuffer) { self.pixelBuffer = pixelBuffer } } public final class NV12Buffer { private let wrapped: CallVideoFrameNV12Buffer public var width: Int { return Int(self.wrapped.width) } public var height: Int { return Int(self.wrapped.height) } public var y: Data { return self.wrapped.y } public var strideY: Int { return Int(self.wrapped.strideY) } public var uv: Data { return self.wrapped.uv } public var strideUV: Int { return Int(self.wrapped.strideUV) } init(wrapped: CallVideoFrameNV12Buffer) { self.wrapped = wrapped } } public final class I420Buffer { private let wrapped: CallVideoFrameI420Buffer public var width: Int { return Int(self.wrapped.width) } public var height: Int { return Int(self.wrapped.height) } public var y: Data { return self.wrapped.y } public var strideY: Int { return Int(self.wrapped.strideY) } public var u: Data { return self.wrapped.u } public var strideU: Int { return Int(self.wrapped.strideU) } public var v: Data { return self.wrapped.v } public var strideV: Int { return Int(self.wrapped.strideV) } init(wrapped: CallVideoFrameI420Buffer) { self.wrapped = wrapped } } public enum Buffer { case native(NativeBuffer) case nv12(NV12Buffer) case i420(I420Buffer) } public let buffer: Buffer public let width: Int public let height: Int public let orientation: OngoingCallVideoOrientation public let mirrorHorizontally: Bool public let mirrorVertically: Bool init(frameData: CallVideoFrameData) { if let nativeBuffer = frameData.buffer as? CallVideoFrameNativePixelBuffer { self.buffer = .native(NativeBuffer(pixelBuffer: nativeBuffer.pixelBuffer)) } else if let nv12Buffer = frameData.buffer as? CallVideoFrameNV12Buffer { self.buffer = .nv12(NV12Buffer(wrapped: nv12Buffer)) } else if let i420Buffer = frameData.buffer as? CallVideoFrameI420Buffer { self.buffer = .i420(I420Buffer(wrapped: i420Buffer)) } else { preconditionFailure() } self.width = Int(frameData.width) self.height = Int(frameData.height) self.orientation = OngoingCallVideoOrientation(frameData.orientation) self.mirrorHorizontally = frameData.mirrorHorizontally self.mirrorVertically = frameData.mirrorVertically } } public struct Stats { public struct IncomingVideoStats { public var receivingQuality: Int public var availableQuality: Int } public var incomingVideoStats: [String: IncomingVideoStats] } public final class Tone { public let samples: Data public let sampleRate: Int public let loopCount: Int public init(samples: Data, sampleRate: Int, loopCount: Int) { self.samples = samples self.sampleRate = sampleRate self.loopCount = loopCount } } private final class Impl { let queue: Queue let context: GroupCallThreadLocalContext #if os(iOS) let audioDevice: SharedCallAudioDevice? #endif let sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max)) let joinPayload = Promise<(String, UInt32)>() let networkState = ValuePromise(NetworkState(isConnected: false, isTransitioningFromBroadcastToRtc: false), ignoreRepeated: true) let isMuted = ValuePromise(true, ignoreRepeated: true) let isNoiseSuppressionEnabled = ValuePromise(true, ignoreRepeated: true) let audioLevels = ValuePipe<[(AudioLevelKey, Float, Bool)]>() private var currentRequestedVideoChannels: [VideoChannel] = [] private let broadcastPartsSource = Atomic(value: nil) private let audioSessionActiveDisposable = MetaDisposable() init(queue: Queue, inputDeviceId: String, outputDeviceId: String, audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { self.queue = queue #if os(iOS) self.audioDevice = nil let audioDevice = self.audioDevice #endif var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)? let _videoContentType: OngoingGroupCallVideoContentType switch videoContentType { case .generic: _videoContentType = .generic case .screencast: _videoContentType = .screencast case .none: _videoContentType = .none } var getBroadcastPartsSource: (() -> BroadcastPartSource?)? #if os(iOS) self.context = GroupCallThreadLocalContext( queue: ContextQueueImpl(queue: queue), networkStateUpdated: { state in networkStateUpdatedImpl?(state) }, audioLevelsUpdated: { levels in audioLevelsUpdatedImpl?(levels) }, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, videoCapturer: video?.impl, requestMediaChannelDescriptions: { ssrcs, completion in final class OngoingGroupCallMediaChannelDescriptionTaskImpl : NSObject, OngoingGroupCallMediaChannelDescriptionTask { private let disposable: Disposable init(disposable: Disposable) { self.disposable = disposable } func cancel() { self.disposable.dispose() } } let disposable = requestMediaChannelDescriptions(Set(ssrcs.map { $0.uint32Value }), { channels in completion(channels.map { channel -> OngoingGroupCallMediaChannelDescription in let mappedType: OngoingGroupCallMediaChannelType switch channel.kind { case .audio: mappedType = .audio case .video: mappedType = .video } return OngoingGroupCallMediaChannelDescription( type: mappedType, audioSsrc: channel.audioSsrc, videoDescription: channel.videoDescription ) }) }) return OngoingGroupCallMediaChannelDescriptionTaskImpl(disposable: disposable) }, requestCurrentTime: { completion in let disposable = MetaDisposable() queue.async { disposable.set(getBroadcastPartsSource?()?.requestTime(completion: completion)) } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) }, requestAudioBroadcastPart: { timestampMilliseconds, durationMilliseconds, completion in let disposable = MetaDisposable() queue.async { disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .audio, completion: completion, rejoinNeeded: { rejoinNeeded() })) } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) }, requestVideoBroadcastPart: { timestampMilliseconds, durationMilliseconds, channelId, quality, completion in let disposable = MetaDisposable() queue.async { let mappedQuality: OngoingGroupCallContext.VideoChannel.Quality switch quality { case .thumbnail: mappedQuality = .thumbnail case .medium: mappedQuality = .medium case .full: mappedQuality = .full @unknown default: mappedQuality = .thumbnail } disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .video(channelId: channelId, quality: mappedQuality), completion: completion, rejoinNeeded: { rejoinNeeded() })) } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32, videoContentType: _videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath, audioDevice: audioDevice ) #else self.context = GroupCallThreadLocalContext( queue: ContextQueueImpl(queue: queue), networkStateUpdated: { state in networkStateUpdatedImpl?(state) }, audioLevelsUpdated: { levels in audioLevelsUpdatedImpl?(levels) }, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, videoCapturer: video?.impl, requestMediaChannelDescriptions: { ssrcs, completion in final class OngoingGroupCallMediaChannelDescriptionTaskImpl : NSObject, OngoingGroupCallMediaChannelDescriptionTask { private let disposable: Disposable init(disposable: Disposable) { self.disposable = disposable } func cancel() { self.disposable.dispose() } } let disposable = requestMediaChannelDescriptions(Set(ssrcs.map { $0.uint32Value }), { channels in completion(channels.map { channel -> OngoingGroupCallMediaChannelDescription in let mappedType: OngoingGroupCallMediaChannelType switch channel.kind { case .audio: mappedType = .audio case .video: mappedType = .video } return OngoingGroupCallMediaChannelDescription( type: mappedType, audioSsrc: channel.audioSsrc, videoDescription: channel.videoDescription ) }) }) return OngoingGroupCallMediaChannelDescriptionTaskImpl(disposable: disposable) }, requestCurrentTime: { completion in let disposable = MetaDisposable() queue.async { disposable.set(getBroadcastPartsSource?()?.requestTime(completion: completion)) } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) }, requestAudioBroadcastPart: { timestampMilliseconds, durationMilliseconds, completion in let disposable = MetaDisposable() queue.async { disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .audio, completion: completion, rejoinNeeded: { rejoinNeeded() })) } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) }, requestVideoBroadcastPart: { timestampMilliseconds, durationMilliseconds, channelId, quality, completion in let disposable = MetaDisposable() queue.async { let mappedQuality: OngoingGroupCallContext.VideoChannel.Quality switch quality { case .thumbnail: mappedQuality = .thumbnail case .medium: mappedQuality = .medium case .full: mappedQuality = .full @unknown default: mappedQuality = .thumbnail } disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .video(channelId: channelId, quality: mappedQuality), completion: completion, rejoinNeeded: { rejoinNeeded() })) } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32, videoContentType: _videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath ) #endif let queue = self.queue let broadcastPartsSource = self.broadcastPartsSource getBroadcastPartsSource = { return broadcastPartsSource.with { $0 } } networkStateUpdatedImpl = { [weak self] state in queue.async { guard let strongSelf = self else { return } strongSelf.networkState.set(NetworkState(isConnected: state.isConnected, isTransitioningFromBroadcastToRtc: state.isTransitioningFromBroadcastToRtc)) } } let audioLevels = self.audioLevels audioLevelsUpdatedImpl = { levels in var mappedLevels: [(AudioLevelKey, Float, Bool)] = [] var i = 0 while i < levels.count { let uintValue = levels[i].uint32Value let key: AudioLevelKey if uintValue == 0 { key = .local } else { key = .source(uintValue) } mappedLevels.append((key, levels[i + 1].floatValue, levels[i + 2].boolValue)) i += 3 } queue.async { audioLevels.putNext(mappedLevels) } } self.context.emitJoinPayload({ [weak self] payload, ssrc in queue.async { guard let strongSelf = self else { return } strongSelf.joinPayload.set(.single((payload, ssrc))) } }) self.audioSessionActiveDisposable.set((audioSessionActive |> deliverOn(queue)).start(next: { [weak self] isActive in guard let `self` = self else { return } // self.audioDevice?.setManualAudioSessionIsActive(isActive) #if os(iOS) self.context.setManualAudioSessionIsActive(isActive) #endif })) } deinit { self.audioSessionActiveDisposable.dispose() } func setJoinResponse(payload: String) { self.context.setJoinResponsePayload(payload) } func setAudioStreamData(audioStreamData: AudioStreamData?) { if let audioStreamData = audioStreamData { let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream) let _ = self.broadcastPartsSource.swap(broadcastPartsSource) } } func addSsrcs(ssrcs: [UInt32]) { } func removeSsrcs(ssrcs: [UInt32]) { if ssrcs.isEmpty { return } self.context.removeSsrcs(ssrcs.map { ssrc in return ssrc as NSNumber }) } func removeIncomingVideoSource(_ ssrc: UInt32) { self.context.removeIncomingVideoSource(ssrc) } func setVolume(ssrc: UInt32, volume: Double) { self.context.setVolumeForSsrc(ssrc, volume: volume) } func setRequestedVideoChannels(_ channels: [VideoChannel]) { if self.currentRequestedVideoChannels != channels { self.currentRequestedVideoChannels = channels self.context.setRequestedVideoChannels(channels.map { channel -> OngoingGroupCallRequestedVideoChannel in let mappedMinQuality: OngoingGroupCallRequestedVideoQuality switch channel.minQuality { case .thumbnail: mappedMinQuality = .thumbnail case .medium: mappedMinQuality = .medium case .full: mappedMinQuality = .full } let mappedMaxQuality: OngoingGroupCallRequestedVideoQuality switch channel.maxQuality { case .thumbnail: mappedMaxQuality = .thumbnail case .medium: mappedMaxQuality = .medium case .full: mappedMaxQuality = .full } return OngoingGroupCallRequestedVideoChannel( audioSsrc: channel.audioSsrc, endpointId: channel.endpointId, ssrcGroups: channel.ssrcGroups.map { group in return OngoingGroupCallSsrcGroup( semantics: group.semantics, ssrcs: group.ssrcs.map { $0 as NSNumber }) }, minQuality: mappedMinQuality, maxQuality: mappedMaxQuality ) }) } } func stop() { self.context.stop() } func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool, isUnifiedBroadcast: Bool) { let mappedConnectionMode: OngoingCallConnectionMode switch connectionMode { case .none: mappedConnectionMode = .none case .rtc: mappedConnectionMode = .rtc case .broadcast: mappedConnectionMode = .broadcast } self.context.setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled, isUnifiedBroadcast: isUnifiedBroadcast) if (mappedConnectionMode != .rtc) { self.joinPayload.set(.never()) let queue = self.queue self.context.emitJoinPayload({ [weak self] payload, ssrc in queue.async { guard let strongSelf = self else { return } strongSelf.joinPayload.set(.single((payload, ssrc))) } }) } } func setIsMuted(_ isMuted: Bool) { self.isMuted.set(isMuted) self.context.setIsMuted(isMuted) } func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) { self.isNoiseSuppressionEnabled.set(isNoiseSuppressionEnabled) self.context.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) } func requestVideo(_ capturer: OngoingCallVideoCapturer?) { let queue = self.queue self.context.requestVideo(capturer?.impl, completion: { [weak self] payload, ssrc in queue.async { guard let strongSelf = self else { return } strongSelf.joinPayload.set(.single((payload, ssrc))) } }) } public func disableVideo() { let queue = self.queue self.context.disableVideo({ [weak self] payload, ssrc in queue.async { guard let strongSelf = self else { return } strongSelf.joinPayload.set(.single((payload, ssrc))) } }) } func switchAudioInput(_ deviceId: String) { self.context.switchAudioInput(deviceId) } func switchAudioOutput(_ deviceId: String) { self.context.switchAudioOutput(deviceId) } func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (OngoingCallContextPresentationCallVideoView?, OngoingCallContextPresentationCallVideoView?) -> Void) { self.context.makeIncomingVideoView(withEndpointId: endpointId, requestClone: requestClone, completion: { mainView, cloneView in if let mainView = mainView { #if os(iOS) let mainVideoView = OngoingCallContextPresentationCallVideoView( view: mainView, setOnFirstFrameReceived: { [weak mainView] f in mainView?.setOnFirstFrameReceived(f) }, getOrientation: { [weak mainView] in if let mainView = mainView { return OngoingCallVideoOrientation(mainView.orientation) } else { return .rotation0 } }, getAspect: { [weak mainView] in if let mainView = mainView { return mainView.aspect } else { return 0.0 } }, setOnOrientationUpdated: { [weak mainView] f in mainView?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, setOnIsMirroredUpdated: { [weak mainView] f in mainView?.setOnIsMirroredUpdated { value in f?(value) } }, updateIsEnabled: { [weak mainView] value in mainView?.updateIsEnabled(value) } ) var cloneVideoView: OngoingCallContextPresentationCallVideoView? if let cloneView = cloneView { cloneVideoView = OngoingCallContextPresentationCallVideoView( view: cloneView, setOnFirstFrameReceived: { [weak cloneView] f in cloneView?.setOnFirstFrameReceived(f) }, getOrientation: { [weak cloneView] in if let cloneView = cloneView { return OngoingCallVideoOrientation(cloneView.orientation) } else { return .rotation0 } }, getAspect: { [weak cloneView] in if let cloneView = cloneView { return cloneView.aspect } else { return 0.0 } }, setOnOrientationUpdated: { [weak cloneView] f in cloneView?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, setOnIsMirroredUpdated: { [weak cloneView] f in cloneView?.setOnIsMirroredUpdated { value in f?(value) } }, updateIsEnabled: { [weak cloneView] value in cloneView?.updateIsEnabled(value) } ) } completion(mainVideoView, cloneVideoView) #else let mainVideoView = OngoingCallContextPresentationCallVideoView( view: mainView, setOnFirstFrameReceived: { [weak mainView] f in mainView?.setOnFirstFrameReceived(f) }, getOrientation: { [weak mainView] in if let mainView = mainView { return OngoingCallVideoOrientation(mainView.orientation) } else { return .rotation0 } }, getAspect: { [weak mainView] in if let mainView = mainView { return mainView.aspect } else { return 0.0 } }, setOnOrientationUpdated: { [weak mainView] f in mainView?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, setVideoContentMode: { [weak mainView] mode in mainView?.setVideoContentMode(mode) }, setOnIsMirroredUpdated: { [weak mainView] f in mainView?.setOnIsMirroredUpdated { value in f?(value) } }, setIsPaused: { [weak mainView] paused in mainView?.setIsPaused(paused) }, renderToSize: { [weak mainView] size, animated in mainView?.render(to: size, animated: animated) } ) completion(mainVideoView, nil) #endif } else { completion(nil, nil) } }) } func video(endpointId: String) -> Signal { let queue = self.queue return Signal { [weak self] subscriber in let disposable = MetaDisposable() queue.async { guard let strongSelf = self else { return } let innerDisposable = strongSelf.context.addVideoOutput(withEndpointId: endpointId) { videoFrameData in subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData)) } disposable.set(ActionDisposable { innerDisposable.dispose() }) } return disposable } } func addExternalAudioData(data: Data) { self.context.addExternalAudioData(data) } func getStats(completion: @escaping (Stats) -> Void) { self.context.getStats({ stats in var incomingVideoStats: [String: Stats.IncomingVideoStats] = [:] for (key, value) in stats.incomingVideoStats { incomingVideoStats[key] = Stats.IncomingVideoStats(receivingQuality: Int(value.receivingQuality), availableQuality: Int(value.availableQuality)) } completion(Stats(incomingVideoStats: incomingVideoStats)) }) } func setTone(tone: Tone?) { #if os(iOS) let mappedTone = tone.flatMap { tone in CallAudioTone(samples: tone.samples, sampleRate: tone.sampleRate, loopCount: tone.loopCount) } // if let audioDevice = self.audioDevice { // audioDevice.setTone(mappedTone) // } else { self.context.setTone(mappedTone) // } #endif } } private let queue = Queue() private let impl: QueueLocalObject public var joinPayload: Signal<(String, UInt32), NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.joinPayload.get().start(next: { value in subscriber.putNext(value) })) } return disposable } } public var networkState: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.networkState.get().start(next: { value in subscriber.putNext(value) })) } return disposable } } public var audioLevels: Signal<[(AudioLevelKey, Float, Bool)], NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.audioLevels.signal().start(next: { value in subscriber.putNext(value) })) } return disposable } } public var isMuted: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.isMuted.get().start(next: { value in subscriber.putNext(value) })) } return disposable } } public var isNoiseSuppressionEnabled: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.isNoiseSuppressionEnabled.get().start(next: { value in subscriber.putNext(value) })) } return disposable } } public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath) }) } public func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool, isUnifiedBroadcast: Bool) { self.impl.with { impl in impl.setConnectionMode(connectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled, isUnifiedBroadcast: isUnifiedBroadcast) } } public func setIsMuted(_ isMuted: Bool) { self.impl.with { impl in impl.setIsMuted(isMuted) } } public func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) { self.impl.with { impl in impl.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) } } public func requestVideo(_ capturer: OngoingCallVideoCapturer?) { self.impl.with { impl in impl.requestVideo(capturer) } } public func disableVideo() { self.impl.with { impl in impl.disableVideo() } } public func switchAudioInput(_ deviceId: String) { self.impl.with { impl in impl.switchAudioInput(deviceId) } } public func switchAudioOutput(_ deviceId: String) { self.impl.with { impl in impl.switchAudioOutput(deviceId) } } public func setJoinResponse(payload: String) { self.impl.with { impl in impl.setJoinResponse(payload: payload) } } public func setAudioStreamData(audioStreamData: AudioStreamData?) { self.impl.with { impl in impl.setAudioStreamData(audioStreamData: audioStreamData) } } public func addSsrcs(ssrcs: [UInt32]) { self.impl.with { impl in impl.addSsrcs(ssrcs: ssrcs) } } public func removeSsrcs(ssrcs: [UInt32]) { self.impl.with { impl in impl.removeSsrcs(ssrcs: ssrcs) } } public func removeIncomingVideoSource(_ ssrc: UInt32) { self.impl.with { impl in impl.removeIncomingVideoSource(ssrc) } } public func setVolume(ssrc: UInt32, volume: Double) { self.impl.with { impl in impl.setVolume(ssrc: ssrc, volume: volume) } } public func setRequestedVideoChannels(_ channels: [VideoChannel]) { self.impl.with { impl in impl.setRequestedVideoChannels(channels) } } public func stop() { self.impl.with { impl in impl.stop() } } public func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (OngoingCallContextPresentationCallVideoView?, OngoingCallContextPresentationCallVideoView?) -> Void) { self.impl.with { impl in impl.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: completion) } } public func video(endpointId: String) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.video(endpointId: endpointId).start(next: { value in subscriber.putNext(value) })) } return disposable } } public func addExternalAudioData(data: Data) { self.impl.with { impl in impl.addExternalAudioData(data: data) } } public func getStats(completion: @escaping (Stats) -> Void) { self.impl.with { impl in impl.getStats(completion: completion) } } public func setTone(tone: Tone?) { self.impl.with { impl in impl.setTone(tone: tone) } } }