Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TelegramVoip",
module_name = "TelegramVoip",
srcs = glob([
"Sources/**/*.swift",
], exclude = [
"Sources/macOS/**/*",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TgVoipWebrtc:TgVoipWebrtc",
"//submodules/FFMpegBinding",
"//submodules/ManagedFile",
"//submodules/AppBundle",
],
visibility = [
"//visibility:public",
],
)
+44
View File
@@ -0,0 +1,44 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "TelegramVoip",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "TelegramVoip",
targets: ["TelegramVoip"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "TgVoipWebrtc", path: "../../../tgcalls"),
.package(name: "SSignalKit", path: "../SSignalKit"),
.package(name: "FFMpegBinding", path: "../FFMpegBinding"),
.package(name: "TelegramCore", path: "../TelegramCore")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "TelegramVoip",
dependencies: [
.product(name: "TgVoipWebrtc", package: "TgVoipWebrtc", condition: nil),
.product(name: "SwiftSignalKit", package: "SSignalKit", condition: nil),
.product(name: "FFMpegBinding", package: "FFMpegBinding", condition: nil),
.product(name: "TelegramCore", package: "TelegramCore", condition: nil),
],
path: "Sources",
exclude: [
"IpcGroupCallContext.swift",
"OngoingCallContext.swift",
],
cxxSettings: [
.define("WEBRTC_MAC", to: "1", nil),
]),
]
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,424 @@
import Foundation
import SwiftSignalKit
import Network
@available(macOS 10.14, *)
public final class ReflectorBenchmark {
public struct Results {
public let bandwidthBytesPerSecond: Int
public let expectedBandwidthBytesPerSecond: Int
public let averageDelay: Double
public init(bandwidthBytesPerSecond: Int, expectedBandwidthBytesPerSecond: Int, averageDelay: Double) {
self.bandwidthBytesPerSecond = bandwidthBytesPerSecond
self.expectedBandwidthBytesPerSecond = expectedBandwidthBytesPerSecond
self.averageDelay = averageDelay
}
}
@available(macOS 10.14, *)
private final class Impl {
let queue: Queue
let address: String
let port: Int
let incomingTag: Data
let outgoingTag: Data
let outgoingRandomTag: Data
let targetBandwidthBytesPerSecond: Int
let sendPacketInterval: Double
let maxPacketCount: Int
var outgoingConnection: NWConnection?
var incomingConnection: NWConnection?
var completion: ((Results) -> Void)?
var incomingPingSendTimestamp: Double?
var didReceiveIncomingPing: Bool = false
var outgoingPingSendTimestamp: Double?
var didReceiveOutgoingPing: Bool = false
var sentPacketCount: Int = 0
var receivedPacketCount: Int = 0
var firstReceiveTimestamp: Double?
var lastReceiveTimestamp: Double?
var unconfirmedPacketSendTimestamp: [Data: Double] = [:]
var packetSizeAndTimeToReceive: [(Int, Double)] = []
var pingTimer: SwiftSignalKit.Timer?
var sendPacketTimer: SwiftSignalKit.Timer?
var bandwidthTimer: SwiftSignalKit.Timer?
init(queue: Queue, address: String, port: Int) {
self.queue = queue
self.address = address
self.port = port
self.targetBandwidthBytesPerSecond = 700 * 1024 / 8
self.sendPacketInterval = 1.0 / 30.0
self.maxPacketCount = Int(5.0 / self.sendPacketInterval)
var incomingTag = Data(count: 16)
var outgoingTag = Data(count: 16)
incomingTag.withUnsafeMutableBytes { incomingBuffer -> Void in
outgoingTag.withUnsafeMutableBytes { outgoingBuffer -> Void in
let incoming = incomingBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
let outgoing = outgoingBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
arc4random_buf(incoming, incomingBuffer.count)
memcpy(outgoing, incoming, incomingBuffer.count)
incoming[0] = 0
outgoing[0] = 1
}
}
var outgoingRandomTag = Data(count: 4)
outgoingRandomTag.withUnsafeMutableBytes { buffer -> Void in
arc4random_buf(buffer.baseAddress!, buffer.count)
}
self.incomingTag = incomingTag
self.outgoingTag = outgoingTag
self.outgoingRandomTag = outgoingRandomTag
}
deinit {
self.incomingConnection?.cancel()
self.outgoingConnection?.cancel()
self.pingTimer?.invalidate()
self.sendPacketTimer?.invalidate()
self.bandwidthTimer?.invalidate()
}
func start(completion: @escaping (Results) -> Void) {
self.completion = completion
let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(self.address), port: NWEndpoint.Port(integerLiteral: UInt16(self.port)))
let incomingConnection = NWConnection(to: endpoint, using: .udp)
self.incomingConnection = incomingConnection
let outgoingConnection = NWConnection(to: endpoint, using: .udp)
self.outgoingConnection = outgoingConnection
incomingConnection.start(queue: self.queue.queue)
outgoingConnection.start(queue: self.queue.queue)
self.receiveIncomingPacket()
self.receiveOutgoingPacket()
self.sendIncomingPingPackets()
self.pingTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.sendIncomingPingPackets()
self.sendOutgoingPingPackets()
}, queue: self.queue)
self.pingTimer?.start()
self.sendPacketTimer = SwiftSignalKit.Timer(timeout: self.sendPacketInterval, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.sendOutgoingPacket()
}, queue: self.queue)
self.sendPacketTimer?.start()
self.bandwidthTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.calculateStats()
}, queue: self.queue)
self.bandwidthTimer?.start()
}
private func calculateStats() {
guard let firstReceiveTimestamp = self.firstReceiveTimestamp, let lastReceiveTimestamp = self.lastReceiveTimestamp else {
return
}
if self.sentPacketCount < self.maxPacketCount {
return
}
var totalSize = 0
var totalDelay: Double = 0.0
let totalTime: Double = lastReceiveTimestamp - firstReceiveTimestamp
for item in self.packetSizeAndTimeToReceive {
totalSize += item.0
totalDelay += item.1
}
let averageDelay: Double
if !self.packetSizeAndTimeToReceive.isEmpty {
averageDelay = totalDelay / Double(self.packetSizeAndTimeToReceive.count)
} else {
averageDelay = 0.0
}
if totalTime != 0.0 {
let bandwidthBytesPerSecond = Int(Double(totalSize) / totalTime)
if let completion = self.completion {
self.completion = nil
completion(Results(
bandwidthBytesPerSecond: bandwidthBytesPerSecond,
expectedBandwidthBytesPerSecond: self.targetBandwidthBytesPerSecond,
averageDelay: averageDelay
))
}
}
}
private func sendIncomingPingPackets() {
guard let connection = self.incomingConnection else {
return
}
var packetData = Data()
packetData.append(self.incomingTag)
var controlByte1: UInt8 = 0xff
for _ in 0 ..< 12 {
packetData.append(&controlByte1, count: 1)
}
var controlByte2: UInt8 = 0xfe
packetData.append(&controlByte2, count: 1)
for _ in 0 ..< 3 {
packetData.append(&controlByte1, count: 1)
}
var testValue: UInt64 = 123
withUnsafeBytes(of: &testValue, { buffer -> Void in
packetData.append(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, count: 8)
})
var zeroByte: UInt8 = 0
while packetData.count % 4 != 0 {
packetData.append(&zeroByte, count: 1)
}
if self.incomingPingSendTimestamp == nil {
self.incomingPingSendTimestamp = CFAbsoluteTimeGetCurrent()
}
connection.send(content: packetData, completion: .contentProcessed({ _ in }))
}
private func sendOutgoingPingPackets() {
guard let connection = self.outgoingConnection else {
return
}
var packetData = Data()
packetData.append(self.outgoingTag)
var controlByte1: UInt8 = 0xff
for _ in 0 ..< 12 {
packetData.append(&controlByte1, count: 1)
}
var controlByte2: UInt8 = 0xfe
packetData.append(&controlByte2, count: 1)
for _ in 0 ..< 3 {
packetData.append(&controlByte1, count: 1)
}
var testValue: UInt64 = 123
withUnsafeBytes(of: &testValue, { buffer -> Void in
packetData.append(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, count: 8)
})
var zeroByte: UInt8 = 0
while packetData.count % 4 != 0 {
packetData.append(&zeroByte, count: 1)
}
if self.outgoingPingSendTimestamp == nil {
self.outgoingPingSendTimestamp = CFAbsoluteTimeGetCurrent()
}
connection.send(content: packetData, completion: .contentProcessed({ _ in }))
}
private func sendOutgoingPacket() {
let timestamp = CFAbsoluteTimeGetCurrent()
var timedOutPacketIds: [Data] = []
for (packetId, packetTimestamp) in self.unconfirmedPacketSendTimestamp {
let packetDelay = timestamp - packetTimestamp
if packetDelay > 2.0 {
timedOutPacketIds.append(packetId)
self.receivedPacketCount += 1
self.lastReceiveTimestamp = timestamp
}
}
for packetId in timedOutPacketIds {
self.unconfirmedPacketSendTimestamp.removeValue(forKey: packetId)
}
if let outgoingPingSendTimestamp = self.outgoingPingSendTimestamp, !self.didReceiveOutgoingPing {
if outgoingPingSendTimestamp < timestamp - 2.0 {
self.didReceiveOutgoingPing = true
self.sentPacketCount = self.maxPacketCount
}
}
if let incomingPingSendTimestamp = self.incomingPingSendTimestamp, !self.didReceiveIncomingPing {
if incomingPingSendTimestamp < timestamp - 2.0 {
self.didReceiveIncomingPing = true
self.sentPacketCount = self.maxPacketCount
}
}
guard let connection = self.outgoingConnection else {
return
}
if self.sentPacketCount >= self.maxPacketCount {
return
}
if !self.didReceiveIncomingPing && self.didReceiveOutgoingPing {
return
}
let bandwidthAdjustedPacketLength: Int32 = Int32(Double(self.targetBandwidthBytesPerSecond) * self.sendPacketInterval) + Int32.random(in: 0 ..< 1 * 1024)
var remainingPacketLength = bandwidthAdjustedPacketLength
while remainingPacketLength > 0 {
var packetData = Data()
packetData.append(self.outgoingTag)
packetData.append(self.outgoingRandomTag)
let packetLength = min(remainingPacketLength, 1 * 1024)
var dataLength: Int32 = 8 + bandwidthAdjustedPacketLength
withUnsafeBytes(of: &dataLength, { buffer -> Void in
packetData.append(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, count: buffer.count)
})
var packetId = Data(count: 8)
packetId.withUnsafeMutableBytes { buffer -> Void in
arc4random_buf(buffer.baseAddress!, buffer.count)
}
packetData.append(packetId)
var innerData = Data(count: Int(packetLength))
innerData.withUnsafeMutableBytes { buffer -> Void in
arc4random_buf(buffer.baseAddress!, buffer.count)
}
packetData.append(innerData)
var zeroByte: UInt8 = 0
while packetData.count % 4 != 0 {
packetData.append(&zeroByte, count: 1)
}
self.unconfirmedPacketSendTimestamp[packetId] = timestamp
remainingPacketLength -= packetLength
self.sentPacketCount += 1
if self.firstReceiveTimestamp == nil {
self.firstReceiveTimestamp = timestamp
}
connection.send(content: packetData, completion: .contentProcessed({ _ in }))
}
}
private func receiveIncomingPacket() {
guard let connection = self.incomingConnection else {
return
}
connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] content, _, _, error in
guard let self else {
return
}
if let content {
if content.count >= 16 + 4 + 4 + 8 {
let tag = content.subdata(in: 0 ..< 16)
if tag == self.incomingTag {
let packetId = content.subdata(in: (16 + 4 + 4) ..< (16 + 4 + 4 + 8))
if let sentTimestamp = self.unconfirmedPacketSendTimestamp.removeValue(forKey: packetId) {
let timestamp = CFAbsoluteTimeGetCurrent()
let packetSendReceiveDuration = timestamp - sentTimestamp
self.lastReceiveTimestamp = timestamp
self.receivedPacketCount += 1
self.packetSizeAndTimeToReceive.append((content.count, packetSendReceiveDuration))
} else {
var pingHeaderData = Data()
var controlByte1: UInt8 = 0xff
for _ in 0 ..< 8 {
pingHeaderData.append(&controlByte1, count: 1)
}
let pingPacketId = content.subdata(in: 16 ..< (16 + 8))
if pingPacketId == pingHeaderData {
self.didReceiveIncomingPing = true
} else {
//print("Unknown incoming packet id")
}
}
} else {
print("Invalid incoming tag")
}
} else {
print("Invalid content length: \(content.count)")
}
} else {
print("Incoming data receive error")
}
self.receiveIncomingPacket()
})
}
private func receiveOutgoingPacket() {
guard let connection = self.outgoingConnection else {
return
}
connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] content, _, _, error in
guard let self else {
return
}
if let content {
if content.count >= 16 + 8 {
let tag = content.subdata(in: 0 ..< 16)
if tag == self.outgoingTag {
let packetId = content.subdata(in: 16 ..< (16 + 8))
var pingHeaderData = Data()
var controlByte1: UInt8 = 0xff
for _ in 0 ..< 8 {
pingHeaderData.append(&controlByte1, count: 1)
}
if packetId == pingHeaderData {
self.didReceiveOutgoingPing = true
} else {
print("Unknown outgoing packet id")
}
} else {
print("Invalid outgoing tag")
}
} else {
print("Invalid content length: \(content.count)")
}
}
self.receiveOutgoingPacket()
})
}
}
private static let sharedQueue = Queue(name: "ReflectorBenchmark")
private let impl: QueueLocalObject<Impl>
public init(address: String, port: Int) {
let queue = ReflectorBenchmark.sharedQueue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, address: address, port: port)
})
}
public func start(completion: @escaping (Results) -> Void) {
self.impl.with { impl in
impl.start(completion: completion)
}
}
}
@@ -0,0 +1,968 @@
import Foundation
import SwiftSignalKit
import TgVoipWebrtc
import TelegramCore
import Network
import Postbox
import FFMpegBinding
import ManagedFile
@available(iOS 12.0, macOS 14.0, *)
public final class WrappedMediaStreamingContext {
private final class Impl {
let queue: Queue
let context: MediaStreamingContext
private let broadcastPartsSource = Atomic<BroadcastPartSource?>(value: nil)
init(queue: Queue, rejoinNeeded: @escaping () -> Void) {
self.queue = queue
var getBroadcastPartsSource: (() -> BroadcastPartSource?)?
self.context = MediaStreamingContext(
queue: ContextQueueImpl(queue: queue),
requestCurrentTime: { completion in
let disposable = MetaDisposable()
queue.async {
if let source = getBroadcastPartsSource?() {
disposable.set(source.requestTime(completion: completion))
} else {
completion(0)
}
}
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)
}
)
let broadcastPartsSource = self.broadcastPartsSource
getBroadcastPartsSource = {
return broadcastPartsSource.with { $0 }
}
}
deinit {
}
func setAudioStreamData(audioStreamData: OngoingGroupCallContext.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)
self.context.start()
}
}
func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
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() { videoFrameData in
subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData))
}
disposable.set(ActionDisposable {
innerDisposable.dispose()
})
}
return disposable
}
}
}
private let queue = Queue()
private let impl: QueueLocalObject<Impl>
public init(rejoinNeeded: @escaping () -> Void) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, rejoinNeeded: rejoinNeeded)
})
}
public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
self.impl.with { impl in
impl.setAudioStreamData(audioStreamData: audioStreamData)
}
}
public func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.video().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
}
@available(iOS 12.0, macOS 14.0, *)
public final class ExternalMediaStreamingContext: SharedHLSServerSource {
private final class Impl {
let queue: Queue
private var broadcastPartsSource: BroadcastPartSource?
private let resetPlaylistDisposable = MetaDisposable()
private let updatePlaylistDisposable = MetaDisposable()
let masterPlaylistData = Promise<String>()
let playlistData = Promise<String>()
let mediumPlaylistData = Promise<String>()
init(queue: Queue, rejoinNeeded: @escaping () -> Void) {
self.queue = queue
}
deinit {
self.updatePlaylistDisposable.dispose()
}
func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
if let audioStreamData {
let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream)
self.broadcastPartsSource = broadcastPartsSource
self.updatePlaylistDisposable.set(nil)
let queue = self.queue
self.resetPlaylistDisposable.set(broadcastPartsSource.requestTime(completion: { [weak self] timestamp in
queue.async {
guard let self else {
return
}
let segmentDuration: Int64 = 1000
var adjustedTimestamp: Int64 = 0
if timestamp > 0 {
adjustedTimestamp = timestamp / segmentDuration * segmentDuration - 4 * segmentDuration
}
if adjustedTimestamp > 0 {
var masterPlaylistData = "#EXTM3U\n" +
"#EXT-X-VERSION:6\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=3300000,RESOLUTION=1280x720,CODECS=\"avc1.64001f,mp4a.40.2\"\n" +
"hls_level_0.m3u8\n"
masterPlaylistData += "#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=640x360,CODECS=\"avc1.64001f,mp4a.40.2\"\n" +
"hls_level_1.m3u8\n"
self.masterPlaylistData.set(.single(masterPlaylistData))
self.beginUpdatingPlaylist(initialHeadTimestamp: adjustedTimestamp)
}
}
}))
}
}
private func beginUpdatingPlaylist(initialHeadTimestamp: Int64) {
let segmentDuration: Int64 = 1000
var timestamp = initialHeadTimestamp
self.updatePlaylist(headTimestamp: timestamp, quality: 0)
self.updatePlaylist(headTimestamp: timestamp, quality: 1)
self.updatePlaylistDisposable.set((
Signal<Void, NoError>.single(Void())
|> delay(1.0, queue: self.queue)
|> restart
|> deliverOn(self.queue)
).start(next: { [weak self] _ in
guard let self else {
return
}
timestamp += segmentDuration
self.updatePlaylist(headTimestamp: timestamp, quality: 0)
self.updatePlaylist(headTimestamp: timestamp, quality: 1)
}))
}
private func updatePlaylist(headTimestamp: Int64, quality: Int) {
let segmentDuration: Int64 = 1000
let headIndex = headTimestamp / segmentDuration
let minIndex = headIndex - 20
var playlistData = "#EXTM3U\n" +
"#EXT-X-VERSION:6\n" +
"#EXT-X-TARGETDURATION:1\n" +
"#EXT-X-MEDIA-SEQUENCE:\(minIndex)\n" +
"#EXT-X-INDEPENDENT-SEGMENTS\n"
for index in minIndex ... headIndex {
playlistData.append("#EXTINF:1.000000,\n")
playlistData.append("hls_stream\(quality)_\(index).ts\n")
}
//print("Player: updating playlist \(quality) \(minIndex) ... \(headIndex)")
if quality == 0 {
self.playlistData.set(.single(playlistData))
} else {
self.mediumPlaylistData.set(.single(playlistData))
}
}
func partData(index: Int, quality: Int) -> Signal<Data?, NoError> {
let segmentDuration: Int64 = 1000
let timestamp = Int64(index) * segmentDuration
print("Player: request part(q: \(quality)) \(index) -> \(timestamp)")
guard let broadcastPartsSource = self.broadcastPartsSource else {
return .single(nil)
}
return Signal { subscriber in
return broadcastPartsSource.requestPart(
timestampMilliseconds: timestamp,
durationMilliseconds: segmentDuration,
subject: .video(channelId: 1, quality: quality == 0 ? .full : .medium),
completion: { part in
var data = part.oggData
if data.count > 32 {
data = data.subdata(in: 32 ..< data.count)
}
subscriber.putNext(data)
},
rejoinNeeded: {
//TODO
}
)
}
}
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> {
return .never()
}
}
private let queue = Queue()
let internalId: CallSessionInternalId
private let impl: QueueLocalObject<Impl>
private var hlsServerDisposable: Disposable?
public var id: String {
return self.internalId.uuidString
}
public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) {
self.internalId = id
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, rejoinNeeded: rejoinNeeded)
})
self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(source: self, completion: {})
}
deinit {
self.hlsServerDisposable?.dispose()
}
public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
self.impl.with { impl in
impl.setAudioStreamData(audioStreamData: audioStreamData)
}
}
public func masterPlaylistData() -> Signal<String, NoError> {
return self.impl.signalWith { impl, subscriber in
impl.masterPlaylistData.get().start(next: subscriber.putNext)
}
}
public func playlistData(quality: Int) -> Signal<String, NoError> {
return self.impl.signalWith { impl, subscriber in
if quality == 0 {
impl.playlistData.get().start(next: subscriber.putNext)
} else {
impl.mediumPlaylistData.get().start(next: subscriber.putNext)
}
}
}
public func partData(index: Int, quality: Int) -> Signal<Data?, NoError> {
return self.impl.signalWith { impl, subscriber in
impl.partData(index: index, quality: quality).start(next: subscriber.putNext)
}
}
public func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError> {
return self.impl.signalWith { impl, subscriber in
impl.fileData(id: id, range: range).start(next: subscriber.putNext)
}
}
public func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> {
return .single(nil)
}
}
public final class DirectMediaStreamingContext {
public struct Playlist: Equatable {
public struct Part: Equatable {
public let index: Int
public let timestamp: Double
public let duration: Double
public init(index: Int, timestamp: Double, duration: Double) {
self.index = index
self.timestamp = timestamp
self.duration = duration
}
}
public var parts: [Part]
public init(parts: [Part]) {
self.parts = parts
}
}
private final class Impl {
let queue: Queue
private var broadcastPartsSource: BroadcastPartSource?
private let resetPlaylistDisposable = MetaDisposable()
private let updatePlaylistDisposable = MetaDisposable()
let playlistData = Promise<Playlist>()
init(queue: Queue, rejoinNeeded: @escaping () -> Void) {
self.queue = queue
}
deinit {
self.updatePlaylistDisposable.dispose()
}
func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
if let audioStreamData {
let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream)
self.broadcastPartsSource = broadcastPartsSource
self.updatePlaylistDisposable.set(nil)
let queue = self.queue
self.resetPlaylistDisposable.set(broadcastPartsSource.requestTime(completion: { [weak self] timestamp in
queue.async {
guard let self else {
return
}
let segmentDuration: Int64 = 1000
var adjustedTimestamp: Int64 = 0
if timestamp > 0 {
adjustedTimestamp = timestamp / segmentDuration * segmentDuration - 4 * segmentDuration
}
if adjustedTimestamp > 0 {
self.beginUpdatingPlaylist(initialHeadTimestamp: adjustedTimestamp)
}
}
}))
}
}
private func beginUpdatingPlaylist(initialHeadTimestamp: Int64) {
let segmentDuration: Int64 = 1000
var timestamp = initialHeadTimestamp
self.updatePlaylist(headTimestamp: timestamp)
self.updatePlaylistDisposable.set((
Signal<Void, NoError>.single(Void())
|> delay(1.0, queue: self.queue)
|> restart
|> deliverOn(self.queue)
).start(next: { [weak self] _ in
guard let self else {
return
}
timestamp += segmentDuration
self.updatePlaylist(headTimestamp: timestamp)
}))
}
private func updatePlaylist(headTimestamp: Int64) {
let segmentDuration: Int64 = 1000
let headIndex = headTimestamp / segmentDuration
let minIndex = headIndex - 20
var parts: [Playlist.Part] = []
for index in minIndex ... headIndex {
parts.append(DirectMediaStreamingContext.Playlist.Part(
index: Int(index),
timestamp: Double(index),
duration: 1.0
))
}
self.playlistData.set(.single(Playlist(parts: parts)))
}
func partData(index: Int) -> Signal<Data?, NoError> {
let segmentDuration: Int64 = 1000
let timestamp = Int64(index) * segmentDuration
//print("Player: request part(q: \(quality)) \(index) -> \(timestamp)")
guard let broadcastPartsSource = self.broadcastPartsSource else {
return .single(nil)
}
return Signal { subscriber in
return broadcastPartsSource.requestPart(
timestampMilliseconds: timestamp,
durationMilliseconds: segmentDuration,
subject: .video(channelId: 1, quality: .full),
completion: { part in
var data = part.oggData
if data.count > 32 {
data = data.subdata(in: 32 ..< data.count)
}
subscriber.putNext(data)
},
rejoinNeeded: {
//TODO
}
)
}
}
}
private let queue = Queue()
let internalId: CallSessionInternalId
private let impl: QueueLocalObject<Impl>
private var hlsServerDisposable: Disposable?
public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) {
self.internalId = id
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, rejoinNeeded: rejoinNeeded)
})
}
deinit {
}
public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
self.impl.with { impl in
impl.setAudioStreamData(audioStreamData: audioStreamData)
}
}
public func playlistData() -> Signal<Playlist, NoError> {
return self.impl.signalWith { impl, subscriber in
impl.playlistData.get().start(next: subscriber.putNext)
}
}
public func partData(index: Int) -> Signal<Data?, NoError> {
return self.impl.signalWith { impl, subscriber in
impl.partData(index: index).start(next: subscriber.putNext)
}
}
}
public protocol SharedHLSServerSource: AnyObject {
var id: String { get }
func masterPlaylistData() -> Signal<String, NoError>
func playlistData(quality: Int) -> Signal<String, NoError>
func partData(index: Int, quality: Int) -> Signal<Data?, NoError>
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError>
func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError>
}
@available(iOS 12.0, macOS 14.0, *)
public final class SharedHLSServer {
public typealias Source = SharedHLSServerSource
public static let shared: SharedHLSServer = {
return SharedHLSServer()
}()
private enum ResponseError {
case badRequest
case notFound
case internalServerError
var httpString: String {
switch self {
case .badRequest:
return "400 Bad Request"
case .notFound:
return "404 Not Found"
case .internalServerError:
return "500 Internal Server Error"
}
}
}
private final class SourceReference {
weak var source: SharedHLSServerSource?
init(source: SharedHLSServerSource) {
self.source = source
}
}
@available(iOS 12.0, macOS 14.0, *)
private final class Impl {
private let queue: Queue
private let port: NWEndpoint.Port
private var listener: NWListener?
private var sourceReferences = Bag<SourceReference>()
private var referenceCheckTimer: SwiftSignalKit.Timer?
private var shutdownTimer: SwiftSignalKit.Timer?
init(queue: Queue, port: UInt16) {
self.queue = queue
self.port = NWEndpoint.Port(rawValue: port)!
}
deinit {
self.referenceCheckTimer?.invalidate()
self.shutdownTimer?.invalidate()
}
private func updateNeedsListener() {
var isEmpty = true
for item in self.sourceReferences.copyItems() {
if let _ = item.source {
isEmpty = false
break
}
}
if isEmpty {
if self.listener != nil {
if self.shutdownTimer == nil {
self.shutdownTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
guard let self else {
return
}
self.shutdownTimer = nil
self.stopListener()
}, queue: self.queue)
self.shutdownTimer?.start()
}
}
if let referenceCheckTimer = self.referenceCheckTimer {
self.referenceCheckTimer = nil
referenceCheckTimer.invalidate()
}
} else {
if let shutdownTimer = self.shutdownTimer {
self.shutdownTimer = nil
shutdownTimer.invalidate()
}
if self.listener == nil {
self.startListener()
}
if self.referenceCheckTimer == nil {
self.referenceCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.updateNeedsListener()
}, queue: self.queue)
self.referenceCheckTimer?.start()
}
}
}
private func startListener() {
let listener: NWListener
do {
listener = try NWListener(using: .tcp, on: self.port)
} catch {
Logger.shared.log("SharedHLSServer", "Failed to create listener: \(error)")
return
}
self.listener = listener
listener.newConnectionHandler = { [weak self] connection in
guard let self else {
return
}
self.handleConnection(connection: connection)
}
listener.stateUpdateHandler = { [weak self, weak listener] state in
guard let self, let listener else {
return
}
switch state {
case .ready:
Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)")
case let .failed(error):
Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)")
listener.cancel()
listener.start(queue: self.queue.queue)
default:
break
}
}
listener.start(queue: self.queue.queue)
}
private func stopListener() {
guard let listener = self.listener else {
return
}
self.listener = nil
listener.cancel()
}
private func handleConnection(connection: NWConnection) {
connection.start(queue: self.queue.queue)
connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] data, _, isComplete, error in
guard let self else {
return
}
if let data, !data.isEmpty {
self.handleRequest(data: data, connection: connection)
} else if isComplete {
connection.cancel()
} else if let error = error {
Logger.shared.log("SharedHLSServer", "Error on connection: \(error)")
connection.cancel()
}
})
}
private func handleRequest(data: Data, connection: NWConnection) {
guard let requestString = String(data: data, encoding: .utf8) else {
connection.cancel()
return
}
if !requestString.hasPrefix("GET /") {
self.sendErrorAndClose(connection: connection)
return
}
guard let firstCrLf = requestString.range(of: "\r\n") else {
self.sendErrorAndClose(connection: connection)
return
}
let firstLine = String(requestString[requestString.index(requestString.startIndex, offsetBy: "GET /".count) ..< firstCrLf.lowerBound])
if !(firstLine.hasSuffix(" HTTP/1.0") || firstLine.hasSuffix(" HTTP/1.1")) {
self.sendErrorAndClose(connection: connection)
return
}
let requestPath = String(firstLine[firstLine.startIndex ..< firstLine.index(firstLine.endIndex, offsetBy: -" HTTP/1.1".count)])
var requestRange: Range<Int>?
if let rangeRange = requestString.range(of: "Range: bytes=") {
if let endRange = requestString.range(of: "\r\n", range: rangeRange.upperBound ..< requestString.endIndex) {
let rangeString = String(requestString[rangeRange.upperBound ..< endRange.lowerBound])
if let dashRange = rangeString.range(of: "-") {
let lowerBoundString = String(rangeString[rangeString.startIndex ..< dashRange.lowerBound])
let upperBoundString = String(rangeString[dashRange.upperBound ..< rangeString.endIndex])
if let lowerBound = Int(lowerBoundString), let upperBound = Int(upperBoundString) {
requestRange = lowerBound ..< upperBound
}
}
}
}
guard let firstSlash = requestPath.range(of: "/") else {
self.sendErrorAndClose(connection: connection, error: .notFound)
return
}
let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])
guard let source = self.sourceReferences.copyItems().first(where: { $0.source?.id == streamId })?.source else {
self.sendErrorAndClose(connection: connection)
return
}
let filePath = String(requestPath[firstSlash.upperBound...])
if filePath == "master.m3u8" {
let _ = (source.masterPlaylistData()
|> deliverOn(self.queue)
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!)
})
} else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") {
guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else {
self.sendErrorAndClose(connection: connection)
return
}
let _ = (source.playlistData(quality: levelIndex)
|> deliverOn(self.queue)
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!)
})
} else if filePath.hasPrefix("hls_stream") && filePath.hasSuffix(".ts") {
let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_stream".count) ..< filePath.index(filePath.endIndex, offsetBy: -".ts".count)])
guard let underscoreRange = fileId.range(of: "_") else {
self.sendErrorAndClose(connection: connection)
return
}
guard let levelIndex = Int(String(fileId[fileId.startIndex ..< underscoreRange.lowerBound])) else {
self.sendErrorAndClose(connection: connection)
return
}
guard let partIndex = Int(String(fileId[underscoreRange.upperBound...])) else {
self.sendErrorAndClose(connection: connection)
return
}
let _ = (source.partData(index: partIndex, quality: levelIndex)
|> deliverOn(self.queue)
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
if let result {
let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4")
let tempFile = TempBox.shared.tempFile(fileName: "part.ts")
defer {
TempBox.shared.dispose(sourceTempFile)
TempBox.shared.dispose(tempFile)
}
guard let _ = try? result.write(to: URL(fileURLWithPath: sourceTempFile.path)) else {
self.sendErrorAndClose(connection: connection, error: .internalServerError)
return
}
let sourcePath = sourceTempFile.path
FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex))
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) {
self.sendResponseAndClose(connection: connection, data: data)
} else {
self.sendErrorAndClose(connection: connection, error: .internalServerError)
}
} else {
self.sendErrorAndClose(connection: connection, error: .notFound)
}
})
} else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") {
let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)])
guard let fileIdValue = Int64(fileId) else {
self.sendErrorAndClose(connection: connection)
return
}
guard let requestRange else {
self.sendErrorAndClose(connection: connection)
return
}
let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1)
|> deliverOn(self.queue)
//|> timeout(5.0, queue: self.queue, alternate: .single(nil))
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
if let (tempFile, tempFileRange, totalSize) = result {
self.sendResponseFileAndClose(connection: connection, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize)
} else {
self.sendErrorAndClose(connection: connection, error: .internalServerError)
}
})
} else {
let _ = (source.arbitraryFileData(path: filePath)
|> deliverOn(self.queue)
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
if let result {
self.sendResponseAndClose(connection: connection, data: result.data, contentType: result.contentType)
} else {
self.sendErrorAndClose(connection: connection, error: .notFound)
}
})
}
}
private func sendErrorAndClose(connection: NWConnection, error: ResponseError = .badRequest) {
let errorResponse = "HTTP/1.1 \(error.httpString)\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"
connection.send(content: errorResponse.data(using: .utf8), completion: .contentProcessed { error in
if let error {
Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)")
}
connection.cancel()
})
}
private func sendResponseAndClose(connection: NWConnection, data: Data, contentType: String = "application/octet-stream", range: Range<Int>? = nil, totalSize: Int? = nil) {
var responseHeaders = "HTTP/1.1 200 OK\r\n"
responseHeaders.append("Content-Length: \(data.count)\r\n")
if let range, let totalSize {
responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n")
}
responseHeaders.append("Content-Type: \(contentType)\r\n")
responseHeaders.append("Connection: close\r\n")
responseHeaders.append("Access-Control-Allow-Origin: *\r\n")
responseHeaders.append("\r\n")
var responseData = Data()
responseData.append(responseHeaders.data(using: .utf8)!)
responseData.append(data)
connection.send(content: responseData, completion: .contentProcessed { error in
if let error {
Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)")
}
connection.cancel()
})
}
private static func sendRemainingFileRange(queue: Queue, connection: NWConnection, tempFile: TempBoxFile, managedFile: ManagedFile, remainingRange: Range<Int>, fileSize: Int) -> Void {
let blockSize = 256 * 1024
let clippedLowerBound = min(remainingRange.lowerBound, fileSize)
var clippedUpperBound = min(remainingRange.upperBound, fileSize)
clippedUpperBound = min(clippedUpperBound, clippedLowerBound + blockSize)
if clippedUpperBound == clippedLowerBound {
TempBox.shared.dispose(tempFile)
connection.cancel()
} else {
let _ = managedFile.seek(position: Int64(clippedLowerBound))
let data = managedFile.readData(count: Int(clippedUpperBound - clippedLowerBound))
let nextRange = clippedUpperBound ..< remainingRange.upperBound
connection.send(content: data, completion: .contentProcessed { error in
queue.async {
if let error {
Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)")
connection.cancel()
TempBox.shared.dispose(tempFile)
} else {
sendRemainingFileRange(queue: queue, connection: connection, tempFile: tempFile, managedFile: managedFile, remainingRange: nextRange, fileSize: fileSize)
}
}
})
}
}
private func sendResponseFileAndClose(connection: NWConnection, file: TempBoxFile, fileRange: Range<Int>, range: Range<Int>, totalSize: Int) {
let queue = self.queue
guard let managedFile = ManagedFile(queue: nil, path: file.path, mode: .read), let fileSize = managedFile.getSize() else {
self.sendErrorAndClose(connection: connection, error: .internalServerError)
TempBox.shared.dispose(file)
return
}
var responseHeaders = "HTTP/1.1 200 OK\r\n"
responseHeaders.append("Content-Length: \(fileRange.upperBound - fileRange.lowerBound)\r\n")
responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n")
responseHeaders.append("Content-Type: application/octet-stream\r\n")
responseHeaders.append("Connection: close\r\n")
responseHeaders.append("Access-Control-Allow-Origin: *\r\n")
responseHeaders.append("\r\n")
connection.send(content: responseHeaders.data(using: .utf8)!, completion: .contentProcessed({ _ in }))
Impl.sendRemainingFileRange(queue: queue, connection: connection, tempFile: file, managedFile: managedFile, remainingRange: fileRange, fileSize: Int(fileSize))
}
func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable {
let queue = self.queue
let index = self.sourceReferences.add(SourceReference(source: source))
self.updateNeedsListener()
completion()
return ActionDisposable { [weak self] in
queue.async {
guard let self else {
return
}
self.sourceReferences.remove(index)
self.updateNeedsListener()
}
}
}
}
private static let queue = Queue(name: "SharedHLSServer")
public let port: UInt16 = 8016
private let impl: QueueLocalObject<Impl>
private init() {
let queue = SharedHLSServer.queue
let port = self.port
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, port: port)
})
}
public func registerPlayer(source: SharedHLSServerSource, completion: @escaping () -> Void) -> Disposable {
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.registerPlayer(source: source, completion: completion))
}
return disposable
}
}
@@ -0,0 +1,160 @@
//
// OngoingCallVideoCapturer.swift
// TelegramVoip
//
// Created by Mikhail Filimonov on 07.01.2021.
//
import Foundation
import Cocoa
import TgVoipWebrtc
import SwiftSignalKit
public enum OngoingCallVideoOrientation {
case rotation0
case rotation90
case rotation180
case rotation270
}
public extension OngoingCallVideoOrientation {
init(_ orientation: OngoingCallVideoOrientationWebrtc) {
switch orientation {
case .orientation0:
self = .rotation0
case .orientation90:
self = .rotation90
case .orientation180:
self = .rotation180
case .orientation270:
self = .rotation270
@unknown default:
self = .rotation0
}
}
}
public final class OngoingCallContextPresentationCallVideoView {
public let view: NSView
public let setOnFirstFrameReceived: (((Float) -> Void)?) -> Void
public let getOrientation: () -> OngoingCallVideoOrientation
public let getAspect: () -> CGFloat
public let setOnOrientationUpdated: (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void
public let setVideoContentMode: (CALayerContentsGravity) -> Void
public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void
public let setIsPaused: (Bool) -> Void
public let renderToSize:(NSSize, Bool)->Void
public init(
view: NSView,
setOnFirstFrameReceived: @escaping (((Float) -> Void)?) -> Void,
getOrientation: @escaping () -> OngoingCallVideoOrientation,
getAspect: @escaping () -> CGFloat,
setOnOrientationUpdated: @escaping (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void,
setVideoContentMode: @escaping(CALayerContentsGravity) -> Void,
setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void,
setIsPaused: @escaping(Bool) -> Void,
renderToSize: @escaping(NSSize, Bool) -> Void
) {
self.view = view
self.setOnFirstFrameReceived = setOnFirstFrameReceived
self.getOrientation = getOrientation
self.getAspect = getAspect
self.setOnOrientationUpdated = setOnOrientationUpdated
self.setVideoContentMode = setVideoContentMode
self.setOnIsMirroredUpdated = setOnIsMirroredUpdated
self.setIsPaused = setIsPaused
self.renderToSize = renderToSize
}
}
public final class OngoingCallVideoCapturer {
public let impl: OngoingCallThreadLocalContextVideoCapturer
public let deviceId: String
public init(_ deviceId: String = "", keepLandscape: Bool = true) {
self.deviceId = deviceId
self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: deviceId, keepLandscape: keepLandscape)
}
public func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
return Signal { [weak self] subscriber in
let disposable = MetaDisposable()
guard let strongSelf = self else {
return disposable
}
let innerDisposable = strongSelf.impl.addVideoOutput({ videoFrameData in
subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData))
})
disposable.set(ActionDisposable {
innerDisposable.dispose()
})
return disposable
}
}
public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) {
self.impl.makeOutgoingVideoView(false, completion: { view, _ in
if let view = view {
completion(OngoingCallContextPresentationCallVideoView(
view: view, setOnFirstFrameReceived: { [weak view] f in
view?.setOnFirstFrameReceived(f)
}, getOrientation: {
return .rotation90
},
getAspect: { [weak view] in
if let view = view {
return view.aspect
} else {
return 0.0
}
},
setOnOrientationUpdated: { [weak view] f in
},
setVideoContentMode: { [weak view] mode in
view?.setVideoContentMode(mode)
}, setOnIsMirroredUpdated: { [weak view] f in
view?.setOnIsMirroredUpdated { value in
f?(value)
}
}, setIsPaused: { [weak view] paused in
view?.setIsPaused(paused)
},
renderToSize: { [weak view] size, animated in
view?.render(to: size, animated: animated)
}
))
} else {
completion(nil)
}
})
}
public func setIsVideoEnabled(_ value: Bool) {
self.impl.setIsVideoEnabled(value)
}
public func setOnFatalError(_ onError: @escaping()->Void) {
self.impl.setOnFatalError({
DispatchQueue.main.async(execute: onError)
})
}
public func setOnPause(_ onPause: @escaping(Bool)->Void) {
self.impl.setOnPause({ pause in
DispatchQueue.main.async(execute: {
onPause(pause)
})
})
}
public func switchVideoInput(_ deviceId: String) {
self.impl.switchVideoInput(deviceId)
}
}