mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 09:20:08 +00:00
[Temp]
This commit is contained in:
parent
ad0188f0ed
commit
22980d7c9a
@ -6,26 +6,60 @@ import SwiftSignalKit
|
|||||||
import BuildConfig
|
import BuildConfig
|
||||||
import BroadcastUploadHelpers
|
import BroadcastUploadHelpers
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
|
import Postbox
|
||||||
|
import CoreMedia
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
||||||
return appGroupPath + "/telegram-data"
|
return appGroupPath + "/telegram-data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 10.0, *)
|
private protocol BroadcastUploadImpl: AnyObject {
|
||||||
@objc(BroadcastUploadSampleHandler) class BroadcastUploadSampleHandler: RPBroadcastSampleHandler {
|
func initialize(rootPath: String)
|
||||||
|
func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer)
|
||||||
|
func processAudioSampleBuffer(data: Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class InProcessBroadcastUploadImpl: BroadcastUploadImpl {
|
||||||
|
private weak var extensionContext: RPBroadcastSampleHandler?
|
||||||
private var screencastBufferClientContext: IpcGroupCallBufferBroadcastContext?
|
private var screencastBufferClientContext: IpcGroupCallBufferBroadcastContext?
|
||||||
private var statusDisposable: Disposable?
|
private var statusDisposable: Disposable?
|
||||||
private var audioConverter: CustomAudioConverter?
|
|
||||||
|
init(extensionContext: RPBroadcastSampleHandler) {
|
||||||
|
self.extensionContext = extensionContext
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.statusDisposable?.dispose()
|
self.statusDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func beginRequest(with context: NSExtensionContext) {
|
func initialize(rootPath: String) {
|
||||||
super.beginRequest(with: context)
|
let screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: rootPath + "/broadcast-coordination")
|
||||||
|
self.screencastBufferClientContext = screencastBufferClientContext
|
||||||
|
|
||||||
|
var wasRunning = false
|
||||||
|
self.statusDisposable = (screencastBufferClientContext.status
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case .active:
|
||||||
|
wasRunning = true
|
||||||
|
case let .finished(reason):
|
||||||
|
if wasRunning {
|
||||||
|
self.finish(with: .screencastEnded)
|
||||||
|
} else {
|
||||||
|
self.finish(with: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func finish(with reason: IpcGroupCallBufferBroadcastContext.Status.FinishReason) {
|
private func finish(with reason: IpcGroupCallBufferBroadcastContext.Status.FinishReason) {
|
||||||
|
guard let extensionContext = self.extensionContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
var errorString: String?
|
var errorString: String?
|
||||||
switch reason {
|
switch reason {
|
||||||
case .callEnded:
|
case .callEnded:
|
||||||
@ -39,16 +73,247 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
|||||||
let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [
|
let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [
|
||||||
NSLocalizedDescriptionKey: errorString
|
NSLocalizedDescriptionKey: errorString
|
||||||
])
|
])
|
||||||
finishBroadcastWithError(error)
|
extensionContext.finishBroadcastWithError(error)
|
||||||
} else {
|
} else {
|
||||||
finishBroadcastGracefully(self)
|
finishBroadcastGracefully(extensionContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
||||||
|
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var orientation = CGImagePropertyOrientation.up
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber {
|
||||||
|
orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) ?? .up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let data = serializePixelBuffer(buffer: pixelBuffer) {
|
||||||
|
self.screencastBufferClientContext?.setCurrentFrame(data: data, orientation: orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAudioSampleBuffer(data: Data) {
|
||||||
|
self.screencastBufferClientContext?.writeAudioData(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class EmbeddedBroadcastUploadImpl: BroadcastUploadImpl {
|
||||||
|
private weak var extensionContext: RPBroadcastSampleHandler?
|
||||||
|
|
||||||
|
private var clientContext: IpcGroupCallEmbeddedBroadcastContext?
|
||||||
|
private var statusDisposable: Disposable?
|
||||||
|
|
||||||
|
private var callContextId: UInt32?
|
||||||
|
private var callContextDidSetJoinResponse: Bool = false
|
||||||
|
private var callContext: OngoingGroupCallContext?
|
||||||
|
private let screencastCapturer: OngoingCallVideoCapturer
|
||||||
|
|
||||||
|
private var joinPayloadDisposable: Disposable?
|
||||||
|
|
||||||
|
private var sampleBuffers: [CMSampleBuffer] = []
|
||||||
|
private var lastAcceptedTimestamp: Double?
|
||||||
|
|
||||||
|
init(extensionContext: RPBroadcastSampleHandler) {
|
||||||
|
self.extensionContext = extensionContext
|
||||||
|
|
||||||
|
self.screencastCapturer = OngoingCallVideoCapturer(isCustom: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.joinPayloadDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialize(rootPath: String) {
|
||||||
|
let clientContext = IpcGroupCallEmbeddedBroadcastContext(basePath: rootPath + "/embedded-broadcast-coordination")
|
||||||
|
self.clientContext = clientContext
|
||||||
|
|
||||||
|
var wasRunning = false
|
||||||
|
self.statusDisposable = (clientContext.status
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case let .active(id, joinResponse):
|
||||||
|
wasRunning = true
|
||||||
|
|
||||||
|
if self.callContextId != id {
|
||||||
|
if let callContext = self.callContext {
|
||||||
|
self.callContext = nil
|
||||||
|
self.callContextId = nil
|
||||||
|
self.callContextDidSetJoinResponse = false
|
||||||
|
self.joinPayloadDisposable?.dispose()
|
||||||
|
self.joinPayloadDisposable = nil
|
||||||
|
callContext.stop(account: nil, reportCallId: nil, debugLog: Promise())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let id {
|
||||||
|
if self.callContext == nil {
|
||||||
|
self.callContextId = id
|
||||||
|
let callContext = OngoingGroupCallContext(
|
||||||
|
audioSessionActive: .single(true),
|
||||||
|
video: self.screencastCapturer,
|
||||||
|
requestMediaChannelDescriptions: { _, _ in EmptyDisposable },
|
||||||
|
rejoinNeeded: { },
|
||||||
|
outgoingAudioBitrateKbit: nil,
|
||||||
|
videoContentType: .screencast,
|
||||||
|
enableNoiseSuppression: false,
|
||||||
|
disableAudioInput: true,
|
||||||
|
enableSystemMute: false,
|
||||||
|
preferX264: false,
|
||||||
|
logPath: "",
|
||||||
|
onMutedSpeechActivityDetected: { _ in },
|
||||||
|
encryptionKey: nil,
|
||||||
|
isConference: false,
|
||||||
|
sharedAudioDevice: nil
|
||||||
|
)
|
||||||
|
self.callContext = callContext
|
||||||
|
self.joinPayloadDisposable = (callContext.joinPayload
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] joinPayload in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.callContextId != id {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.clientContext?.joinPayload = IpcGroupCallEmbeddedAppContext.JoinPayload(
|
||||||
|
id: id,
|
||||||
|
data: joinPayload.0,
|
||||||
|
ssrc: joinPayload.1
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let callContext = self.callContext {
|
||||||
|
if let joinResponse, !self.callContextDidSetJoinResponse {
|
||||||
|
self.callContextDidSetJoinResponse = true
|
||||||
|
callContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
|
||||||
|
callContext.setJoinResponse(payload: joinResponse.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .finished(reason):
|
||||||
|
if wasRunning {
|
||||||
|
self.finish(with: .screencastEnded)
|
||||||
|
} else {
|
||||||
|
self.finish(with: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finish(with reason: IpcGroupCallEmbeddedBroadcastContext.Status.FinishReason) {
|
||||||
|
guard let extensionContext = self.extensionContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var errorString: String?
|
||||||
|
switch reason {
|
||||||
|
case .callEnded:
|
||||||
|
errorString = "You're not in a voice chat"
|
||||||
|
case .error:
|
||||||
|
errorString = "Finished"
|
||||||
|
case .screencastEnded:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let errorString = errorString {
|
||||||
|
let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [
|
||||||
|
NSLocalizedDescriptionKey: errorString
|
||||||
|
])
|
||||||
|
extensionContext.finishBroadcastWithError(error)
|
||||||
|
} else {
|
||||||
|
finishBroadcastGracefully(extensionContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
||||||
|
let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds
|
||||||
|
if let lastAcceptedTimestamp = self.lastAcceptedTimestamp {
|
||||||
|
if lastAcceptedTimestamp + 1.0 / 30.0 > timestamp {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.lastAcceptedTimestamp = timestamp
|
||||||
|
|
||||||
|
guard let sourceImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let sourcePixelBuffer: CVPixelBuffer = sourceImageBuffer as CVPixelBuffer
|
||||||
|
|
||||||
|
let width = CVPixelBufferGetWidth(sourcePixelBuffer)
|
||||||
|
let height = CVPixelBufferGetHeight(sourcePixelBuffer)
|
||||||
|
let sourceBytesPerRow = CVPixelBufferGetBytesPerRow(sourcePixelBuffer)
|
||||||
|
|
||||||
|
var outputPixelBuffer: CVPixelBuffer?
|
||||||
|
let pixelFormat = CVPixelBufferGetPixelFormatType(sourcePixelBuffer)
|
||||||
|
CVPixelBufferCreate(nil, width, height, pixelFormat, nil, &outputPixelBuffer)
|
||||||
|
guard let outputPixelBuffer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CVPixelBufferLockBaseAddress(sourcePixelBuffer, [])
|
||||||
|
CVPixelBufferLockBaseAddress(outputPixelBuffer, [])
|
||||||
|
|
||||||
|
let outputBytesPerRow = CVPixelBufferGetBytesPerRow(outputPixelBuffer)
|
||||||
|
|
||||||
|
let sourceBaseAddress = CVPixelBufferGetBaseAddress(sourcePixelBuffer)
|
||||||
|
let outputBaseAddress = CVPixelBufferGetBaseAddress(outputPixelBuffer)
|
||||||
|
|
||||||
|
if outputBytesPerRow == sourceBytesPerRow {
|
||||||
|
memcpy(outputBaseAddress!, sourceBaseAddress!, height * outputBytesPerRow)
|
||||||
|
} else {
|
||||||
|
for y in 0 ..< height {
|
||||||
|
memcpy(outputBaseAddress!.advanced(by: y * outputBytesPerRow), sourceBaseAddress!.advanced(by: y * sourceBytesPerRow), min(sourceBytesPerRow, outputBytesPerRow))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
CVPixelBufferUnlockBaseAddress(sourcePixelBuffer, [])
|
||||||
|
CVPixelBufferUnlockBaseAddress(outputPixelBuffer, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
var orientation = CGImagePropertyOrientation.up
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber {
|
||||||
|
orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) ?? .up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let outputSampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: outputPixelBuffer) {
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
self.screencastCapturer.injectSampleBuffer(outputSampleBuffer, rotation: orientation, completion: {
|
||||||
|
//semaphore.signal()
|
||||||
|
})
|
||||||
|
let _ = semaphore.wait(timeout: DispatchTime.now() + 1.0 / 30.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAudioSampleBuffer(data: Data) {
|
||||||
|
self.callContext?.addExternalAudioData(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 10.0, *)
|
||||||
|
@objc(BroadcastUploadSampleHandler) class BroadcastUploadSampleHandler: RPBroadcastSampleHandler {
|
||||||
|
private var impl: BroadcastUploadImpl?
|
||||||
|
private var audioConverter: CustomAudioConverter?
|
||||||
|
|
||||||
|
public override func beginRequest(with context: NSExtensionContext) {
|
||||||
|
super.beginRequest(with: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishWithError() {
|
||||||
|
let errorString = "Finished"
|
||||||
|
let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [
|
||||||
|
NSLocalizedDescriptionKey: errorString
|
||||||
|
])
|
||||||
|
self.finishBroadcastWithError(error)
|
||||||
|
}
|
||||||
|
|
||||||
override public func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
|
override public func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
|
||||||
guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||||
self.finish(with: .error)
|
self.finishWithError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,35 +323,32 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
|||||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||||
|
|
||||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||||
self.finish(with: .error)
|
self.finishWithError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
||||||
|
|
||||||
|
TempBox.initializeShared(basePath: rootPath, processType: "share", launchSpecificId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
|
|
||||||
let logsPath = rootPath + "/logs/broadcast-logs"
|
let logsPath = rootPath + "/logs/broadcast-logs"
|
||||||
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
let screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: rootPath + "/broadcast-coordination")
|
let embeddedBroadcastImplementationTypePath = rootPath + "/broadcast-coordination-type"
|
||||||
self.screencastBufferClientContext = screencastBufferClientContext
|
|
||||||
|
|
||||||
var wasRunning = false
|
var useIPCContext = false
|
||||||
self.statusDisposable = (screencastBufferClientContext.status
|
if let typeData = try? Data(contentsOf: URL(fileURLWithPath: embeddedBroadcastImplementationTypePath)), let type = String(data: typeData, encoding: .utf8) {
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
useIPCContext = type == "ipc"
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
switch status {
|
|
||||||
case .active:
|
let impl: BroadcastUploadImpl
|
||||||
wasRunning = true
|
if useIPCContext {
|
||||||
case let .finished(reason):
|
impl = EmbeddedBroadcastUploadImpl(extensionContext: self)
|
||||||
if wasRunning {
|
|
||||||
strongSelf.finish(with: .screencastEnded)
|
|
||||||
} else {
|
} else {
|
||||||
strongSelf.finish(with: reason)
|
impl = InProcessBroadcastUploadImpl(extensionContext: self)
|
||||||
}
|
}
|
||||||
}
|
self.impl = impl
|
||||||
})
|
impl.initialize(rootPath: rootPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func broadcastPaused() {
|
override public func broadcastPaused() {
|
||||||
@ -112,18 +374,7 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
private func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
||||||
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
self.impl?.processVideoSampleBuffer(sampleBuffer: sampleBuffer)
|
||||||
return
|
|
||||||
}
|
|
||||||
var orientation = CGImagePropertyOrientation.up
|
|
||||||
if #available(iOS 11.0, *) {
|
|
||||||
if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber {
|
|
||||||
orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) ?? .up
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let data = serializePixelBuffer(buffer: pixelBuffer) {
|
|
||||||
self.screencastBufferClientContext?.setCurrentFrame(data: data, orientation: orientation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processAudioSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
private func processAudioSampleBuffer(sampleBuffer: CMSampleBuffer) {
|
||||||
@ -133,9 +384,6 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
|||||||
guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) else {
|
guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
/*guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
|
|
||||||
return
|
|
||||||
}*/
|
|
||||||
|
|
||||||
let format = CustomAudioConverter.Format(
|
let format = CustomAudioConverter.Format(
|
||||||
numChannels: Int(asbd.pointee.mChannelsPerFrame),
|
numChannels: Int(asbd.pointee.mChannelsPerFrame),
|
||||||
@ -146,7 +394,7 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
|||||||
}
|
}
|
||||||
if let audioConverter = self.audioConverter {
|
if let audioConverter = self.audioConverter {
|
||||||
if let data = audioConverter.convert(sampleBuffer: sampleBuffer), !data.isEmpty {
|
if let data = audioConverter.convert(sampleBuffer: sampleBuffer), !data.isEmpty {
|
||||||
self.screencastBufferClientContext?.writeAudioData(data: data)
|
self.impl?.processAudioSampleBuffer(data: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -287,3 +535,36 @@ private func converterComplexInputDataProc(inAudioConverter: AudioConverterRef,
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
|
||||||
|
var maybeFormat: CMVideoFormatDescription?
|
||||||
|
let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat)
|
||||||
|
if status != noErr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let format = maybeFormat else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var timingInfo = CMSampleTimingInfo(
|
||||||
|
duration: CMTimeMake(value: 1, timescale: 30),
|
||||||
|
presentationTimeStamp: CMTimeMake(value: 0, timescale: 30),
|
||||||
|
decodeTimeStamp: CMTimeMake(value: 0, timescale: 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
var maybeSampleBuffer: CMSampleBuffer?
|
||||||
|
let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer)
|
||||||
|
|
||||||
|
if (bufferStatus != noErr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let sampleBuffer = maybeSampleBuffer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray
|
||||||
|
let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary
|
||||||
|
dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber
|
||||||
|
|
||||||
|
return sampleBuffer
|
||||||
|
}
|
||||||
|
|||||||
@ -805,7 +805,8 @@ public protocol TelegramRootControllerInterface: NavigationController {
|
|||||||
func getPrivacySettings() -> Promise<AccountPrivacySettings?>?
|
func getPrivacySettings() -> Promise<AccountPrivacySettings?>?
|
||||||
func openSettings()
|
func openSettings()
|
||||||
func openBirthdaySetup()
|
func openBirthdaySetup()
|
||||||
func openPhotoSetup()
|
func openPhotoSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?)
|
||||||
|
func openAvatars()
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol QuickReplySetupScreenInitialData: AnyObject {
|
public protocol QuickReplySetupScreenInitialData: AnyObject {
|
||||||
|
|||||||
@ -953,6 +953,11 @@ public final class PeerInfoNavigationSourceTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PeerInfoAvatarUploadStatus {
|
||||||
|
case progress(Float)
|
||||||
|
case done
|
||||||
|
}
|
||||||
|
|
||||||
public protocol PeerInfoScreen: ViewController {
|
public protocol PeerInfoScreen: ViewController {
|
||||||
var peerId: PeerId { get }
|
var peerId: PeerId { get }
|
||||||
var privacySettings: Promise<AccountPrivacySettings?> { get }
|
var privacySettings: Promise<AccountPrivacySettings?> { get }
|
||||||
@ -961,7 +966,8 @@ public protocol PeerInfoScreen: ViewController {
|
|||||||
func toggleStorySelection(ids: [Int32], isSelected: Bool)
|
func toggleStorySelection(ids: [Int32], isSelected: Bool)
|
||||||
func togglePaneIsReordering(isReordering: Bool)
|
func togglePaneIsReordering(isReordering: Bool)
|
||||||
func cancelItemSelection()
|
func cancelItemSelection()
|
||||||
func openAvatarSetup()
|
func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?)
|
||||||
|
func openAvatars()
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Peer {
|
public extension Peer {
|
||||||
|
|||||||
@ -112,6 +112,7 @@ swift_library(
|
|||||||
"//submodules/ChatPresentationInterfaceState",
|
"//submodules/ChatPresentationInterfaceState",
|
||||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||||
"//submodules/TelegramUI/Components/LottieComponent",
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TelegramUI/Components/AvatarUploadToastScreen",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -52,6 +52,7 @@ import ArchiveInfoScreen
|
|||||||
import BirthdayPickerScreen
|
import BirthdayPickerScreen
|
||||||
import OldChannelsController
|
import OldChannelsController
|
||||||
import TextFormat
|
import TextFormat
|
||||||
|
import AvatarUploadToastScreen
|
||||||
|
|
||||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||||
let controller: ViewController
|
let controller: ViewController
|
||||||
@ -1208,7 +1209,54 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let rootController = self.navigationController as? TelegramRootControllerInterface {
|
if let rootController = self.navigationController as? TelegramRootControllerInterface {
|
||||||
rootController.openPhotoSetup()
|
rootController.openPhotoSetup(completedWithUploadingImage: { [weak self] image, uploadStatus in
|
||||||
|
guard let self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let toastScreen = AvatarUploadToastScreen(
|
||||||
|
context: self.context,
|
||||||
|
image: image,
|
||||||
|
uploadStatus: uploadStatus,
|
||||||
|
arrowTarget: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let tabController = self.parent as? TabBarController else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let settingsController = tabController.controllers.first(where: { $0 is PeerInfoScreen }) as? PeerInfoScreen else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let tabFrame = tabController.frameForControllerTab(controller: settingsController) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (tabController.view, tabFrame)
|
||||||
|
},
|
||||||
|
viewUploadedAvatar: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let rootController = self.navigationController as? TelegramRootControllerInterface {
|
||||||
|
rootController.openAvatars()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if let navigationController = self.navigationController as? NavigationController {
|
||||||
|
var viewControllers = navigationController.viewControllers
|
||||||
|
if let index = viewControllers.firstIndex(where: { $0 is TabBarController }) {
|
||||||
|
viewControllers.insert(toastScreen, at: index + 1)
|
||||||
|
} else {
|
||||||
|
viewControllers.append(toastScreen)
|
||||||
|
}
|
||||||
|
navigationController.setViewControllers(viewControllers, animated: true)
|
||||||
|
} else {
|
||||||
|
self.push(toastScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toastScreen.targetAvatarView
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3211,7 +3211,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
|
|
||||||
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||||
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||||
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(15.0), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
}
|
}
|
||||||
|
|
||||||
var badgeSize: CGFloat = 0.0
|
var badgeSize: CGFloat = 0.0
|
||||||
@ -4010,8 +4010,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let (actionButtonTitleNodeLayout, apply) = actionButtonTitleNodeLayoutAndApply {
|
if let (actionButtonTitleNodeLayout, apply) = actionButtonTitleNodeLayoutAndApply {
|
||||||
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + 12.0 * 2.0, height: actionButtonTitleNodeLayout.size.height + 5.0 + 4.0)
|
let actionButtonSideInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 12.0 / 17.0)
|
||||||
let actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.maxY - actionButtonSize.height, width: actionButtonSize.width, height: actionButtonSize.height)
|
let actionButtonTopInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 5.0 / 17.0)
|
||||||
|
let actionButtonBottomInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0)
|
||||||
|
|
||||||
|
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset)
|
||||||
|
var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height)
|
||||||
|
actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0))
|
||||||
|
|
||||||
let actionButtonNode: HighlightableButtonNode
|
let actionButtonNode: HighlightableButtonNode
|
||||||
if let current = strongSelf.actionButtonNode {
|
if let current = strongSelf.actionButtonNode {
|
||||||
@ -4030,11 +4035,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
actionButtonBackgroundView = UIImageView()
|
actionButtonBackgroundView = UIImageView()
|
||||||
strongSelf.actionButtonBackgroundView = actionButtonBackgroundView
|
strongSelf.actionButtonBackgroundView = actionButtonBackgroundView
|
||||||
actionButtonNode.view.addSubview(actionButtonBackgroundView)
|
actionButtonNode.view.addSubview(actionButtonBackgroundView)
|
||||||
|
}
|
||||||
|
|
||||||
if actionButtonBackgroundView.image?.size.height != actionButtonSize.height {
|
if actionButtonBackgroundView.image?.size.height != actionButtonSize.height {
|
||||||
actionButtonBackgroundView.image = generateStretchableFilledCircleImage(diameter: actionButtonSize.height, color: .white)?.withRenderingMode(.alwaysTemplate)
|
actionButtonBackgroundView.image = generateStretchableFilledCircleImage(diameter: actionButtonSize.height, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
actionButtonBackgroundView.tintColor = theme.unreadBadgeActiveBackgroundColor
|
actionButtonBackgroundView.tintColor = theme.unreadBadgeActiveBackgroundColor
|
||||||
|
|
||||||
@ -4047,7 +4052,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
|
|
||||||
actionButtonNode.frame = actionButtonFrame
|
actionButtonNode.frame = actionButtonFrame
|
||||||
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
|
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
|
||||||
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: 5.0), size: actionButtonTitleNodeLayout.size)
|
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size)
|
||||||
|
|
||||||
nextBadgeX -= actionButtonSize.width + 6.0
|
nextBadgeX -= actionButtonSize.width + 6.0
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1994,6 +1994,11 @@ public final class ChatListNode: ListView {
|
|||||||
starsSubscriptionsContextPromise.get()
|
starsSubscriptionsContextPromise.get()
|
||||||
)
|
)
|
||||||
|> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext -> Signal<ChatListNotice?, NoError> in
|
|> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext -> Signal<ChatListNotice?, NoError> in
|
||||||
|
#if DEBUG
|
||||||
|
var suggestions = suggestions
|
||||||
|
suggestions.insert(.setupPhoto, at: 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
let (accountPeer, birthday) = data
|
let (accountPeer, birthday) = data
|
||||||
|
|
||||||
if let newSessionReview = newSessionReviews.first {
|
if let newSessionReview = newSessionReviews.first {
|
||||||
|
|||||||
@ -218,6 +218,16 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
}
|
}
|
||||||
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition)
|
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
if let result = super.hitTest(point, with: event) {
|
||||||
|
if result === self.view {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var node: Node {
|
public var node: Node {
|
||||||
|
|||||||
@ -107,7 +107,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
case experimentalCallMute(Bool)
|
case experimentalCallMute(Bool)
|
||||||
case conferenceCalls(Bool)
|
case conferenceCalls(Bool)
|
||||||
case playerV2(Bool)
|
case playerV2(Bool)
|
||||||
case benchmarkReflectors
|
case devRequests(Bool)
|
||||||
|
case fakeAds(Bool)
|
||||||
case enableLocalTranslation(Bool)
|
case enableLocalTranslation(Bool)
|
||||||
case preferredVideoCodec(Int, String, String?, Bool)
|
case preferredVideoCodec(Int, String, String?, Bool)
|
||||||
case disableVideoAspectScaling(Bool)
|
case disableVideoAspectScaling(Bool)
|
||||||
@ -133,7 +134,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
return DebugControllerSection.web.rawValue
|
return DebugControllerSection.web.rawValue
|
||||||
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
|
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
|
||||||
return DebugControllerSection.experiments.rawValue
|
return DebugControllerSection.experiments.rawValue
|
||||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .conferenceCalls, .playerV2, .benchmarkReflectors, .enableLocalTranslation:
|
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .conferenceCalls, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
|
||||||
return DebugControllerSection.experiments.rawValue
|
return DebugControllerSection.experiments.rawValue
|
||||||
case .logTranslationRecognition, .resetTranslationStates:
|
case .logTranslationRecognition, .resetTranslationStates:
|
||||||
return DebugControllerSection.translation.rawValue
|
return DebugControllerSection.translation.rawValue
|
||||||
@ -254,12 +255,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
return 53
|
return 53
|
||||||
case .playerV2:
|
case .playerV2:
|
||||||
return 54
|
return 54
|
||||||
case .benchmarkReflectors:
|
case .devRequests:
|
||||||
return 55
|
return 55
|
||||||
case .enableLocalTranslation:
|
case .fakeAds:
|
||||||
return 56
|
return 56
|
||||||
|
case .enableLocalTranslation:
|
||||||
|
return 57
|
||||||
case let .preferredVideoCodec(index, _, _, _):
|
case let .preferredVideoCodec(index, _, _, _):
|
||||||
return 57 + index
|
return 58 + index
|
||||||
case .disableVideoAspectScaling:
|
case .disableVideoAspectScaling:
|
||||||
return 100
|
return 100
|
||||||
case .enableNetworkFramework:
|
case .enableNetworkFramework:
|
||||||
@ -1368,60 +1371,25 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
})
|
})
|
||||||
}).start()
|
}).start()
|
||||||
})
|
})
|
||||||
case .benchmarkReflectors:
|
case let .devRequests(value):
|
||||||
return ItemListActionItem(presentationData: presentationData, title: "Benchmark Reflectors", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
return ItemListSwitchItem(presentationData: presentationData, title: "PlayerV2", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||||
guard let context = arguments.context else {
|
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||||
return
|
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||||
}
|
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||||
|
settings.devRequests = value
|
||||||
var signal: Signal<ReflectorBenchmark.Results, NoError> = Signal { subscriber in
|
return PreferencesEntry(settings)
|
||||||
var reflectorBenchmark: ReflectorBenchmark? = ReflectorBenchmark(address: "91.108.13.35", port: 599)
|
|
||||||
reflectorBenchmark?.start(completion: { results in
|
|
||||||
subscriber.putNext(results)
|
|
||||||
subscriber.putCompletion()
|
|
||||||
})
|
})
|
||||||
|
}).start()
|
||||||
return ActionDisposable {
|
})
|
||||||
reflectorBenchmark = nil
|
case let .fakeAds(value):
|
||||||
}
|
return ItemListSwitchItem(presentationData: presentationData, title: "Fake Ads", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||||
}
|
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||||
|> runOn(.mainQueue())
|
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||||
|
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||||
var cancelImpl: (() -> Void)?
|
settings.fakeAds = value
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
return PreferencesEntry(settings)
|
||||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
})
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
}).start()
|
||||||
cancelImpl?()
|
|
||||||
}))
|
|
||||||
arguments.presentController(controller, nil)
|
|
||||||
return ActionDisposable { [weak controller] in
|
|
||||||
Queue.mainQueue().async() {
|
|
||||||
controller?.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|> runOn(Queue.mainQueue())
|
|
||||||
|> delay(0.15, queue: Queue.mainQueue())
|
|
||||||
let progressDisposable = progressSignal.start()
|
|
||||||
|
|
||||||
let reindexDisposable = MetaDisposable()
|
|
||||||
|
|
||||||
signal = signal
|
|
||||||
|> afterDisposed {
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
progressDisposable.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelImpl = {
|
|
||||||
reindexDisposable.set(nil)
|
|
||||||
}
|
|
||||||
reindexDisposable.set((signal
|
|
||||||
|> deliverOnMainQueue).start(next: { results in
|
|
||||||
if let context = arguments.context {
|
|
||||||
let controller = textAlertController(context: context, title: nil, text: "Bandwidth: \(results.bandwidthBytesPerSecond * 8 / 1024) kbit/s (expected \(results.expectedBandwidthBytesPerSecond * 8 / 1024) kbit/s)\nAvg latency: \(Int(results.averageDelay * 1000.0)) ms", actions: [TextAlertAction(type: .genericAction, title: "OK", action: {})])
|
|
||||||
arguments.presentController(controller, nil)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
case let .enableLocalTranslation(value):
|
case let .enableLocalTranslation(value):
|
||||||
return ItemListSwitchItem(presentationData: presentationData, title: "Local Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
return ItemListSwitchItem(presentationData: presentationData, title: "Local Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||||
@ -1593,22 +1561,11 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
|||||||
entries.append(.conferenceCalls(experimentalSettings.conferenceCalls))
|
entries.append(.conferenceCalls(experimentalSettings.conferenceCalls))
|
||||||
entries.append(.playerV2(experimentalSettings.playerV2))
|
entries.append(.playerV2(experimentalSettings.playerV2))
|
||||||
|
|
||||||
entries.append(.benchmarkReflectors)
|
entries.append(.devRequests(experimentalSettings.devRequests))
|
||||||
|
entries.append(.fakeAds(experimentalSettings.fakeAds))
|
||||||
entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))
|
entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*let codecs: [(String, String?)] = [
|
|
||||||
("No Preference", nil),
|
|
||||||
("H265", "H265"),
|
|
||||||
("H264", "H264"),
|
|
||||||
("VP8", "VP8"),
|
|
||||||
("VP9", "VP9")
|
|
||||||
]
|
|
||||||
|
|
||||||
for i in 0 ..< codecs.count {
|
|
||||||
entries.append(.preferredVideoCodec(i, codecs[i].0, codecs[i].1, experimentalSettings.preferredVideoCodec == codecs[i].1))
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if isMainApp {
|
if isMainApp {
|
||||||
entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling))
|
entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling))
|
||||||
entries.append(.enableNetworkFramework(networkSettings?.useNetworkFramework ?? useBetaFeatures))
|
entries.append(.enableNetworkFramework(networkSettings?.useNetworkFramework ?? useBetaFeatures))
|
||||||
|
|||||||
@ -535,6 +535,9 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
return self.dim.view
|
return self.dim.view
|
||||||
}
|
}
|
||||||
if self.isFlat {
|
if self.isFlat {
|
||||||
|
if result === self.container.view {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
var currentParent: UIView? = result
|
var currentParent: UIView? = result
|
||||||
|
|||||||
@ -1738,8 +1738,25 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
} else if let _ = item.content as? PlatformVideoContent {
|
} else if let _ = item.content as? PlatformVideoContent {
|
||||||
disablePlayerControls = true
|
disablePlayerControls = true
|
||||||
forceEnablePiP = true
|
forceEnablePiP = true
|
||||||
} else if let _ = item.content as? HLSVideoContent {
|
} else if let content = item.content as? HLSVideoContent {
|
||||||
isAdaptive = true
|
isAdaptive = true
|
||||||
|
|
||||||
|
if let qualitySet = HLSQualitySet(baseFile: content.fileReference, codecConfiguration: HLSCodecConfiguration(isHardwareAv1Supported: false, isSoftwareAv1Supported: true)), let (quality, playlistFile) = qualitySet.playlistFiles.sorted(by: { $0.key < $1.key }).first, let dataFile = qualitySet.qualityFiles[quality] {
|
||||||
|
var alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)] = []
|
||||||
|
for (otherQuality, otherPlaylistFile) in qualitySet.playlistFiles {
|
||||||
|
if otherQuality != quality, let otherDataFile = qualitySet.qualityFiles[otherQuality] {
|
||||||
|
alternativeQualities.append((otherPlaylistFile, dataFile: otherDataFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.videoFramePreview = MediaPlayerFramePreviewHLS(
|
||||||
|
postbox: item.context.account.postbox,
|
||||||
|
userLocation: content.userLocation,
|
||||||
|
userContentType: .video,
|
||||||
|
playlistFile: playlistFile,
|
||||||
|
mainDataFile: dataFile,
|
||||||
|
alternativeQualities: alternativeQualities
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = isAdaptive
|
let _ = isAdaptive
|
||||||
|
|||||||
@ -651,6 +651,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||||||
self.gridNode.scrollView.addSubview(cameraView)
|
self.gridNode.scrollView.addSubview(cameraView)
|
||||||
self.gridNode.addSubnode(self.cameraActivateAreaNode)
|
self.gridNode.addSubnode(self.cameraActivateAreaNode)
|
||||||
} else if useModernCamera, !Camera.isIpad {
|
} else if useModernCamera, !Camera.isIpad {
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
var cameraPosition: Camera.Position = .back
|
var cameraPosition: Camera.Position = .back
|
||||||
if case .assets(nil, .createAvatar) = controller.subject {
|
if case .assets(nil, .createAvatar) = controller.subject {
|
||||||
cameraPosition = .front
|
cameraPosition = .front
|
||||||
@ -703,6 +704,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||||||
} else {
|
} else {
|
||||||
setupCamera()
|
setupCamera()
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
self.containerNode.clipsToBounds = true
|
self.containerNode.clipsToBounds = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ private func FFMpegLookaheadReader_readPacketCallback(userData: UnsafeMutableRaw
|
|||||||
memcpy(buffer, bytes, fetchedData.count)
|
memcpy(buffer, bytes, fetchedData.count)
|
||||||
}
|
}
|
||||||
let fetchedCount = Int32(fetchedData.count)
|
let fetchedCount = Int32(fetchedData.count)
|
||||||
|
print("Fetched from \(context.readingOffset) (\(fetchedCount) bytes)")
|
||||||
context.setReadingOffset(offset: context.readingOffset + Int64(fetchedCount))
|
context.setReadingOffset(offset: context.readingOffset + Int64(fetchedCount))
|
||||||
if fetchedCount == 0 {
|
if fetchedCount == 0 {
|
||||||
return FFMPEG_CONSTANT_AVERROR_EOF
|
return FFMPEG_CONSTANT_AVERROR_EOF
|
||||||
@ -79,12 +80,12 @@ private final class FFMpegLookaheadReader {
|
|||||||
var audioStream: FFMpegFileReader.StreamInfo?
|
var audioStream: FFMpegFileReader.StreamInfo?
|
||||||
var videoStream: FFMpegFileReader.StreamInfo?
|
var videoStream: FFMpegFileReader.StreamInfo?
|
||||||
|
|
||||||
var seekInfo: FFMpegLookaheadThread.State.Seek?
|
var seekInfo: FFMpegLookahead.State.Seek?
|
||||||
var maxReadPts: FFMpegLookaheadThread.State.Seek?
|
var maxReadPts: FFMpegLookahead.State.Seek?
|
||||||
var audioStreamState: FFMpegLookaheadThread.StreamState?
|
var audioStreamState: FFMpegLookahead.StreamState?
|
||||||
var videoStreamState: FFMpegLookaheadThread.StreamState?
|
var videoStreamState: FFMpegLookahead.StreamState?
|
||||||
|
|
||||||
var reportedState: FFMpegLookaheadThread.State?
|
var reportedState: FFMpegLookahead.State?
|
||||||
|
|
||||||
var readingOffset: Int64 = 0
|
var readingOffset: Int64 = 0
|
||||||
var isCancelled: Bool = false
|
var isCancelled: Bool = false
|
||||||
@ -108,6 +109,8 @@ private final class FFMpegLookaheadReader {
|
|||||||
let avFormatContext = FFMpegAVFormatContext()
|
let avFormatContext = FFMpegAVFormatContext()
|
||||||
avFormatContext.setIO(avIoContext)
|
avFormatContext.setIO(avIoContext)
|
||||||
|
|
||||||
|
self.setReadingOffset(offset: 0)
|
||||||
|
|
||||||
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -170,7 +173,7 @@ private final class FFMpegLookaheadReader {
|
|||||||
|
|
||||||
if let preferredStream = self.videoStream ?? self.audioStream {
|
if let preferredStream = self.videoStream ?? self.audioStream {
|
||||||
let pts = CMTimeMakeWithSeconds(params.seekToTimestamp, preferredTimescale: preferredStream.timeScale)
|
let pts = CMTimeMakeWithSeconds(params.seekToTimestamp, preferredTimescale: preferredStream.timeScale)
|
||||||
self.seekInfo = FFMpegLookaheadThread.State.Seek(streamIndex: preferredStream.index, pts: pts.value)
|
self.seekInfo = FFMpegLookahead.State.Seek(streamIndex: preferredStream.index, pts: pts.value)
|
||||||
avFormatContext.seekFrame(forStreamIndex: Int32(preferredStream.index), pts: pts.value, positionOnKeyframe: true)
|
avFormatContext.seekFrame(forStreamIndex: Int32(preferredStream.index), pts: pts.value, positionOnKeyframe: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +226,7 @@ private final class FFMpegLookaheadReader {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxPtsSeconds = max(self.params.seekToTimestamp, currentTimestamp) + 10.0
|
let maxPtsSeconds = max(self.params.seekToTimestamp, currentTimestamp) + self.params.lookaheadDuration
|
||||||
|
|
||||||
var currentAudioPtsSecondsAdvanced: Double = 0.0
|
var currentAudioPtsSecondsAdvanced: Double = 0.0
|
||||||
var currentVideoPtsSecondsAdvanced: Double = 0.0
|
var currentVideoPtsSecondsAdvanced: Double = 0.0
|
||||||
@ -258,14 +261,14 @@ private final class FFMpegLookaheadReader {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
self.maxReadPts = FFMpegLookaheadThread.State.Seek(streamIndex: Int(packet.streamIndex), pts: packet.pts)
|
self.maxReadPts = FFMpegLookahead.State.Seek(streamIndex: Int(packet.streamIndex), pts: packet.pts)
|
||||||
|
|
||||||
if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index {
|
if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index {
|
||||||
let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timeScale)
|
let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timeScale)
|
||||||
if let audioStreamState = self.audioStreamState {
|
if let audioStreamState = self.audioStreamState {
|
||||||
currentAudioPtsSecondsAdvanced += pts.seconds - audioStreamState.readableToTime.seconds
|
currentAudioPtsSecondsAdvanced += pts.seconds - audioStreamState.readableToTime.seconds
|
||||||
}
|
}
|
||||||
self.audioStreamState = FFMpegLookaheadThread.StreamState(
|
self.audioStreamState = FFMpegLookahead.StreamState(
|
||||||
info: audioStream,
|
info: audioStream,
|
||||||
readableToTime: pts
|
readableToTime: pts
|
||||||
)
|
)
|
||||||
@ -274,7 +277,7 @@ private final class FFMpegLookaheadReader {
|
|||||||
if let videoStreamState = self.videoStreamState {
|
if let videoStreamState = self.videoStreamState {
|
||||||
currentVideoPtsSecondsAdvanced += pts.seconds - videoStreamState.readableToTime.seconds
|
currentVideoPtsSecondsAdvanced += pts.seconds - videoStreamState.readableToTime.seconds
|
||||||
}
|
}
|
||||||
self.videoStreamState = FFMpegLookaheadThread.StreamState(
|
self.videoStreamState = FFMpegLookahead.StreamState(
|
||||||
info: videoStream,
|
info: videoStream,
|
||||||
readableToTime: pts
|
readableToTime: pts
|
||||||
)
|
)
|
||||||
@ -300,7 +303,7 @@ private final class FFMpegLookaheadReader {
|
|||||||
stateIsFullyInitialised = false
|
stateIsFullyInitialised = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = FFMpegLookaheadThread.State(
|
let state = FFMpegLookahead.State(
|
||||||
seek: seekInfo,
|
seek: seekInfo,
|
||||||
maxReadablePts: self.maxReadPts,
|
maxReadablePts: self.maxReadPts,
|
||||||
audio: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.audioStreamState : nil,
|
audio: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.audioStreamState : nil,
|
||||||
@ -315,45 +318,10 @@ private final class FFMpegLookaheadReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class FFMpegLookaheadThread: NSObject {
|
private final class FFMpegLookaheadThread: NSObject {
|
||||||
struct StreamState: Equatable {
|
|
||||||
let info: FFMpegFileReader.StreamInfo
|
|
||||||
let readableToTime: CMTime
|
|
||||||
|
|
||||||
init(info: FFMpegFileReader.StreamInfo, readableToTime: CMTime) {
|
|
||||||
self.info = info
|
|
||||||
self.readableToTime = readableToTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct State: Equatable {
|
|
||||||
struct Seek: Equatable {
|
|
||||||
var streamIndex: Int
|
|
||||||
var pts: Int64
|
|
||||||
|
|
||||||
init(streamIndex: Int, pts: Int64) {
|
|
||||||
self.streamIndex = streamIndex
|
|
||||||
self.pts = pts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let seek: Seek
|
|
||||||
let maxReadablePts: Seek?
|
|
||||||
let audio: StreamState?
|
|
||||||
let video: StreamState?
|
|
||||||
let isEnded: Bool
|
|
||||||
|
|
||||||
init(seek: Seek, maxReadablePts: Seek?, audio: StreamState?, video: StreamState?, isEnded: Bool) {
|
|
||||||
self.seek = seek
|
|
||||||
self.maxReadablePts = maxReadablePts
|
|
||||||
self.audio = audio
|
|
||||||
self.video = video
|
|
||||||
self.isEnded = isEnded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class Params: NSObject {
|
final class Params: NSObject {
|
||||||
let seekToTimestamp: Double
|
let seekToTimestamp: Double
|
||||||
let updateState: (State) -> Void
|
let lookaheadDuration: Double
|
||||||
|
let updateState: (FFMpegLookahead.State) -> Void
|
||||||
let fetchInRange: (Range<Int64>) -> Disposable
|
let fetchInRange: (Range<Int64>) -> Disposable
|
||||||
let getDataInRange: (Range<Int64>, @escaping (Data?) -> Void) -> Disposable
|
let getDataInRange: (Range<Int64>, @escaping (Data?) -> Void) -> Disposable
|
||||||
let isDataCachedInRange: (Range<Int64>) -> Bool
|
let isDataCachedInRange: (Range<Int64>) -> Bool
|
||||||
@ -363,7 +331,8 @@ private final class FFMpegLookaheadThread: NSObject {
|
|||||||
|
|
||||||
init(
|
init(
|
||||||
seekToTimestamp: Double,
|
seekToTimestamp: Double,
|
||||||
updateState: @escaping (State) -> Void,
|
lookaheadDuration: Double,
|
||||||
|
updateState: @escaping (FFMpegLookahead.State) -> Void,
|
||||||
fetchInRange: @escaping (Range<Int64>) -> Disposable,
|
fetchInRange: @escaping (Range<Int64>) -> Disposable,
|
||||||
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
|
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
|
||||||
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
|
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
|
||||||
@ -372,6 +341,7 @@ private final class FFMpegLookaheadThread: NSObject {
|
|||||||
currentTimestamp: Atomic<Double?>
|
currentTimestamp: Atomic<Double?>
|
||||||
) {
|
) {
|
||||||
self.seekToTimestamp = seekToTimestamp
|
self.seekToTimestamp = seekToTimestamp
|
||||||
|
self.lookaheadDuration = lookaheadDuration
|
||||||
self.updateState = updateState
|
self.updateState = updateState
|
||||||
self.fetchInRange = fetchInRange
|
self.fetchInRange = fetchInRange
|
||||||
self.getDataInRange = getDataInRange
|
self.getDataInRange = getDataInRange
|
||||||
@ -414,14 +384,51 @@ private final class FFMpegLookaheadThread: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class FFMpegLookahead {
|
final class FFMpegLookahead {
|
||||||
|
struct StreamState: Equatable {
|
||||||
|
let info: FFMpegFileReader.StreamInfo
|
||||||
|
let readableToTime: CMTime
|
||||||
|
|
||||||
|
init(info: FFMpegFileReader.StreamInfo, readableToTime: CMTime) {
|
||||||
|
self.info = info
|
||||||
|
self.readableToTime = readableToTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State: Equatable {
|
||||||
|
struct Seek: Equatable {
|
||||||
|
var streamIndex: Int
|
||||||
|
var pts: Int64
|
||||||
|
|
||||||
|
init(streamIndex: Int, pts: Int64) {
|
||||||
|
self.streamIndex = streamIndex
|
||||||
|
self.pts = pts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let seek: Seek
|
||||||
|
let maxReadablePts: Seek?
|
||||||
|
let audio: StreamState?
|
||||||
|
let video: StreamState?
|
||||||
|
let isEnded: Bool
|
||||||
|
|
||||||
|
init(seek: Seek, maxReadablePts: Seek?, audio: StreamState?, video: StreamState?, isEnded: Bool) {
|
||||||
|
self.seek = seek
|
||||||
|
self.maxReadablePts = maxReadablePts
|
||||||
|
self.audio = audio
|
||||||
|
self.video = video
|
||||||
|
self.isEnded = isEnded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let cancel = Promise<Void>()
|
private let cancel = Promise<Void>()
|
||||||
private let currentTimestamp = Atomic<Double?>(value: nil)
|
private let currentTimestamp = Atomic<Double?>(value: nil)
|
||||||
private let thread: Thread
|
private let thread: Thread
|
||||||
|
|
||||||
init(
|
init(
|
||||||
seekToTimestamp: Double,
|
seekToTimestamp: Double,
|
||||||
updateState: @escaping (FFMpegLookaheadThread.State) -> Void,
|
lookaheadDuration: Double,
|
||||||
|
updateState: @escaping (FFMpegLookahead.State) -> Void,
|
||||||
fetchInRange: @escaping (Range<Int64>) -> Disposable,
|
fetchInRange: @escaping (Range<Int64>) -> Disposable,
|
||||||
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
|
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
|
||||||
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
|
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
|
||||||
@ -432,6 +439,7 @@ private final class FFMpegLookahead {
|
|||||||
selector: #selector(FFMpegLookaheadThread.entryPoint(_:)),
|
selector: #selector(FFMpegLookaheadThread.entryPoint(_:)),
|
||||||
object: FFMpegLookaheadThread.Params(
|
object: FFMpegLookaheadThread.Params(
|
||||||
seekToTimestamp: seekToTimestamp,
|
seekToTimestamp: seekToTimestamp,
|
||||||
|
lookaheadDuration: lookaheadDuration,
|
||||||
updateState: updateState,
|
updateState: updateState,
|
||||||
fetchInRange: fetchInRange,
|
fetchInRange: fetchInRange,
|
||||||
getDataInRange: getDataInRange,
|
getDataInRange: getDataInRange,
|
||||||
@ -496,7 +504,7 @@ final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl {
|
|||||||
let lookaheadId = self.currentLookaheadId
|
let lookaheadId = self.currentLookaheadId
|
||||||
|
|
||||||
let resource = self.resource
|
let resource = self.resource
|
||||||
let updateState: (FFMpegLookaheadThread.State) -> Void = { [weak self] state in
|
let updateState: (FFMpegLookahead.State) -> Void = { [weak self] state in
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -580,6 +588,7 @@ final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl {
|
|||||||
|
|
||||||
self.lookahead = FFMpegLookahead(
|
self.lookahead = FFMpegLookahead(
|
||||||
seekToTimestamp: position,
|
seekToTimestamp: position,
|
||||||
|
lookaheadDuration: 10.0,
|
||||||
updateState: updateState,
|
updateState: updateState,
|
||||||
fetchInRange: { range in
|
fetchInRange: { range in
|
||||||
return fetchedMediaResource(
|
return fetchedMediaResource(
|
||||||
|
|||||||
@ -26,16 +26,41 @@ private func FFMpegFileReader_readPacketCallback(userData: UnsafeMutableRawPoint
|
|||||||
return Int32(result)
|
return Int32(result)
|
||||||
case let .resource(resource):
|
case let .resource(resource):
|
||||||
let readCount = min(256 * 1024, Int64(bufferSize))
|
let readCount = min(256 * 1024, Int64(bufferSize))
|
||||||
let requestRange: Range<Int64> = resource.readingPosition ..< (resource.readingPosition + readCount)
|
|
||||||
|
|
||||||
|
var bufferOffset = 0
|
||||||
|
let doRead: (Range<Int64>) -> Void = { range in
|
||||||
//TODO:improve thread safe read if incomplete
|
//TODO:improve thread safe read if incomplete
|
||||||
if let (file, readSize) = resource.mediaBox.internal_resourceData(id: resource.resource.id, size: resource.size, in: requestRange) {
|
if let (file, readSize) = resource.mediaBox.internal_resourceData(id: resource.resource.id, size: resource.resourceSize, in: range) {
|
||||||
let result = file.read(buffer, readSize)
|
let effectiveReadSize = max(0, min(Int(readCount) - bufferOffset, readSize))
|
||||||
if result == 0 {
|
let count = file.read(buffer.advanced(by: bufferOffset), effectiveReadSize)
|
||||||
return FFMPEG_CONSTANT_AVERROR_EOF
|
bufferOffset += count
|
||||||
|
resource.readingPosition += Int64(count)
|
||||||
}
|
}
|
||||||
resource.readingPosition += Int64(result)
|
}
|
||||||
return Int32(result)
|
|
||||||
|
var mappedRangePosition: Int64 = 0
|
||||||
|
for mappedRange in resource.mappedRanges {
|
||||||
|
let bytesToRead = readCount - Int64(bufferOffset)
|
||||||
|
if bytesToRead <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
|
||||||
|
let mappedRangeReadingPosition = resource.readingPosition - mappedRangePosition
|
||||||
|
|
||||||
|
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
|
||||||
|
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
|
||||||
|
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
|
||||||
|
if mappedRangeBytesToRead > 0 {
|
||||||
|
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
|
||||||
|
doRead(mappedReadRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedRangePosition += mappedRangeSize
|
||||||
|
}
|
||||||
|
if bufferOffset != 0 {
|
||||||
|
return Int32(bufferOffset)
|
||||||
} else {
|
} else {
|
||||||
return FFMPEG_CONSTANT_AVERROR_EOF
|
return FFMPEG_CONSTANT_AVERROR_EOF
|
||||||
}
|
}
|
||||||
@ -65,7 +90,7 @@ private func FFMpegFileReader_seekCallback(userData: UnsafeMutableRawPointer?, o
|
|||||||
final class FFMpegFileReader {
|
final class FFMpegFileReader {
|
||||||
enum SourceDescription {
|
enum SourceDescription {
|
||||||
case file(String)
|
case file(String)
|
||||||
case resource(mediaBox: MediaBox, resource: MediaResource, size: Int64)
|
case resource(mediaBox: MediaBox, resource: MediaResource, resourceSize: Int64, mappedRanges: [Range<Int64>])
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StreamInfo: Equatable {
|
final class StreamInfo: Equatable {
|
||||||
@ -117,12 +142,21 @@ final class FFMpegFileReader {
|
|||||||
final class Resource {
|
final class Resource {
|
||||||
let mediaBox: MediaBox
|
let mediaBox: MediaBox
|
||||||
let resource: MediaResource
|
let resource: MediaResource
|
||||||
|
let resourceSize: Int64
|
||||||
|
let mappedRanges: [Range<Int64>]
|
||||||
let size: Int64
|
let size: Int64
|
||||||
var readingPosition: Int64 = 0
|
var readingPosition: Int64 = 0
|
||||||
|
|
||||||
init(mediaBox: MediaBox, resource: MediaResource, size: Int64) {
|
init(mediaBox: MediaBox, resource: MediaResource, resourceSize: Int64, mappedRanges: [Range<Int64>]) {
|
||||||
self.mediaBox = mediaBox
|
self.mediaBox = mediaBox
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
|
self.resourceSize = resourceSize
|
||||||
|
self.mappedRanges = mappedRanges
|
||||||
|
|
||||||
|
var size: Int64 = 0
|
||||||
|
for range in mappedRanges {
|
||||||
|
size += range.upperBound - range.lowerBound
|
||||||
|
}
|
||||||
self.size = size
|
self.size = size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,6 +213,11 @@ final class FFMpegFileReader {
|
|||||||
case index(Int)
|
case index(Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Seek {
|
||||||
|
case stream(streamIndex: Int, pts: Int64)
|
||||||
|
case direct(position: Double)
|
||||||
|
}
|
||||||
|
|
||||||
enum ReadFrameResult {
|
enum ReadFrameResult {
|
||||||
case frame(MediaTrackFrame)
|
case frame(MediaTrackFrame)
|
||||||
case waitingForMoreData
|
case waitingForMoreData
|
||||||
@ -200,7 +239,7 @@ final class FFMpegFileReader {
|
|||||||
private var lastReadPts: (streamIndex: Int, pts: Int64)?
|
private var lastReadPts: (streamIndex: Int, pts: Int64)?
|
||||||
private var isWaitingForMoreData: Bool = false
|
private var isWaitingForMoreData: Bool = false
|
||||||
|
|
||||||
public init?(source: SourceDescription, passthroughDecoder: Bool = false, useHardwareAcceleration: Bool, selectedStream: SelectedStream, seek: (streamIndex: Int, pts: Int64)?, maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) {
|
public init?(source: SourceDescription, passthroughDecoder: Bool = false, useHardwareAcceleration: Bool, selectedStream: SelectedStream, seek: Seek?, maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) {
|
||||||
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
|
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
|
||||||
|
|
||||||
switch source {
|
switch source {
|
||||||
@ -209,8 +248,8 @@ final class FFMpegFileReader {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
self.source = .file(file)
|
self.source = .file(file)
|
||||||
case let .resource(mediaBox, resource, size):
|
case let .resource(mediaBox, resource, resourceSize, mappedRanges):
|
||||||
self.source = .resource(Source.Resource(mediaBox: mediaBox, resource: resource, size: size))
|
self.source = .resource(Source.Resource(mediaBox: mediaBox, resource: resource, resourceSize: resourceSize, mappedRanges: mappedRanges))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.maxReadablePts = maxReadablePts
|
self.maxReadablePts = maxReadablePts
|
||||||
@ -350,7 +389,12 @@ final class FFMpegFileReader {
|
|||||||
self.stream = stream
|
self.stream = stream
|
||||||
|
|
||||||
if let seek {
|
if let seek {
|
||||||
avFormatContext.seekFrame(forStreamIndex: Int32(seek.streamIndex), pts: seek.pts, positionOnKeyframe: true)
|
switch seek {
|
||||||
|
case let .stream(streamIndex, pts):
|
||||||
|
avFormatContext.seekFrame(forStreamIndex: Int32(streamIndex), pts: pts, positionOnKeyframe: true)
|
||||||
|
case let .direct(position):
|
||||||
|
avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: CMTimeMakeWithSeconds(Float64(position), preferredTimescale: stream.info.timeScale).value, positionOnKeyframe: true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: 0, positionOnKeyframe: true)
|
avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: 0, positionOnKeyframe: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,14 +135,27 @@ public final class FFMpegMediaDataReaderV2: MediaDataReader {
|
|||||||
self.isVideo = isVideo
|
self.isVideo = isVideo
|
||||||
|
|
||||||
let source: FFMpegFileReader.SourceDescription
|
let source: FFMpegFileReader.SourceDescription
|
||||||
var seek: (streamIndex: Int, pts: Int64)?
|
var seek: FFMpegFileReader.Seek?
|
||||||
var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
|
var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
|
||||||
switch content {
|
switch content {
|
||||||
case let .tempFile(tempFile):
|
case let .tempFile(tempFile):
|
||||||
source = .file(tempFile.file.path)
|
source = .file(tempFile.file.path)
|
||||||
case let .directStream(directStream):
|
case let .directStream(directStream):
|
||||||
source = .resource(mediaBox: directStream.mediaBox, resource: directStream.resource, size: directStream.size)
|
let mappedRanges: [Range<Int64>]
|
||||||
seek = (directStream.seek.streamIndex, directStream.seek.pts)
|
#if DEBUG && false
|
||||||
|
var mappedRangesValue: [Range<Int64>] = []
|
||||||
|
var testOffset: Int64 = 0
|
||||||
|
while testOffset < directStream.size {
|
||||||
|
let testBlock: Int64 = min(3 * 1024 + 1, directStream.size - testOffset)
|
||||||
|
mappedRangesValue.append(testOffset ..< (testOffset + testBlock))
|
||||||
|
testOffset += testBlock
|
||||||
|
}
|
||||||
|
mappedRanges = mappedRangesValue
|
||||||
|
#else
|
||||||
|
mappedRanges = [0 ..< directStream.size]
|
||||||
|
#endif
|
||||||
|
source = .resource(mediaBox: directStream.mediaBox, resource: directStream.resource, resourceSize: directStream.size, mappedRanges: mappedRanges)
|
||||||
|
seek = .stream(streamIndex: directStream.seek.streamIndex, pts: directStream.seek.pts)
|
||||||
maxReadablePts = directStream.maxReadablePts
|
maxReadablePts = directStream.maxReadablePts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import SwiftSignalKit
|
|||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import FFMpegBinding
|
import FFMpegBinding
|
||||||
|
import VideoToolbox
|
||||||
|
|
||||||
public enum FramePreviewResult {
|
public enum FramePreviewResult {
|
||||||
case image(UIImage)
|
case image(UIImage)
|
||||||
@ -151,3 +152,534 @@ public final class MediaPlayerFramePreview: FramePreview {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class MediaPlayerFramePreviewHLS: FramePreview {
|
||||||
|
private final class Impl {
|
||||||
|
private struct Part {
|
||||||
|
var timestamp: Int
|
||||||
|
var duration: Int
|
||||||
|
var range: Range<Int>
|
||||||
|
|
||||||
|
init(timestamp: Int, duration: Int, range: Range<Int>) {
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.duration = duration
|
||||||
|
self.range = range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Playlist {
|
||||||
|
let dataFile: FileMediaReference
|
||||||
|
let initializationPart: Part
|
||||||
|
let parts: [Part]
|
||||||
|
|
||||||
|
init(dataFile: FileMediaReference, initializationPart: Part, parts: [Part]) {
|
||||||
|
self.dataFile = dataFile
|
||||||
|
self.initializationPart = initializationPart
|
||||||
|
self.parts = parts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue: Queue
|
||||||
|
let postbox: Postbox
|
||||||
|
let userLocation: MediaResourceUserLocation
|
||||||
|
let userContentType: MediaResourceUserContentType
|
||||||
|
let playlistFile: FileMediaReference
|
||||||
|
let mainDataFile: FileMediaReference
|
||||||
|
let alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)]
|
||||||
|
|
||||||
|
private var playlist: Playlist?
|
||||||
|
private var alternativePlaylists: [Playlist] = []
|
||||||
|
private var fetchPlaylistDisposable: Disposable?
|
||||||
|
private var playlistDisposable: Disposable?
|
||||||
|
|
||||||
|
private var pendingFrame: (Int, FFMpegLookahead)?
|
||||||
|
private let nextRequestedFrame: Atomic<Double?>
|
||||||
|
|
||||||
|
let framePipe = ValuePipe<FramePreviewResult>()
|
||||||
|
|
||||||
|
init(
|
||||||
|
queue: Queue,
|
||||||
|
postbox: Postbox,
|
||||||
|
userLocation: MediaResourceUserLocation,
|
||||||
|
userContentType: MediaResourceUserContentType,
|
||||||
|
playlistFile: FileMediaReference,
|
||||||
|
mainDataFile: FileMediaReference,
|
||||||
|
alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)],
|
||||||
|
nextRequestedFrame: Atomic<Double?>
|
||||||
|
) {
|
||||||
|
self.queue = queue
|
||||||
|
self.postbox = postbox
|
||||||
|
self.userLocation = userLocation
|
||||||
|
self.userContentType = userContentType
|
||||||
|
self.playlistFile = playlistFile
|
||||||
|
self.mainDataFile = mainDataFile
|
||||||
|
self.alternativeQualities = alternativeQualities
|
||||||
|
self.nextRequestedFrame = nextRequestedFrame
|
||||||
|
|
||||||
|
self.loadPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.fetchPlaylistDisposable?.dispose()
|
||||||
|
self.playlistDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFrame() {
|
||||||
|
if self.pendingFrame != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateFrameRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelPendingFrames() {
|
||||||
|
self.pendingFrame = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPlaylist() {
|
||||||
|
if self.fetchPlaylistDisposable != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadPlaylist: (FileMediaReference, FileMediaReference) -> Signal<Playlist?, NoError> = { playlistFile, dataFile in
|
||||||
|
return self.postbox.mediaBox.resourceData(playlistFile.media.resource)
|
||||||
|
|> mapToSignal { data -> Signal<Playlist?, NoError> in
|
||||||
|
if !data.complete {
|
||||||
|
return .never()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
guard let playlistString = String(data: data, encoding: .utf8) else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var durations: [Int] = []
|
||||||
|
var byteRanges: [Range<Int>] = []
|
||||||
|
|
||||||
|
let extinfRegex = try! NSRegularExpression(pattern: "EXTINF:(\\d+)", options: [])
|
||||||
|
let byteRangeRegex = try! NSRegularExpression(pattern: "EXT-X-BYTERANGE:(\\d+)@(\\d+)", options: [])
|
||||||
|
|
||||||
|
let extinfResults = extinfRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString))
|
||||||
|
for result in extinfResults {
|
||||||
|
if let durationRange = Range(result.range(at: 1), in: playlistString) {
|
||||||
|
if let duration = Int(String(playlistString[durationRange])) {
|
||||||
|
durations.append(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let byteRangeResults = byteRangeRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString))
|
||||||
|
for result in byteRangeResults {
|
||||||
|
if let lengthRange = Range(result.range(at: 1), in: playlistString), let upperBoundRange = Range(result.range(at: 2), in: playlistString) {
|
||||||
|
if let length = Int(String(playlistString[lengthRange])), let lowerBound = Int(String(playlistString[upperBoundRange])) {
|
||||||
|
byteRanges.append(lowerBound ..< (lowerBound + length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if durations.count != byteRanges.count {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationOffset = 0
|
||||||
|
var initializationPart: Part?
|
||||||
|
var parts: [Part] = []
|
||||||
|
for i in 0 ..< durations.count {
|
||||||
|
let part = Part(timestamp: durationOffset, duration: durations[i], range: byteRanges[i])
|
||||||
|
if i == 0 {
|
||||||
|
initializationPart = Part(timestamp: 0, duration: 0, range: 0 ..< byteRanges[i].lowerBound)
|
||||||
|
}
|
||||||
|
parts.append(part)
|
||||||
|
durationOffset += durations[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if let initializationPart {
|
||||||
|
return .single(Playlist(dataFile: dataFile, initializationPart: initializationPart, parts: parts))
|
||||||
|
} else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchPlaylist: (FileMediaReference) -> Signal<Never, NoError> = { playlistFile in
|
||||||
|
return fetchedMediaResource(
|
||||||
|
mediaBox: self.postbox.mediaBox,
|
||||||
|
userLocation: self.userLocation,
|
||||||
|
userContentType: self.userContentType,
|
||||||
|
reference: playlistFile.resourceReference(playlistFile.media.resource)
|
||||||
|
)
|
||||||
|
|> ignoreValues
|
||||||
|
|> `catch` { _ -> Signal<Never, NoError> in
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchSignals: [Signal<Never, NoError>] = []
|
||||||
|
fetchSignals.append(fetchPlaylist(self.playlistFile))
|
||||||
|
for quality in self.alternativeQualities {
|
||||||
|
fetchSignals.append(fetchPlaylist(quality.playlist))
|
||||||
|
}
|
||||||
|
self.fetchPlaylistDisposable = combineLatest(fetchSignals).startStrict()
|
||||||
|
|
||||||
|
self.playlistDisposable = (combineLatest(queue: self.queue,
|
||||||
|
loadPlaylist(self.playlistFile, self.mainDataFile),
|
||||||
|
combineLatest(self.alternativeQualities.map {
|
||||||
|
return loadPlaylist($0.playlist, $0.dataFile)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|> deliverOn(self.queue)).startStrict(next: { [weak self] mainPlaylist, alternativePlaylists in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playlist = mainPlaylist
|
||||||
|
self.alternativePlaylists = alternativePlaylists.compactMap{ $0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateFrameRequest() {
|
||||||
|
guard let playlist = self.playlist else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.pendingFrame != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let nextRequestedFrame = self.nextRequestedFrame.swap(nil) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var allPlaylists: [Playlist] = [playlist]
|
||||||
|
allPlaylists.append(contentsOf: self.alternativePlaylists)
|
||||||
|
outer: for playlist in allPlaylists {
|
||||||
|
if let dataFileSize = playlist.dataFile.media.size, let part = playlist.parts.first(where: { $0.timestamp <= Int(nextRequestedFrame) && ($0.timestamp + $0.duration) > Int(nextRequestedFrame) }) {
|
||||||
|
let mappedRanges: [Range<Int64>] = [
|
||||||
|
Int64(playlist.initializationPart.range.lowerBound) ..< Int64(playlist.initializationPart.range.upperBound),
|
||||||
|
Int64(part.range.lowerBound) ..< Int64(part.range.upperBound)
|
||||||
|
]
|
||||||
|
for mappedRange in mappedRanges {
|
||||||
|
if !self.postbox.mediaBox.internal_resourceDataIsCached(id: playlist.dataFile.media.resource.id, size: dataFileSize, in: mappedRange) {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let directReader = FFMpegFileReader(
|
||||||
|
source: .resource(mediaBox: self.postbox.mediaBox, resource: playlist.dataFile.media.resource, resourceSize: dataFileSize, mappedRanges: mappedRanges),
|
||||||
|
useHardwareAcceleration: false,
|
||||||
|
selectedStream: .mediaType(.video),
|
||||||
|
seek: .direct(position: nextRequestedFrame),
|
||||||
|
maxReadablePts: nil
|
||||||
|
) {
|
||||||
|
var lastFrame: CMSampleBuffer?
|
||||||
|
findFrame: while true {
|
||||||
|
switch directReader.readFrame() {
|
||||||
|
case let .frame(frame):
|
||||||
|
if lastFrame == nil {
|
||||||
|
lastFrame = frame.sampleBuffer
|
||||||
|
} else if CMSampleBufferGetPresentationTimeStamp(frame.sampleBuffer).seconds > nextRequestedFrame {
|
||||||
|
break findFrame
|
||||||
|
} else {
|
||||||
|
lastFrame = frame.sampleBuffer
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break findFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let lastFrame {
|
||||||
|
if let imageBuffer = CMSampleBufferGetImageBuffer(lastFrame) {
|
||||||
|
var cgImage: CGImage?
|
||||||
|
VTCreateCGImageFromCVPixelBuffer(imageBuffer, options: nil, imageOut: &cgImage)
|
||||||
|
if let cgImage {
|
||||||
|
self.framePipe.putNext(.image(UIImage(cgImage: cgImage)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateFrameRequest()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let initializationPart = playlist.initializationPart
|
||||||
|
guard let part = playlist.parts.first(where: { $0.timestamp <= Int(nextRequestedFrame) && ($0.timestamp + $0.duration) > Int(nextRequestedFrame) }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let dataFileSize = self.mainDataFile.media.size else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let resource = self.mainDataFile.media.resource
|
||||||
|
let postbox = self.postbox
|
||||||
|
let userLocation = self.userLocation
|
||||||
|
let userContentType = self.userContentType
|
||||||
|
let dataFile = self.mainDataFile
|
||||||
|
|
||||||
|
let partRange: Range<Int64> = Int64(part.range.lowerBound) ..< Int64(part.range.upperBound)
|
||||||
|
|
||||||
|
let mappedRanges: [Range<Int64>] = [
|
||||||
|
Int64(initializationPart.range.lowerBound) ..< Int64(initializationPart.range.upperBound),
|
||||||
|
partRange
|
||||||
|
]
|
||||||
|
var mappedSize: Int64 = 0
|
||||||
|
for range in mappedRanges {
|
||||||
|
mappedSize += range.upperBound - range.lowerBound
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue = self.queue
|
||||||
|
let updateState: (FFMpegLookahead.State) -> Void = { [weak self] state in
|
||||||
|
queue.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.pendingFrame?.0 != part.timestamp {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let video = state.video else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let directReader = FFMpegFileReader(
|
||||||
|
source: .resource(mediaBox: postbox.mediaBox, resource: resource, resourceSize: dataFileSize, mappedRanges: mappedRanges),
|
||||||
|
useHardwareAcceleration: false,
|
||||||
|
selectedStream: .index(video.info.index),
|
||||||
|
seek: .stream(streamIndex: state.seek.streamIndex, pts: state.seek.pts),
|
||||||
|
maxReadablePts: (video.info.index, video.readableToTime.value, state.isEnded)
|
||||||
|
) {
|
||||||
|
switch directReader.readFrame() {
|
||||||
|
case let .frame(frame):
|
||||||
|
if let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer) {
|
||||||
|
var cgImage: CGImage?
|
||||||
|
VTCreateCGImageFromCVPixelBuffer(imageBuffer, options: nil, imageOut: &cgImage)
|
||||||
|
if let cgImage {
|
||||||
|
self.framePipe.putNext(.image(UIImage(cgImage: cgImage)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingFrame = nil
|
||||||
|
self.updateFrameRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lookahead = FFMpegLookahead(
|
||||||
|
seekToTimestamp: 0.0,
|
||||||
|
lookaheadDuration: 0.0,
|
||||||
|
updateState: updateState,
|
||||||
|
fetchInRange: { fetchRange in
|
||||||
|
let disposable = DisposableSet()
|
||||||
|
|
||||||
|
let readCount = fetchRange.upperBound - fetchRange.lowerBound
|
||||||
|
var readingPosition = fetchRange.lowerBound
|
||||||
|
|
||||||
|
var bufferOffset = 0
|
||||||
|
let doRead: (Range<Int64>) -> Void = { range in
|
||||||
|
disposable.add(fetchedMediaResource(
|
||||||
|
mediaBox: postbox.mediaBox,
|
||||||
|
userLocation: userLocation,
|
||||||
|
userContentType: userContentType,
|
||||||
|
reference: dataFile.resourceReference(dataFile.media.resource),
|
||||||
|
range: (range, .elevated),
|
||||||
|
statsCategory: .video,
|
||||||
|
preferBackgroundReferenceRevalidation: false
|
||||||
|
).startStrict())
|
||||||
|
let count = Int(range.upperBound - range.lowerBound)
|
||||||
|
bufferOffset += count
|
||||||
|
readingPosition += Int64(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedRangePosition: Int64 = 0
|
||||||
|
for mappedRange in mappedRanges {
|
||||||
|
let bytesToRead = readCount - Int64(bufferOffset)
|
||||||
|
if bytesToRead <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
|
||||||
|
let mappedRangeReadingPosition = readingPosition - mappedRangePosition
|
||||||
|
|
||||||
|
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
|
||||||
|
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
|
||||||
|
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
|
||||||
|
if mappedRangeBytesToRead > 0 {
|
||||||
|
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
|
||||||
|
doRead(mappedReadRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedRangePosition += mappedRangeSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return disposable
|
||||||
|
},
|
||||||
|
getDataInRange: { getRange, completion in
|
||||||
|
var signals: [Signal<(Data, Bool), NoError>] = []
|
||||||
|
|
||||||
|
let readCount = getRange.upperBound - getRange.lowerBound
|
||||||
|
var readingPosition = getRange.lowerBound
|
||||||
|
|
||||||
|
var bufferOffset = 0
|
||||||
|
let doRead: (Range<Int64>) -> Void = { range in
|
||||||
|
signals.append(postbox.mediaBox.resourceData(resource, size: dataFileSize, in: range, mode: .complete))
|
||||||
|
|
||||||
|
let readSize = Int(range.upperBound - range.lowerBound)
|
||||||
|
let effectiveReadSize = max(0, min(Int(readCount) - bufferOffset, readSize))
|
||||||
|
let count = effectiveReadSize
|
||||||
|
bufferOffset += count
|
||||||
|
readingPosition += Int64(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedRangePosition: Int64 = 0
|
||||||
|
for mappedRange in mappedRanges {
|
||||||
|
let bytesToRead = readCount - Int64(bufferOffset)
|
||||||
|
if bytesToRead <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
|
||||||
|
let mappedRangeReadingPosition = readingPosition - mappedRangePosition
|
||||||
|
|
||||||
|
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
|
||||||
|
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
|
||||||
|
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
|
||||||
|
if mappedRangeBytesToRead > 0 {
|
||||||
|
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
|
||||||
|
doRead(mappedReadRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedRangePosition += mappedRangeSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let singal = combineLatest(signals)
|
||||||
|
|> map { results -> Data? in
|
||||||
|
var result = Data()
|
||||||
|
for (partData, partIsComplete) in results {
|
||||||
|
if !partIsComplete {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result.append(partData)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return singal.start(next: { result in
|
||||||
|
completion(result)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isDataCachedInRange: { cachedRange in
|
||||||
|
let readCount = cachedRange.upperBound - cachedRange.lowerBound
|
||||||
|
var readingPosition = cachedRange.lowerBound
|
||||||
|
|
||||||
|
var allDataIsCached = true
|
||||||
|
|
||||||
|
var bufferOffset = 0
|
||||||
|
let doRead: (Range<Int64>) -> Void = { range in
|
||||||
|
let isCached = postbox.mediaBox.internal_resourceDataIsCached(
|
||||||
|
id: resource.id,
|
||||||
|
size: dataFileSize,
|
||||||
|
in: range
|
||||||
|
)
|
||||||
|
if !isCached {
|
||||||
|
allDataIsCached = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let effectiveReadSize = Int(range.upperBound - range.lowerBound)
|
||||||
|
let count = effectiveReadSize
|
||||||
|
bufferOffset += count
|
||||||
|
readingPosition += Int64(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedRangePosition: Int64 = 0
|
||||||
|
for mappedRange in mappedRanges {
|
||||||
|
let bytesToRead = readCount - Int64(bufferOffset)
|
||||||
|
if bytesToRead <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedRangeSize = mappedRange.upperBound - mappedRange.lowerBound
|
||||||
|
let mappedRangeReadingPosition = readingPosition - mappedRangePosition
|
||||||
|
|
||||||
|
if mappedRangeReadingPosition >= 0 && mappedRangeReadingPosition < mappedRangeSize {
|
||||||
|
let mappedRangeAvailableBytesToRead = mappedRangeSize - mappedRangeReadingPosition
|
||||||
|
let mappedRangeBytesToRead = min(bytesToRead, mappedRangeAvailableBytesToRead)
|
||||||
|
if mappedRangeBytesToRead > 0 {
|
||||||
|
let mappedReadRange = (mappedRange.lowerBound + mappedRangeReadingPosition) ..< (mappedRange.lowerBound + mappedRangeReadingPosition + mappedRangeBytesToRead)
|
||||||
|
doRead(mappedReadRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedRangePosition += mappedRangeSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDataIsCached
|
||||||
|
},
|
||||||
|
size: mappedSize
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pendingFrame = (part.timestamp, lookahead)
|
||||||
|
|
||||||
|
lookahead.updateCurrentTimestamp(timestamp: 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue: Queue
|
||||||
|
private let impl: QueueLocalObject<Impl>
|
||||||
|
|
||||||
|
public var generatedFrames: Signal<FramePreviewResult, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.impl.with { impl in
|
||||||
|
disposable.set(impl.framePipe.signal().start(next: { result in
|
||||||
|
subscriber.putNext(result)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let nextRequestedFrame = Atomic<Double?>(value: nil)
|
||||||
|
|
||||||
|
public init(
|
||||||
|
postbox: Postbox,
|
||||||
|
userLocation: MediaResourceUserLocation,
|
||||||
|
userContentType: MediaResourceUserContentType,
|
||||||
|
playlistFile: FileMediaReference,
|
||||||
|
mainDataFile: FileMediaReference,
|
||||||
|
alternativeQualities: [(playlist: FileMediaReference, dataFile: FileMediaReference)]
|
||||||
|
) {
|
||||||
|
let queue = Queue()
|
||||||
|
self.queue = queue
|
||||||
|
let nextRequestedFrame = self.nextRequestedFrame
|
||||||
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||||
|
return Impl(
|
||||||
|
queue: queue,
|
||||||
|
postbox: postbox,
|
||||||
|
userLocation: userLocation,
|
||||||
|
userContentType: userContentType,
|
||||||
|
playlistFile: playlistFile,
|
||||||
|
mainDataFile: mainDataFile,
|
||||||
|
alternativeQualities: alternativeQualities,
|
||||||
|
nextRequestedFrame: nextRequestedFrame
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func generateFrame(at timestamp: Double) {
|
||||||
|
let _ = self.nextRequestedFrame.swap(timestamp)
|
||||||
|
self.impl.with { impl in
|
||||||
|
impl.generateFrame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func cancelPendingFrames() {
|
||||||
|
self.impl.with { impl in
|
||||||
|
impl.cancelPendingFrames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -918,8 +918,12 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
self.audioSessionShouldBeActive.set(false)
|
self.audioSessionShouldBeActive.set(false)
|
||||||
if wasActive {
|
if wasActive {
|
||||||
let debugLogValue = Promise<String?>()
|
let debugLogValue = Promise<String?>()
|
||||||
|
if let conferenceCall = self.conferenceCall {
|
||||||
|
debugLogValue.set(conferenceCall.debugLog.get())
|
||||||
|
let _ = conferenceCall.leave(terminateIfPossible: false).start()
|
||||||
|
} else {
|
||||||
self.ongoingContext?.stop(debugLogValue: debugLogValue)
|
self.ongoingContext?.stop(debugLogValue: debugLogValue)
|
||||||
let _ = self.conferenceCall?.leave(terminateIfPossible: false).start()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var terminating = false
|
var terminating = false
|
||||||
@ -1198,8 +1202,12 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
public func hangUp() -> Signal<Bool, NoError> {
|
public func hangUp() -> Signal<Bool, NoError> {
|
||||||
let debugLogValue = Promise<String?>()
|
let debugLogValue = Promise<String?>()
|
||||||
self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get())
|
self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get())
|
||||||
|
if let conferenceCall = self.conferenceCall {
|
||||||
|
debugLogValue.set(conferenceCall.debugLog.get())
|
||||||
|
let _ = conferenceCall.leave(terminateIfPossible: false).start()
|
||||||
|
} else {
|
||||||
self.ongoingContext?.stop(debugLogValue: debugLogValue)
|
self.ongoingContext?.stop(debugLogValue: debugLogValue)
|
||||||
let _ = self.conferenceCall?.leave(terminateIfPossible: false).start()
|
}
|
||||||
|
|
||||||
return self.hungUpPromise.get()
|
return self.hungUpPromise.get()
|
||||||
}
|
}
|
||||||
@ -1207,8 +1215,12 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
public func rejectBusy() {
|
public func rejectBusy() {
|
||||||
self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil))
|
self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil))
|
||||||
let debugLog = Promise<String?>()
|
let debugLog = Promise<String?>()
|
||||||
|
if let conferenceCall = self.conferenceCall {
|
||||||
|
debugLog.set(conferenceCall.debugLog.get())
|
||||||
|
let _ = conferenceCall.leave(terminateIfPossible: false).start()
|
||||||
|
} else {
|
||||||
self.ongoingContext?.stop(debugLogValue: debugLog)
|
self.ongoingContext?.stop(debugLogValue: debugLog)
|
||||||
let _ = self.conferenceCall?.leave(terminateIfPossible: false).start()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func toggleIsMuted() {
|
public func toggleIsMuted() {
|
||||||
@ -1262,7 +1274,11 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
guard let screencastCapturer = screencastCapturer else {
|
guard let screencastCapturer = screencastCapturer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
screencastCapturer.injectPixelBuffer(screencastFrame.0, rotation: screencastFrame.1)
|
guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {})
|
||||||
}))
|
}))
|
||||||
self.screencastAudioDataDisposable.set((screencastBufferServerContext.audioData
|
self.screencastAudioDataDisposable.set((screencastBufferServerContext.audioData
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||||
@ -1467,3 +1483,36 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
self.videoCapturer?.switchVideoInput(isFront: self.useFrontCamera)
|
self.videoCapturer?.switchVideoInput(isFront: self.useFrontCamera)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
|
||||||
|
var maybeFormat: CMVideoFormatDescription?
|
||||||
|
let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat)
|
||||||
|
if status != noErr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let format = maybeFormat else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var timingInfo = CMSampleTimingInfo(
|
||||||
|
duration: CMTimeMake(value: 1, timescale: 30),
|
||||||
|
presentationTimeStamp: CMTimeMake(value: 0, timescale: 30),
|
||||||
|
decodeTimeStamp: CMTimeMake(value: 0, timescale: 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
var maybeSampleBuffer: CMSampleBuffer?
|
||||||
|
let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer)
|
||||||
|
|
||||||
|
if (bufferStatus != noErr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let sampleBuffer = maybeSampleBuffer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray
|
||||||
|
let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary
|
||||||
|
dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber
|
||||||
|
|
||||||
|
return sampleBuffer
|
||||||
|
}
|
||||||
|
|||||||
@ -321,12 +321,12 @@ private extension CurrentImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop(account: Account, reportCallId: CallId?) {
|
func stop(account: Account, reportCallId: CallId?, debugLog: Promise<String?>) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .call(callContext):
|
case let .call(callContext):
|
||||||
callContext.stop(account: account, reportCallId: reportCallId)
|
callContext.stop(account: account, reportCallId: reportCallId, debugLog: debugLog)
|
||||||
case .mediaStream, .externalMediaStream:
|
case .mediaStream, .externalMediaStream:
|
||||||
break
|
debugLog.set(.single(nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,6 +466,138 @@ public func allocateCallLogPath(account: Account) -> String {
|
|||||||
return "\(path)/\(name).log"
|
return "\(path)/\(name).log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private protocol ScreencastIPCContext: AnyObject {
|
||||||
|
var isActive: Signal<Bool, NoError> { get }
|
||||||
|
|
||||||
|
func requestScreencast() -> Signal<(String, UInt32), NoError>?
|
||||||
|
func setJoinResponse(clientParams: String)
|
||||||
|
func disableScreencast(account: Account)
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ScreencastInProcessIPCContext: ScreencastIPCContext {
|
||||||
|
private let isConference: Bool
|
||||||
|
|
||||||
|
private let screencastBufferServerContext: IpcGroupCallBufferAppContext
|
||||||
|
private var screencastCallContext: ScreencastContext?
|
||||||
|
private let screencastCapturer: OngoingCallVideoCapturer
|
||||||
|
private var screencastFramesDisposable: Disposable?
|
||||||
|
private var screencastAudioDataDisposable: Disposable?
|
||||||
|
|
||||||
|
var isActive: Signal<Bool, NoError> {
|
||||||
|
return self.screencastBufferServerContext.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
init(basePath: String, isConference: Bool) {
|
||||||
|
self.isConference = isConference
|
||||||
|
|
||||||
|
let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath + "/broadcast-coordination")
|
||||||
|
self.screencastBufferServerContext = screencastBufferServerContext
|
||||||
|
let screencastCapturer = OngoingCallVideoCapturer(isCustom: true)
|
||||||
|
self.screencastCapturer = screencastCapturer
|
||||||
|
self.screencastFramesDisposable = (screencastBufferServerContext.frames
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in
|
||||||
|
guard let screencastCapturer = screencastCapturer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {})
|
||||||
|
})
|
||||||
|
self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.screencastCallContext?.addExternalAudioData(data: data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.screencastFramesDisposable?.dispose()
|
||||||
|
self.screencastAudioDataDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestScreencast() -> Signal<(String, UInt32), NoError>? {
|
||||||
|
if self.screencastCallContext == nil {
|
||||||
|
let screencastCallContext = InProcessScreencastContext(
|
||||||
|
context: OngoingGroupCallContext(
|
||||||
|
audioSessionActive: .single(true),
|
||||||
|
video: self.screencastCapturer,
|
||||||
|
requestMediaChannelDescriptions: { _, _ in EmptyDisposable },
|
||||||
|
rejoinNeeded: { },
|
||||||
|
outgoingAudioBitrateKbit: nil,
|
||||||
|
videoContentType: .screencast,
|
||||||
|
enableNoiseSuppression: false,
|
||||||
|
disableAudioInput: true,
|
||||||
|
enableSystemMute: false,
|
||||||
|
preferX264: false,
|
||||||
|
logPath: "",
|
||||||
|
onMutedSpeechActivityDetected: { _ in },
|
||||||
|
encryptionKey: nil,
|
||||||
|
isConference: self.isConference,
|
||||||
|
sharedAudioDevice: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.screencastCallContext = screencastCallContext
|
||||||
|
return screencastCallContext.joinPayload
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setJoinResponse(clientParams: String) {
|
||||||
|
if let screencastCallContext = self.screencastCallContext {
|
||||||
|
screencastCallContext.setRTCJoinResponse(clientParams: clientParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableScreencast(account: Account) {
|
||||||
|
if let screencastCallContext = self.screencastCallContext {
|
||||||
|
self.screencastCallContext = nil
|
||||||
|
screencastCallContext.stop(account: account, reportCallId: nil)
|
||||||
|
|
||||||
|
self.screencastBufferServerContext.stopScreencast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ScreencastEmbeddedIPCContext: ScreencastIPCContext {
|
||||||
|
private let serverContext: IpcGroupCallEmbeddedAppContext
|
||||||
|
|
||||||
|
var isActive: Signal<Bool, NoError> {
|
||||||
|
return self.serverContext.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
init(basePath: String) {
|
||||||
|
self.serverContext = IpcGroupCallEmbeddedAppContext(basePath: basePath + "/embedded-broadcast-coordination")
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestScreencast() -> Signal<(String, UInt32), NoError>? {
|
||||||
|
if let id = self.serverContext.startScreencast() {
|
||||||
|
return self.serverContext.joinPayload
|
||||||
|
|> filter { joinPayload -> Bool in
|
||||||
|
return joinPayload.id == id
|
||||||
|
}
|
||||||
|
|> map { joinPayload -> (String, UInt32) in
|
||||||
|
return (joinPayload.data, joinPayload.ssrc)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setJoinResponse(clientParams: String) {
|
||||||
|
self.serverContext.joinResponse = IpcGroupCallEmbeddedAppContext.JoinResponse(data: clientParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableScreencast(account: Account) {
|
||||||
|
self.serverContext.stopScreencast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class PresentationGroupCallImpl: PresentationGroupCall {
|
public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||||
private enum InternalState {
|
private enum InternalState {
|
||||||
case requesting
|
case requesting
|
||||||
@ -629,9 +761,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
let externalMediaStream = Promise<DirectMediaStreamingContext>()
|
let externalMediaStream = Promise<DirectMediaStreamingContext>()
|
||||||
|
|
||||||
private var screencastCallContext: OngoingGroupCallContext?
|
private var screencastIPCContext: ScreencastIPCContext?
|
||||||
private var screencastBufferServerContext: IpcGroupCallBufferAppContext?
|
|
||||||
private var screencastCapturer: OngoingCallVideoCapturer?
|
|
||||||
|
|
||||||
private struct SsrcMapping {
|
private struct SsrcMapping {
|
||||||
var peerId: EnginePeer.Id
|
var peerId: EnginePeer.Id
|
||||||
@ -860,8 +990,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return self.isSpeakingPromise.get()
|
return self.isSpeakingPromise.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var screencastFramesDisposable: Disposable?
|
|
||||||
private var screencastAudioDataDisposable: Disposable?
|
|
||||||
private var screencastStateDisposable: Disposable?
|
private var screencastStateDisposable: Disposable?
|
||||||
|
|
||||||
public let isStream: Bool
|
public let isStream: Bool
|
||||||
@ -876,6 +1004,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
public var onMutedSpeechActivityDetected: ((Bool) -> Void)?
|
public var onMutedSpeechActivityDetected: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
let debugLog = Promise<String?>()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
accountContext: AccountContext,
|
accountContext: AccountContext,
|
||||||
audioSession: ManagedAudioSession,
|
audioSession: ManagedAudioSession,
|
||||||
@ -1149,26 +1279,24 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.requestCall(movingFromBroadcastToRtc: false)
|
self.requestCall(movingFromBroadcastToRtc: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
let basePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination"
|
var useIPCContext = "".isEmpty
|
||||||
let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath)
|
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_use_inprocess_screencast"] != nil {
|
||||||
self.screencastBufferServerContext = screencastBufferServerContext
|
useIPCContext = false
|
||||||
let screencastCapturer = OngoingCallVideoCapturer(isCustom: true)
|
|
||||||
self.screencastCapturer = screencastCapturer
|
|
||||||
self.screencastFramesDisposable = (screencastBufferServerContext.frames
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in
|
|
||||||
guard let screencastCapturer = screencastCapturer else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
screencastCapturer.injectPixelBuffer(screencastFrame.0, rotation: screencastFrame.1)
|
|
||||||
})
|
let embeddedBroadcastImplementationTypePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination-type"
|
||||||
self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
let screencastIPCContext: ScreencastIPCContext
|
||||||
guard let strongSelf = self else {
|
if useIPCContext {
|
||||||
return
|
screencastIPCContext = ScreencastEmbeddedIPCContext(basePath: self.accountContext.sharedContext.basePath)
|
||||||
|
let _ = try? "ipc".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
||||||
|
} else {
|
||||||
|
screencastIPCContext = ScreencastInProcessIPCContext(basePath: self.accountContext.sharedContext.basePath, isConference: self.isConference)
|
||||||
|
let _ = try? "legacy".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
strongSelf.screencastCallContext?.addExternalAudioData(data: data)
|
self.screencastIPCContext = screencastIPCContext
|
||||||
})
|
|
||||||
self.screencastStateDisposable = (screencastBufferServerContext.isActive
|
self.screencastStateDisposable = (screencastIPCContext.isActive
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] isActive in
|
|> deliverOnMainQueue).start(next: { [weak self] isActive in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1228,8 +1356,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
self.peerUpdatesSubscription?.dispose()
|
self.peerUpdatesSubscription?.dispose()
|
||||||
|
|
||||||
self.screencastFramesDisposable?.dispose()
|
|
||||||
self.screencastAudioDataDisposable?.dispose()
|
|
||||||
self.screencastStateDisposable?.dispose()
|
self.screencastStateDisposable?.dispose()
|
||||||
|
|
||||||
self.internal_isRemoteConnectedDisposable?.dispose()
|
self.internal_isRemoteConnectedDisposable?.dispose()
|
||||||
@ -2658,10 +2784,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
self.markedAsCanBeRemoved = true
|
self.markedAsCanBeRemoved = true
|
||||||
|
|
||||||
self.genericCallContext?.stop(account: self.account, reportCallId: self.conferenceFromCallId)
|
self.genericCallContext?.stop(account: self.account, reportCallId: self.conferenceFromCallId, debugLog: self.debugLog)
|
||||||
|
self.screencastIPCContext?.disableScreencast(account: self.account)
|
||||||
//self.screencastIpcContext = nil
|
|
||||||
self.screencastCallContext?.stop(account: self.account, reportCallId: nil)
|
|
||||||
|
|
||||||
self._canBeRemoved.set(.single(true))
|
self._canBeRemoved.set(.single(true))
|
||||||
|
|
||||||
@ -3106,16 +3230,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func requestScreencast() {
|
private func requestScreencast() {
|
||||||
guard let callInfo = self.internalState.callInfo, self.screencastCallContext == nil else {
|
guard let callInfo = self.internalState.callInfo else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hasScreencast = true
|
self.hasScreencast = true
|
||||||
|
if let screencastIPCContext = self.screencastIPCContext, let joinPayload = screencastIPCContext.requestScreencast() {
|
||||||
let screencastCallContext = OngoingGroupCallContext(audioSessionActive: .single(true), video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, enableSystemMute: false, preferX264: false, logPath: "", onMutedSpeechActivityDetected: { _ in }, encryptionKey: nil, isConference: self.isConference, sharedAudioDevice: nil)
|
self.screencastJoinDisposable.set((joinPayload
|
||||||
self.screencastCallContext = screencastCallContext
|
|
||||||
|
|
||||||
self.screencastJoinDisposable.set((screencastCallContext.joinPayload
|
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload in
|
|> deliverOnMainQueue).start(next: { [weak self] joinPayload in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -3128,13 +3249,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
joinPayload: joinPayload.0
|
joinPayload: joinPayload.0
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||||
guard let strongSelf = self, let screencastCallContext = strongSelf.screencastCallContext else {
|
guard let strongSelf = self, let screencastIPCContext = strongSelf.screencastIPCContext else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let clientParams = joinCallResult.jsonParams
|
screencastIPCContext.setJoinResponse(clientParams: joinCallResult.jsonParams)
|
||||||
|
|
||||||
screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
|
|
||||||
screencastCallContext.setJoinResponse(payload: clientParams)
|
|
||||||
}, error: { error in
|
}, error: { error in
|
||||||
guard let _ = self else {
|
guard let _ = self else {
|
||||||
return
|
return
|
||||||
@ -3142,12 +3261,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func disableScreencast() {
|
public func disableScreencast() {
|
||||||
self.hasScreencast = false
|
self.hasScreencast = false
|
||||||
if let screencastCallContext = self.screencastCallContext {
|
self.screencastIPCContext?.disableScreencast(account: self.account)
|
||||||
self.screencastCallContext = nil
|
|
||||||
screencastCallContext.stop(account: self.account, reportCallId: nil)
|
|
||||||
|
|
||||||
let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo
|
let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo
|
||||||
|
|
||||||
@ -3157,9 +3275,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
accessHash: callInfo.accessHash
|
accessHash: callInfo.accessHash
|
||||||
).start())
|
).start())
|
||||||
}
|
}
|
||||||
|
|
||||||
self.screencastBufferServerContext?.stopScreencast()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) {
|
public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) {
|
||||||
@ -3608,3 +3723,34 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|> runOn(.mainQueue())
|
|> runOn(.mainQueue())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private protocol ScreencastContext: AnyObject {
|
||||||
|
func addExternalAudioData(data: Data)
|
||||||
|
func stop(account: Account, reportCallId: CallId?)
|
||||||
|
func setRTCJoinResponse(clientParams: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class InProcessScreencastContext: ScreencastContext {
|
||||||
|
private let context: OngoingGroupCallContext
|
||||||
|
|
||||||
|
var joinPayload: Signal<(String, UInt32), NoError> {
|
||||||
|
return self.context.joinPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
init(context: OngoingGroupCallContext) {
|
||||||
|
self.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExternalAudioData(data: Data) {
|
||||||
|
self.context.addExternalAudioData(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop(account: Account, reportCallId: CallId?) {
|
||||||
|
self.context.stop(account: account, reportCallId: reportCallId, debugLog: Promise())
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRTCJoinResponse(clientParams: String) {
|
||||||
|
self.context.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
|
||||||
|
self.context.setJoinResponse(payload: clientParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,39 +8,6 @@ import TelegramVoip
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import LibYuvBinding
|
import LibYuvBinding
|
||||||
|
|
||||||
private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
|
|
||||||
var maybeFormat: CMVideoFormatDescription?
|
|
||||||
let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat)
|
|
||||||
if status != noErr {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let format = maybeFormat else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var timingInfo = CMSampleTimingInfo(
|
|
||||||
duration: CMTimeMake(value: 1, timescale: 30),
|
|
||||||
presentationTimeStamp: CMTimeMake(value: 0, timescale: 30),
|
|
||||||
decodeTimeStamp: CMTimeMake(value: 0, timescale: 30)
|
|
||||||
)
|
|
||||||
|
|
||||||
var maybeSampleBuffer: CMSampleBuffer?
|
|
||||||
let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer)
|
|
||||||
|
|
||||||
if (bufferStatus != noErr) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let sampleBuffer = maybeSampleBuffer else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray
|
|
||||||
let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary
|
|
||||||
dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber
|
|
||||||
|
|
||||||
return sampleBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool {
|
private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool {
|
||||||
guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else {
|
guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "AvatarUploadToastScreen",
|
||||||
|
module_name = "AvatarUploadToastScreen",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/Postbox",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
|
"//submodules/Components/ViewControllerComponent",
|
||||||
|
"//submodules/Components/MultilineTextComponent",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/RadialStatusNode",
|
||||||
|
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||||
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -0,0 +1,466 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ComponentFlow
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import AppBundle
|
||||||
|
import ViewControllerComponent
|
||||||
|
import AccountContext
|
||||||
|
import MultilineTextComponent
|
||||||
|
import RadialStatusNode
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AnimatedTextComponent
|
||||||
|
import PlainButtonComponent
|
||||||
|
|
||||||
|
private final class AvatarUploadToastScreenComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let image: UIImage
|
||||||
|
let uploadStatus: Signal<PeerInfoAvatarUploadStatus, NoError>
|
||||||
|
let arrowTarget: () -> (UIView, CGRect)?
|
||||||
|
let viewUploadedAvatar: () -> Void
|
||||||
|
|
||||||
|
init(context: AccountContext, image: UIImage, uploadStatus: Signal<PeerInfoAvatarUploadStatus, NoError>, arrowTarget: @escaping () -> (UIView, CGRect)?, viewUploadedAvatar: @escaping () -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.image = image
|
||||||
|
self.uploadStatus = uploadStatus
|
||||||
|
self.arrowTarget = arrowTarget
|
||||||
|
self.viewUploadedAvatar = viewUploadedAvatar
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: AvatarUploadToastScreenComponent, rhs: AvatarUploadToastScreenComponent) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let contentView: UIView
|
||||||
|
private let backgroundView: BlurredBackgroundView
|
||||||
|
|
||||||
|
private let backgroundMaskView: UIView
|
||||||
|
private let backgroundMainMaskView: UIView
|
||||||
|
private let backgroundArrowMaskView: UIImageView
|
||||||
|
|
||||||
|
private let avatarView: UIImageView
|
||||||
|
private let progressNode: RadialStatusNode
|
||||||
|
private let content = ComponentView<Empty>()
|
||||||
|
private let actionButton = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var isUpdating: Bool = false
|
||||||
|
private var component: AvatarUploadToastScreenComponent?
|
||||||
|
private var environment: EnvironmentType?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
private var status: PeerInfoAvatarUploadStatus = .progress(0.0)
|
||||||
|
private var statusDisposable: Disposable?
|
||||||
|
|
||||||
|
private var doneTimer: Foundation.Timer?
|
||||||
|
private var currentIsDone: Bool = false
|
||||||
|
|
||||||
|
private var isDisplaying: Bool = false
|
||||||
|
|
||||||
|
var targetAvatarView: UIView? {
|
||||||
|
return self.avatarView
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.contentView = UIView()
|
||||||
|
|
||||||
|
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||||
|
|
||||||
|
self.backgroundMaskView = UIView()
|
||||||
|
|
||||||
|
self.backgroundMainMaskView = UIView()
|
||||||
|
self.backgroundMainMaskView.backgroundColor = .white
|
||||||
|
|
||||||
|
self.backgroundArrowMaskView = UIImageView()
|
||||||
|
|
||||||
|
self.avatarView = UIImageView()
|
||||||
|
self.progressNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.backgroundView.mask = self.backgroundMaskView
|
||||||
|
self.backgroundMaskView.addSubview(self.backgroundMainMaskView)
|
||||||
|
self.backgroundMaskView.addSubview(self.backgroundArrowMaskView)
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
|
||||||
|
self.addSubview(self.contentView)
|
||||||
|
self.contentView.addSubview(self.avatarView)
|
||||||
|
self.contentView.addSubview(self.progressNode.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.statusDisposable?.dispose()
|
||||||
|
self.doneTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
if !self.contentView.frame.contains(point) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
func generateParabollicMotionKeyframes(from sourcePoint: CGFloat, elevation: CGFloat) -> [CGFloat] {
|
||||||
|
let midPoint = sourcePoint - elevation
|
||||||
|
|
||||||
|
let y1 = sourcePoint
|
||||||
|
let y2 = midPoint
|
||||||
|
let y3 = sourcePoint
|
||||||
|
|
||||||
|
let x1 = 0.0
|
||||||
|
let x2 = 100.0
|
||||||
|
let x3 = 200.0
|
||||||
|
|
||||||
|
var keyframes: [CGFloat] = []
|
||||||
|
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
|
||||||
|
for i in 0 ..< 10 {
|
||||||
|
let k = listViewAnimationCurveSystem(CGFloat(i) / CGFloat(10 - 1))
|
||||||
|
let x = x3 * k
|
||||||
|
let y = a * x * x + b * x + c
|
||||||
|
|
||||||
|
keyframes.append(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyframes
|
||||||
|
}
|
||||||
|
let offsetValues = generateParabollicMotionKeyframes(from: 0.0, elevation: -10.0)
|
||||||
|
self.layer.animateKeyframes(values: offsetValues.map { $0 as NSNumber }, duration: 0.5, keyPath: "position.y", additive: true)
|
||||||
|
|
||||||
|
self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.isDisplaying = true
|
||||||
|
if !self.isUpdating {
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
|
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||||
|
})
|
||||||
|
self.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: AvatarUploadToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.isUpdating = true
|
||||||
|
defer {
|
||||||
|
self.isUpdating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||||
|
|
||||||
|
if self.component == nil {
|
||||||
|
self.statusDisposable = (component.uploadStatus
|
||||||
|
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.status = status
|
||||||
|
if !self.isUpdating {
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .done = status, self.doneTimer == nil {
|
||||||
|
self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 4.0, repeats: false, block: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.environment?.controller()?.dismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.environment = environment
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
var isDone = false
|
||||||
|
let effectiveProgress: CGFloat
|
||||||
|
switch self.status {
|
||||||
|
case let .progress(value):
|
||||||
|
effectiveProgress = CGFloat(value)
|
||||||
|
case .done:
|
||||||
|
isDone = true
|
||||||
|
effectiveProgress = 1.0
|
||||||
|
}
|
||||||
|
let previousIsDone = self.currentIsDone
|
||||||
|
self.currentIsDone = isDone
|
||||||
|
|
||||||
|
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
|
||||||
|
|
||||||
|
let tabBarHeight: CGFloat
|
||||||
|
if !environment.safeInsets.left.isZero {
|
||||||
|
tabBarHeight = 34.0 + environment.safeInsets.bottom
|
||||||
|
} else {
|
||||||
|
tabBarHeight = 49.0 + environment.safeInsets.bottom
|
||||||
|
}
|
||||||
|
let containerInsets = UIEdgeInsets(
|
||||||
|
top: environment.safeInsets.top,
|
||||||
|
left: environment.safeInsets.left + 12.0,
|
||||||
|
bottom: tabBarHeight + 3.0,
|
||||||
|
right: environment.safeInsets.right + 12.0
|
||||||
|
)
|
||||||
|
|
||||||
|
let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom)
|
||||||
|
|
||||||
|
let spacing: CGFloat = 12.0
|
||||||
|
|
||||||
|
let iconSize = CGSize(width: 30.0, height: 30.0)
|
||||||
|
let iconProgressInset: CGFloat = 3.0
|
||||||
|
|
||||||
|
var textItems: [AnimatedTextComponent.Item] = []
|
||||||
|
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: true, content: .text("Your photo is ")))
|
||||||
|
if isDone {
|
||||||
|
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text("now set.")))
|
||||||
|
} else {
|
||||||
|
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text("uploading.")))
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionButtonSize = self.actionButton.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(PlainButtonComponent(
|
||||||
|
content: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: "View", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)))
|
||||||
|
)),
|
||||||
|
effectAlignment: .center,
|
||||||
|
contentInsets: UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0),
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.doneTimer?.invalidate()
|
||||||
|
self.environment?.controller()?.dismiss()
|
||||||
|
component.viewUploadedAvatar()
|
||||||
|
},
|
||||||
|
animateAlpha: true,
|
||||||
|
animateScale: false,
|
||||||
|
animateContents: false
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width, height: availableContentSize.height)
|
||||||
|
)
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let contentSize = self.content.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(AnimatedTextComponent(
|
||||||
|
font: Font.regular(14.0),
|
||||||
|
color: .white,
|
||||||
|
items: textItems
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - actionButtonSize.width - 16.0 - 4.0, height: availableContentSize.height)
|
||||||
|
)
|
||||||
|
|
||||||
|
var contentHeight: CGFloat = 0.0
|
||||||
|
contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height)
|
||||||
|
|
||||||
|
if self.avatarView.image == nil {
|
||||||
|
self.avatarView.image = generateImage(iconSize, rotatedContext: { size, context in
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
defer {
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
context.addEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.clip()
|
||||||
|
|
||||||
|
component.image.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
|
||||||
|
|
||||||
|
var adjustedAvatarFrame = avatarFrame
|
||||||
|
if !isDone {
|
||||||
|
adjustedAvatarFrame = adjustedAvatarFrame.insetBy(dx: iconProgressInset, dy: iconProgressInset)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: self.avatarView, position: adjustedAvatarFrame.center)
|
||||||
|
transition.setBounds(view: self.avatarView, bounds: CGRect(origin: CGPoint(), size: adjustedAvatarFrame.size))
|
||||||
|
if isDone && !previousIsDone {
|
||||||
|
let topScale: CGFloat = 1.1
|
||||||
|
self.avatarView.layer.animateScale(from: 1.0, to: topScale, duration: 0.16, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.avatarView.layer.animateScale(from: topScale, to: 1.0, duration: 0.16)
|
||||||
|
})
|
||||||
|
self.progressNode.layer.animateScale(from: 1.0, to: topScale, duration: 0.16, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.progressNode.layer.animateScale(from: topScale, to: 1.0, duration: 0.16)
|
||||||
|
})
|
||||||
|
HapticFeedback().success()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progressNode.frame = avatarFrame
|
||||||
|
self.progressNode.transitionToState(.progress(color: .white, lineWidth: 1.0 + UIScreenPixel, value: effectiveProgress, cancelEnabled: false, animateRotation: true))
|
||||||
|
transition.setAlpha(view: self.progressNode.view, alpha: isDone ? 0.0 : 1.0)
|
||||||
|
|
||||||
|
if let contentView = self.content.view {
|
||||||
|
if contentView.superview == nil {
|
||||||
|
self.contentView.addSubview(contentView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let actionButtonView = self.actionButton.view {
|
||||||
|
if actionButtonView.superview == nil {
|
||||||
|
self.contentView.addSubview(actionButtonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: availableContentSize.width - contentInsets.right - 16.0 - actionButtonSize.width, y: floor((contentHeight - actionButtonSize.height) * 0.5)), size: actionButtonSize))
|
||||||
|
transition.setAlpha(view: actionButtonView, alpha: isDone ? 1.0 : 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = CGSize(width: availableContentSize.width, height: contentHeight)
|
||||||
|
|
||||||
|
let contentFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size)
|
||||||
|
|
||||||
|
self.backgroundView.updateColor(color: self.isDisplaying ? UIColor(white: 0.0, alpha: 0.7) : UIColor.black, transition: transition.containedViewLayoutTransition)
|
||||||
|
let backgroundFrame: CGRect
|
||||||
|
if self.isDisplaying {
|
||||||
|
backgroundFrame = contentFrame
|
||||||
|
} else {
|
||||||
|
backgroundFrame = CGRect(origin: CGPoint(), size: availableSize)
|
||||||
|
}
|
||||||
|
if self.backgroundView.bounds.size != contentFrame.size {
|
||||||
|
self.backgroundView.update(size: availableSize, cornerRadius: 0.0, transition: transition.containedViewLayoutTransition)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
|
transition.setCornerRadius(layer: self.backgroundMainMaskView.layer, cornerRadius: self.isDisplaying ? 14.0 : 0.0)
|
||||||
|
transition.setFrame(view: self.backgroundMainMaskView, frame: backgroundFrame)
|
||||||
|
|
||||||
|
if self.backgroundArrowMaskView.image == nil {
|
||||||
|
let arrowFactor: CGFloat = 0.75
|
||||||
|
let arrowSize = CGSize(width: floor(29.0 * arrowFactor), height: floor(10.0 * arrowFactor))
|
||||||
|
self.backgroundArrowMaskView.image = generateImage(arrowSize, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.scaleBy(x: size.width / 29.0, y: size.height / 10.0)
|
||||||
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
|
context.scaleBy(x: 0.333, y: 0.333)
|
||||||
|
let _ = try? drawSvgPath(context, path: "M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ")
|
||||||
|
context.fillPath()
|
||||||
|
})?.withRenderingMode(.alwaysTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let arrowImage = self.backgroundArrowMaskView.image, let (targetView, targetRect) = component.arrowTarget() {
|
||||||
|
let targetArrowRect = targetView.convert(targetRect, to: self)
|
||||||
|
self.backgroundArrowMaskView.isHidden = false
|
||||||
|
|
||||||
|
var arrowFrame = CGRect(origin: CGPoint(x: targetArrowRect.minX + floor((targetArrowRect.width - arrowImage.size.width) * 0.5), y: contentFrame.maxY), size: arrowImage.size)
|
||||||
|
if !self.isDisplaying {
|
||||||
|
arrowFrame = arrowFrame.offsetBy(dx: 0.0, dy: -10.0)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: self.backgroundArrowMaskView, frame: arrowFrame)
|
||||||
|
} else {
|
||||||
|
self.backgroundArrowMaskView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(view: self.contentView, frame: contentFrame)
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AvatarUploadToastScreen: ViewControllerComponentContainer {
|
||||||
|
public var targetAvatarView: UIView? {
|
||||||
|
if let view = self.node.hostView.componentView as? AvatarUploadToastScreenComponent.View {
|
||||||
|
return view.targetAvatarView
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var processedDidAppear: Bool = false
|
||||||
|
private var processedDidDisappear: Bool = false
|
||||||
|
|
||||||
|
public init(
|
||||||
|
context: AccountContext,
|
||||||
|
image: UIImage,
|
||||||
|
uploadStatus: Signal<PeerInfoAvatarUploadStatus, NoError>,
|
||||||
|
arrowTarget: @escaping () -> (UIView, CGRect)?,
|
||||||
|
viewUploadedAvatar: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
super.init(
|
||||||
|
context: context,
|
||||||
|
component: AvatarUploadToastScreenComponent(
|
||||||
|
context: context,
|
||||||
|
image: image,
|
||||||
|
uploadStatus: uploadStatus,
|
||||||
|
arrowTarget: arrowTarget,
|
||||||
|
viewUploadedAvatar: viewUploadedAvatar
|
||||||
|
),
|
||||||
|
navigationBarAppearance: .none,
|
||||||
|
statusBarStyle: .ignore,
|
||||||
|
presentationMode: .default,
|
||||||
|
updatedPresentationData: nil
|
||||||
|
)
|
||||||
|
self.navigationPresentation = .flatModal
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if !self.processedDidAppear {
|
||||||
|
self.processedDidAppear = true
|
||||||
|
if let componentView = self.node.hostView.componentView as? AvatarUploadToastScreenComponent.View {
|
||||||
|
componentView.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func superDismiss() {
|
||||||
|
super.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||||
|
if !self.processedDidDisappear {
|
||||||
|
self.processedDidDisappear = true
|
||||||
|
|
||||||
|
if let componentView = self.node.hostView.componentView as? AvatarUploadToastScreenComponent.View {
|
||||||
|
componentView.animateOut(completion: { [weak self] in
|
||||||
|
if let self {
|
||||||
|
self.superDismiss()
|
||||||
|
}
|
||||||
|
completion?()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
super.dismiss(completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1251,7 +1251,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||||||
let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||||
|> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in
|
|> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in
|
||||||
var canViewStarsRevenue = false
|
var canViewStarsRevenue = false
|
||||||
if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal {
|
if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests {
|
||||||
canViewStarsRevenue = true
|
canViewStarsRevenue = true
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -1276,7 +1276,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||||||
)
|
)
|
||||||
|> mapToSignal { peer, canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in
|
|> mapToSignal { peer, canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in
|
||||||
var canViewRevenue = canViewRevenue
|
var canViewRevenue = canViewRevenue
|
||||||
if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal {
|
if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests {
|
||||||
canViewRevenue = true
|
canViewRevenue = true
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@ -3947,38 +3947,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let entriesPromise = Promise<[AvatarGalleryEntry]>(entries)
|
strongSelf.openAvatarGallery(peer: EnginePeer(peer), entries: entries, centralEntry: centralEntry, animateTransition: true)
|
||||||
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: EnginePeer(peer), sourceCorners: .round, remoteEntries: entriesPromise, skipInitial: true, centralEntryIndex: centralEntry.flatMap { entries.firstIndex(of: $0) }, replaceRootController: { controller, ready in
|
|
||||||
})
|
|
||||||
galleryController.openAvatarSetup = { [weak self] completion in
|
|
||||||
self?.controller?.openAvatarForEditing(fromGallery: true, completion: { _ in
|
|
||||||
completion()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
galleryController.avatarPhotoEditCompletion = { [weak self] image in
|
|
||||||
self?.controller?.updateProfilePhoto(image, mode: .generic)
|
|
||||||
}
|
|
||||||
galleryController.avatarVideoEditCompletion = { [weak self] image, asset, adjustments in
|
|
||||||
self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic)
|
|
||||||
}
|
|
||||||
galleryController.removedEntry = { [weak self] entry in
|
|
||||||
if let item = PeerInfoAvatarListItem(entry: entry) {
|
|
||||||
let _ = self?.headerNode.avatarListNode.listContainerNode.deleteItem(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).startStrict(next: { entry in
|
|
||||||
self?.headerNode.updateAvatarIsHidden(entry: entry)
|
|
||||||
}))
|
|
||||||
strongSelf.view.endEditing(true)
|
|
||||||
strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in
|
|
||||||
if let transitionNode = self?.headerNode.avatarTransitionArguments(entry: entry) {
|
|
||||||
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in
|
|
||||||
self?.headerNode.addToAvatarTransitionSurface(view: view)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
Queue.mainQueue().after(0.4) {
|
Queue.mainQueue().after(0.4) {
|
||||||
strongSelf.resetHeaderExpansion()
|
strongSelf.resetHeaderExpansion()
|
||||||
@ -9715,7 +9684,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
mixin.didFinishWithImage = { [weak self] image in
|
mixin.didFinishWithImage = { [weak self] image in
|
||||||
if let image = image {
|
if let image = image {
|
||||||
completion(image)
|
completion(image)
|
||||||
self?.controller?.updateProfilePhoto(image, mode: mode)
|
self?.controller?.updateProfilePhoto(image, mode: mode, uploadStatus: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in
|
mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in
|
||||||
@ -12210,6 +12179,47 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
func cancelItemSelection() {
|
func cancelItemSelection() {
|
||||||
self.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil)
|
self.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openAvatarGallery(peer: EnginePeer, entries: [AvatarGalleryEntry], centralEntry: AvatarGalleryEntry?, animateTransition: Bool) {
|
||||||
|
let entriesPromise = Promise<[AvatarGalleryEntry]>(entries)
|
||||||
|
let galleryController = AvatarGalleryController(context: self.context, peer: peer, sourceCorners: .round, remoteEntries: entriesPromise, skipInitial: true, centralEntryIndex: centralEntry.flatMap { entries.firstIndex(of: $0) }, replaceRootController: { controller, ready in
|
||||||
|
})
|
||||||
|
galleryController.openAvatarSetup = { [weak self] completion in
|
||||||
|
self?.controller?.openAvatarForEditing(fromGallery: true, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
galleryController.avatarPhotoEditCompletion = { [weak self] image in
|
||||||
|
self?.controller?.updateProfilePhoto(image, mode: .generic, uploadStatus: nil)
|
||||||
|
}
|
||||||
|
galleryController.avatarVideoEditCompletion = { [weak self] image, asset, adjustments in
|
||||||
|
self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic)
|
||||||
|
}
|
||||||
|
galleryController.removedEntry = { [weak self] entry in
|
||||||
|
if let item = PeerInfoAvatarListItem(entry: entry) {
|
||||||
|
let _ = self?.headerNode.avatarListNode.listContainerNode.deleteItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).startStrict(next: { [weak self] entry in
|
||||||
|
self?.headerNode.updateAvatarIsHidden(entry: entry)
|
||||||
|
}))
|
||||||
|
self.view.endEditing(true)
|
||||||
|
let arguments = AvatarGalleryControllerPresentationArguments(transitionArguments: { [weak self] _ in
|
||||||
|
if animateTransition, let entry = centralEntry, let transitionNode = self?.headerNode.avatarTransitionArguments(entry: entry) {
|
||||||
|
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in
|
||||||
|
self?.headerNode.addToAvatarTransitionSurface(view: view)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if self.controller?.navigationController != nil {
|
||||||
|
self.controller?.present(galleryController, in: .window(.root), with: arguments)
|
||||||
|
} else {
|
||||||
|
galleryController.presentationArguments = arguments
|
||||||
|
self.context.sharedContext.mainWindow?.present(galleryController, on: .root)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder {
|
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder {
|
||||||
@ -12710,9 +12720,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func openAvatarSetup() {
|
public func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?) {
|
||||||
let proceed = { [weak self] in
|
let proceed = { [weak self] in
|
||||||
self?.openAvatarForEditing()
|
self?.newopenAvatarForEditing(completedWithUploadingImage: completedWithUploadingImage)
|
||||||
}
|
}
|
||||||
if !self.isNodeLoaded {
|
if !self.isNodeLoaded {
|
||||||
self.loadDisplayNode()
|
self.loadDisplayNode()
|
||||||
@ -12724,6 +12734,18 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func openAvatars() {
|
||||||
|
let _ = (self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||||
|
guard let self, let peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.controllerNode.openAvatarGallery(peer: peer, entries: self.controllerNode.headerNode.avatarListNode.listContainerNode.galleryEntries, centralEntry: nil, animateTransition: false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) {
|
func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) {
|
||||||
self.controllerNode.openAvatarForEditing(mode: mode, fromGallery: fromGallery, completion: completion)
|
self.controllerNode.openAvatarForEditing(mode: mode, fromGallery: fromGallery, completion: completion)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,164 +18,197 @@ import PresentationDataUtils
|
|||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
|
|
||||||
extension PeerInfoScreenImpl {
|
extension PeerInfoScreenImpl {
|
||||||
// func newopenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) {
|
func newopenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }, completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView? = { _, _ in nil }) {
|
||||||
// guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else {
|
guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else {
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
// self.view.endEditing(true)
|
self.view.endEditing(true)
|
||||||
//
|
|
||||||
// let peerId = self.peerId
|
let peerId = self.peerId
|
||||||
// var isForum = false
|
var isForum = false
|
||||||
// if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum) {
|
if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum) {
|
||||||
// isForum = true
|
isForum = true
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// var currentIsVideo = false
|
var currentIsVideo = false
|
||||||
// var emojiMarkup: TelegramMediaImage.EmojiMarkup?
|
var emojiMarkup: TelegramMediaImage.EmojiMarkup?
|
||||||
// let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
|
let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
|
||||||
// if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item {
|
if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item {
|
||||||
// currentIsVideo = !videoRepresentations.isEmpty
|
currentIsVideo = !videoRepresentations.isEmpty
|
||||||
// emojiMarkup = emojiMarkupValue
|
emojiMarkup = emojiMarkupValue
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// let _ = isForum
|
let _ = isForum
|
||||||
// let _ = currentIsVideo
|
let _ = currentIsVideo
|
||||||
//
|
|
||||||
// let _ = (self.context.engine.data.get(
|
let _ = (self.context.engine.data.get(
|
||||||
// TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||||
// )
|
)
|
||||||
// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||||
// guard let self, let peer else {
|
guard let self, let peer else {
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// let keyboardInputData = Promise<AvatarKeyboardInputData>()
|
let keyboardInputData = Promise<AvatarKeyboardInputData>()
|
||||||
// keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser))
|
keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser))
|
||||||
//
|
|
||||||
// var hasPhotos = false
|
var hasPhotos = false
|
||||||
// if !peer.profileImageRepresentations.isEmpty {
|
if !peer.profileImageRepresentations.isEmpty {
|
||||||
// hasPhotos = true
|
hasPhotos = true
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// var hasDeleteButton = false
|
var hasDeleteButton = false
|
||||||
// if case .generic = mode {
|
if case .generic = mode {
|
||||||
// hasDeleteButton = hasPhotos && !fromGallery
|
hasDeleteButton = hasPhotos && !fromGallery
|
||||||
// } else if case .custom = mode {
|
} else if case .custom = mode {
|
||||||
// hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true
|
hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true
|
||||||
// } else if case .fallback = mode {
|
} else if case .fallback = mode {
|
||||||
// if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto {
|
if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto {
|
||||||
// hasDeleteButton = photo != nil
|
hasDeleteButton = photo != nil
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// let _ = hasDeleteButton
|
let _ = hasDeleteButton
|
||||||
//
|
|
||||||
// let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController
|
let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController
|
||||||
//
|
|
||||||
// var dismissImpl: (() -> Void)?
|
var dismissImpl: (() -> Void)?
|
||||||
// let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in
|
let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in
|
||||||
// self?.openAvatarRemoval(mode: mode, peer: peer, item: item)
|
self?.openAvatarRemoval(mode: mode, peer: peer, item: item)
|
||||||
// }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in
|
}, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in
|
||||||
// let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
|
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
|
||||||
// if let asset = result as? PHAsset {
|
if let asset = result as? PHAsset {
|
||||||
// subject = .single(.asset(asset))
|
subject = .single(.asset(asset))
|
||||||
// } else if let image = result as? UIImage {
|
} else if let image = result as? UIImage {
|
||||||
// subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||||
// } else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
|
} else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
|
||||||
// subject = result
|
subject = result
|
||||||
// |> map { value -> MediaEditorScreenImpl.Subject? in
|
|> map { value -> MediaEditorScreenImpl.Subject? in
|
||||||
// switch value {
|
switch value {
|
||||||
// case .pendingImage:
|
case .pendingImage:
|
||||||
// return nil
|
return nil
|
||||||
// case let .image(image):
|
case let .image(image):
|
||||||
// return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft)
|
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft)
|
||||||
// case let .video(video):
|
case let .video(video):
|
||||||
// return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft)
|
return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft)
|
||||||
// default:
|
default:
|
||||||
// return nil
|
return nil
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// let peerType: AvatarEditorScreen.PeerType
|
let peerType: AvatarEditorScreen.PeerType
|
||||||
// if mode == .suggest {
|
if mode == .suggest {
|
||||||
// peerType = .suggest
|
peerType = .suggest
|
||||||
// } else if case .legacyGroup = peer {
|
} else if case .legacyGroup = peer {
|
||||||
// peerType = .group
|
peerType = .group
|
||||||
// } else if case let .channel(channel) = peer {
|
} else if case let .channel(channel) = peer {
|
||||||
// if case .group = channel.info {
|
if case .group = channel.info {
|
||||||
// peerType = channel.flags.contains(.isForum) ? .forum : .group
|
peerType = channel.flags.contains(.isForum) ? .forum : .group
|
||||||
// } else {
|
} else {
|
||||||
// peerType = .channel
|
peerType = .channel
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// peerType = .user
|
peerType = .user
|
||||||
// }
|
}
|
||||||
// let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup)
|
let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup)
|
||||||
// //controller.imageCompletion = imageCompletion
|
//controller.imageCompletion = imageCompletion
|
||||||
// //controller.videoCompletion = videoCompletion
|
//controller.videoCompletion = videoCompletion
|
||||||
// parentController?.push(controller)
|
parentController?.push(controller)
|
||||||
// //isFromEditor = true
|
//isFromEditor = true
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// let editorController = MediaEditorScreenImpl(
|
var resultImage: UIImage?
|
||||||
// context: self.context,
|
let uploadStatusPromise = Promise<PeerInfoAvatarUploadStatus>(.progress(0.0))
|
||||||
// mode: .avatarEditor,
|
let editorController = MediaEditorScreenImpl(
|
||||||
// subject: subject,
|
context: self.context,
|
||||||
// transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery(
|
mode: .avatarEditor,
|
||||||
// MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn(
|
subject: subject,
|
||||||
// sourceView: $0,
|
transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery(
|
||||||
// sourceRect: transitionRect,
|
MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn(
|
||||||
// sourceImage: transitionImage
|
sourceView: $0,
|
||||||
// )
|
sourceRect: transitionRect,
|
||||||
// ) }),
|
sourceImage: transitionImage
|
||||||
// transitionOut: { finished, isNew in
|
)
|
||||||
// if !finished, let transitionView {
|
) }),
|
||||||
// return MediaEditorScreenImpl.TransitionOut(
|
transitionOut: { finished, isNew in
|
||||||
// destinationView: transitionView,
|
if !finished {
|
||||||
// destinationRect: transitionView.bounds,
|
if let transitionView {
|
||||||
// destinationCornerRadius: 0.0
|
return MediaEditorScreenImpl.TransitionOut(
|
||||||
// )
|
destinationView: transitionView,
|
||||||
// }
|
destinationRect: transitionView.bounds,
|
||||||
// return nil
|
destinationCornerRadius: 0.0
|
||||||
// }, completion: { [weak self] result, commit in
|
)
|
||||||
// dismissImpl?()
|
}
|
||||||
//
|
} else if let resultImage, let transitionOutView = completedWithUploadingImage(resultImage, uploadStatusPromise.get()) {
|
||||||
// switch result.media {
|
transitionOutView.isHidden = true
|
||||||
// case let .image(image, _):
|
return MediaEditorScreenImpl.TransitionOut(
|
||||||
// self?.updateProfilePhoto(image, mode: mode)
|
destinationView: transitionOutView,
|
||||||
// commit({})
|
destinationRect: transitionOutView.bounds,
|
||||||
// case let .video(video, coverImage, values, _, _):
|
destinationCornerRadius: transitionOutView.bounds.height * 0.5,
|
||||||
// if let coverImage {
|
completion: { [weak transitionOutView] in
|
||||||
// self?.updateProfileVideo(coverImage, asset: video, adjustments: values, mode: mode)
|
transitionOutView?.isHidden = false
|
||||||
// }
|
}
|
||||||
// commit({})
|
)
|
||||||
// default:
|
}
|
||||||
// break
|
return nil
|
||||||
// }
|
}, completion: { [weak self] result, commit in
|
||||||
// } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
switch result.media {
|
||||||
// )
|
case let .image(image, _):
|
||||||
// editorController.cancelled = { _ in
|
resultImage = image
|
||||||
// cancelled()
|
self?.updateProfilePhoto(image, mode: mode, uploadStatus: uploadStatusPromise)
|
||||||
// }
|
commit({})
|
||||||
// self.push(editorController)
|
case let .video(video, coverImage, values, _, _):
|
||||||
// }, dismissed: {
|
if let coverImage {
|
||||||
//
|
let _ = values
|
||||||
// })
|
//TODO:release
|
||||||
// dismissImpl = { [weak mainController] in
|
resultImage = coverImage
|
||||||
// if let mainController, let navigationController = mainController.navigationController {
|
self?.updateProfileVideo(coverImage, asset: video, adjustments: nil, mode: mode)
|
||||||
// var viewControllers = navigationController.viewControllers
|
}
|
||||||
// viewControllers = viewControllers.filter { c in
|
commit({})
|
||||||
// return !(c is CameraScreen) && c !== mainController
|
default:
|
||||||
// }
|
break
|
||||||
// navigationController.setViewControllers(viewControllers, animated: false)
|
}
|
||||||
// }
|
|
||||||
// }
|
dismissImpl?()
|
||||||
// mainController.navigationPresentation = .flatModal
|
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||||
// mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
)
|
||||||
// self.push(mainController)
|
editorController.cancelled = { _ in
|
||||||
// })
|
cancelled()
|
||||||
// }
|
}
|
||||||
|
if self.navigationController != nil {
|
||||||
|
self.push(editorController)
|
||||||
|
} else {
|
||||||
|
self.parentController?.pushViewController(editorController)
|
||||||
|
}
|
||||||
|
}, dismissed: {
|
||||||
|
|
||||||
|
})
|
||||||
|
dismissImpl = { [weak self, weak mainController] in
|
||||||
|
if let mainController, let navigationController = mainController.navigationController {
|
||||||
|
var viewControllers = navigationController.viewControllers
|
||||||
|
viewControllers = viewControllers.filter { c in
|
||||||
|
return !(c is CameraScreen) && c !== mainController
|
||||||
|
}
|
||||||
|
navigationController.setViewControllers(viewControllers, animated: false)
|
||||||
|
}
|
||||||
|
if let self, let navigationController = self.parentController, let mainController {
|
||||||
|
var viewControllers = navigationController.viewControllers
|
||||||
|
viewControllers = viewControllers.filter { c in
|
||||||
|
return !(c is CameraScreen) && c !== mainController
|
||||||
|
}
|
||||||
|
navigationController.setViewControllers(viewControllers, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainController.navigationPresentation = .flatModal
|
||||||
|
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||||
|
if self.navigationController != nil {
|
||||||
|
self.push(mainController)
|
||||||
|
} else {
|
||||||
|
self.parentController?.pushViewController(mainController)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func openAvatarRemoval(mode: PeerInfoAvatarEditingMode, peer: EnginePeer? = nil, item: PeerInfoAvatarListItem? = nil, completion: @escaping () -> Void = {}) {
|
func openAvatarRemoval(mode: PeerInfoAvatarEditingMode, peer: EnginePeer? = nil, item: PeerInfoAvatarListItem? = nil, completion: @escaping () -> Void = {}) {
|
||||||
let proceed = { [weak self] in
|
let proceed = { [weak self] in
|
||||||
@ -250,8 +283,9 @@ extension PeerInfoScreenImpl {
|
|||||||
(self.navigationController?.topViewController as? ViewController)?.present(actionSheet, in: .window(.root))
|
(self.navigationController?.topViewController as? ViewController)?.present(actionSheet, in: .window(.root))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode) {
|
public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
|
||||||
guard let data = image.jpegData(compressionQuality: 0.6) else {
|
guard let data = image.jpegData(compressionQuality: 0.6) else {
|
||||||
|
uploadStatus?.set(.single(.done))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,8 +361,10 @@ extension PeerInfoScreenImpl {
|
|||||||
}
|
}
|
||||||
switch result {
|
switch result {
|
||||||
case .complete:
|
case .complete:
|
||||||
|
uploadStatus?.set(.single(.done))
|
||||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
|
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
|
||||||
case let .progress(value):
|
case let .progress(value):
|
||||||
|
uploadStatus?.set(.single(.progress(value)))
|
||||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(value)))
|
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(value)))
|
||||||
}
|
}
|
||||||
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
|
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
|
||||||
|
|||||||
@ -1221,7 +1221,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
controller.imageCompletion = { [weak self] image, commit in
|
controller.imageCompletion = { [weak self] image, commit in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
|
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
|
||||||
settingsController.updateProfilePhoto(image, mode: .accept)
|
settingsController.updateProfilePhoto(image, mode: .accept, uploadStatus: nil)
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1258,7 +1258,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}, imageCompletion: { [weak self] image in
|
}, imageCompletion: { [weak self] image in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
|
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
|
||||||
settingsController.updateProfilePhoto(image, mode: .accept)
|
settingsController.updateProfilePhoto(image, mode: .accept, uploadStatus: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, videoCompletion: { [weak self] image, url, adjustments in
|
}, videoCompletion: { [weak self] image, url, adjustments in
|
||||||
|
|||||||
@ -784,9 +784,89 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
self.adMessagesContext = adMessagesContext
|
self.adMessagesContext = adMessagesContext
|
||||||
if peerId.namespace == Namespaces.Peer.CloudUser {
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||||
adMessages = .single((nil, []))
|
adMessages = .single((nil, []))
|
||||||
|
} else {
|
||||||
|
if context.sharedContext.immediateExperimentalUISettings.fakeAds {
|
||||||
|
adMessages = context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||||
|
)
|
||||||
|
|> map { peer -> (interPostInterval: Int32?, messages: [Message]) in
|
||||||
|
let fakeAdMessages: [Message] = (0 ..< 10).map { i -> Message in
|
||||||
|
var attributes: [MessageAttribute] = []
|
||||||
|
|
||||||
|
let mappedMessageType: AdMessageAttribute.MessageType = .sponsored
|
||||||
|
attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false))
|
||||||
|
|
||||||
|
var messagePeers = SimpleDictionary<PeerId, Peer>()
|
||||||
|
|
||||||
|
if let peer {
|
||||||
|
messagePeers[peer.id] = peer._asPeer()
|
||||||
|
}
|
||||||
|
|
||||||
|
let author: Peer = TelegramChannel(
|
||||||
|
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
|
||||||
|
accessHash: nil,
|
||||||
|
title: "Fake Ad",
|
||||||
|
username: nil,
|
||||||
|
photo: [],
|
||||||
|
creationDate: 0,
|
||||||
|
version: 0,
|
||||||
|
participationStatus: .left,
|
||||||
|
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
|
||||||
|
flags: [],
|
||||||
|
restrictionInfo: nil,
|
||||||
|
adminRights: nil,
|
||||||
|
bannedRights: nil,
|
||||||
|
defaultBannedRights: nil,
|
||||||
|
usernames: [],
|
||||||
|
storiesHidden: nil,
|
||||||
|
nameColor: .blue,
|
||||||
|
backgroundEmojiId: nil,
|
||||||
|
profileColor: nil,
|
||||||
|
profileBackgroundEmojiId: nil,
|
||||||
|
emojiStatus: nil,
|
||||||
|
approximateBoostLevel: nil,
|
||||||
|
subscriptionUntilDate: nil,
|
||||||
|
verificationIconFileId: nil
|
||||||
|
)
|
||||||
|
messagePeers[author.id] = author
|
||||||
|
|
||||||
|
let messageText = "Fake Ad N\(i)"
|
||||||
|
let messageHash = (messageText.hashValue &+ 31 &* peerId.hashValue) &* 31 &+ author.id.hashValue
|
||||||
|
let messageStableVersion = UInt32(bitPattern: Int32(truncatingIfNeeded: messageHash))
|
||||||
|
|
||||||
|
return Message(
|
||||||
|
stableId: 0,
|
||||||
|
stableVersion: messageStableVersion,
|
||||||
|
id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0),
|
||||||
|
globallyUniqueId: nil,
|
||||||
|
groupingKey: nil,
|
||||||
|
groupInfo: nil,
|
||||||
|
threadId: nil,
|
||||||
|
timestamp: Int32.max - 1,
|
||||||
|
flags: [.Incoming],
|
||||||
|
tags: [],
|
||||||
|
globalTags: [],
|
||||||
|
localTags: [],
|
||||||
|
customTags: [],
|
||||||
|
forwardInfo: nil,
|
||||||
|
author: author,
|
||||||
|
text: messageText,
|
||||||
|
attributes: attributes,
|
||||||
|
media: [],
|
||||||
|
peers: messagePeers,
|
||||||
|
associatedMessages: SimpleDictionary<MessageId, Message>(),
|
||||||
|
associatedMessageIds: [],
|
||||||
|
associatedMedia: [:],
|
||||||
|
associatedThreadInfo: nil,
|
||||||
|
associatedStories: [:]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (10, fakeAdMessages)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
adMessages = adMessagesContext.state
|
adMessages = adMessagesContext.state
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.adMessagesContext = nil
|
self.adMessagesContext = nil
|
||||||
adMessages = .single((nil, []))
|
adMessages = .single((nil, []))
|
||||||
@ -2444,6 +2524,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
var insertionTimestamp: Int32?
|
var insertionTimestamp: Int32?
|
||||||
if self.currentPrefetchDirectionIsToLater {
|
if self.currentPrefetchDirectionIsToLater {
|
||||||
outer: for i in selectedRange.0 ... selectedRange.1 {
|
outer: for i in selectedRange.0 ... selectedRange.1 {
|
||||||
|
if historyView.originalView.laterId == nil && i >= historyView.filteredEntries.count - 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
switch historyView.filteredEntries[i] {
|
switch historyView.filteredEntries[i] {
|
||||||
case let .MessageEntry(message, _, _, _, _, _):
|
case let .MessageEntry(message, _, _, _, _, _):
|
||||||
if message.id.namespace == Namespaces.Message.Cloud {
|
if message.id.namespace == Namespaces.Message.Cloud {
|
||||||
|
|||||||
@ -497,7 +497,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat
|
|||||||
|
|
||||||
var isFirstUpdate = true
|
var isFirstUpdate = true
|
||||||
self.itemsDisposable = (combineLatest(
|
self.itemsDisposable = (combineLatest(
|
||||||
context.engine.stickers.availableReactions(),
|
context.availableReactions,
|
||||||
context.engine.stickers.savedMessageTagData(),
|
context.engine.stickers.savedMessageTagData(),
|
||||||
tagsAndFiles
|
tagsAndFiles
|
||||||
)
|
)
|
||||||
|
|||||||
@ -750,8 +750,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||||||
self.accountSettingsController?.openBirthdaySetup()
|
self.accountSettingsController?.openBirthdaySetup()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func openPhotoSetup() {
|
public func openPhotoSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?) {
|
||||||
self.accountSettingsController?.openAvatarSetup()
|
self.accountSettingsController?.openAvatarSetup(completedWithUploadingImage: completedWithUploadingImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func openAvatars() {
|
||||||
|
self.accountSettingsController?.openAvatars()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
public var autoBenchmarkReflectors: Bool?
|
public var autoBenchmarkReflectors: Bool?
|
||||||
public var conferenceCalls: Bool
|
public var conferenceCalls: Bool
|
||||||
public var playerV2: Bool
|
public var playerV2: Bool
|
||||||
|
public var devRequests: Bool
|
||||||
|
public var fakeAds: Bool
|
||||||
|
|
||||||
public static var defaultSettings: ExperimentalUISettings {
|
public static var defaultSettings: ExperimentalUISettings {
|
||||||
return ExperimentalUISettings(
|
return ExperimentalUISettings(
|
||||||
@ -105,7 +107,9 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
enableLocalTranslation: false,
|
enableLocalTranslation: false,
|
||||||
autoBenchmarkReflectors: nil,
|
autoBenchmarkReflectors: nil,
|
||||||
conferenceCalls: false,
|
conferenceCalls: false,
|
||||||
playerV2: false
|
playerV2: false,
|
||||||
|
devRequests: false,
|
||||||
|
fakeAds: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +152,9 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
enableLocalTranslation: Bool,
|
enableLocalTranslation: Bool,
|
||||||
autoBenchmarkReflectors: Bool?,
|
autoBenchmarkReflectors: Bool?,
|
||||||
conferenceCalls: Bool,
|
conferenceCalls: Bool,
|
||||||
playerV2: Bool
|
playerV2: Bool,
|
||||||
|
devRequests: Bool,
|
||||||
|
fakeAds: Bool
|
||||||
) {
|
) {
|
||||||
self.keepChatNavigationStack = keepChatNavigationStack
|
self.keepChatNavigationStack = keepChatNavigationStack
|
||||||
self.skipReadHistory = skipReadHistory
|
self.skipReadHistory = skipReadHistory
|
||||||
@ -189,6 +195,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
self.autoBenchmarkReflectors = autoBenchmarkReflectors
|
self.autoBenchmarkReflectors = autoBenchmarkReflectors
|
||||||
self.conferenceCalls = conferenceCalls
|
self.conferenceCalls = conferenceCalls
|
||||||
self.playerV2 = playerV2
|
self.playerV2 = playerV2
|
||||||
|
self.devRequests = devRequests
|
||||||
|
self.fakeAds = fakeAds
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
@ -233,6 +241,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
self.autoBenchmarkReflectors = try container.decodeIfPresent(Bool.self, forKey: "autoBenchmarkReflectors")
|
self.autoBenchmarkReflectors = try container.decodeIfPresent(Bool.self, forKey: "autoBenchmarkReflectors")
|
||||||
self.conferenceCalls = try container.decodeIfPresent(Bool.self, forKey: "conferenceCalls") ?? false
|
self.conferenceCalls = try container.decodeIfPresent(Bool.self, forKey: "conferenceCalls") ?? false
|
||||||
self.playerV2 = try container.decodeIfPresent(Bool.self, forKey: "playerV2") ?? false
|
self.playerV2 = try container.decodeIfPresent(Bool.self, forKey: "playerV2") ?? false
|
||||||
|
self.devRequests = try container.decodeIfPresent(Bool.self, forKey: "devRequests") ?? false
|
||||||
|
self.fakeAds = try container.decodeIfPresent(Bool.self, forKey: "fakeAds") ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
@ -277,6 +287,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
try container.encodeIfPresent(self.autoBenchmarkReflectors, forKey: "autoBenchmarkReflectors")
|
try container.encodeIfPresent(self.autoBenchmarkReflectors, forKey: "autoBenchmarkReflectors")
|
||||||
try container.encodeIfPresent(self.conferenceCalls, forKey: "conferenceCalls")
|
try container.encodeIfPresent(self.conferenceCalls, forKey: "conferenceCalls")
|
||||||
try container.encodeIfPresent(self.playerV2, forKey: "playerV2")
|
try container.encodeIfPresent(self.playerV2, forKey: "playerV2")
|
||||||
|
try container.encodeIfPresent(self.devRequests, forKey: "devRequests")
|
||||||
|
try container.encodeIfPresent(self.fakeAds, forKey: "fakeAds")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -230,7 +230,7 @@ public final class HLSVideoContent: UniversalVideoContent {
|
|||||||
|
|
||||||
public let id: AnyHashable
|
public let id: AnyHashable
|
||||||
public let nativeId: NativeVideoContentId
|
public let nativeId: NativeVideoContentId
|
||||||
let userLocation: MediaResourceUserLocation
|
public let userLocation: MediaResourceUserLocation
|
||||||
public let fileReference: FileMediaReference
|
public let fileReference: FileMediaReference
|
||||||
public let dimensions: CGSize
|
public let dimensions: CGSize
|
||||||
public let duration: Double
|
public let duration: Double
|
||||||
|
|||||||
@ -461,7 +461,6 @@ public final class OngoingGroupCallContext {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let audioDevice: OngoingCallContext.AudioDevice?
|
let audioDevice: OngoingCallContext.AudioDevice?
|
||||||
#endif
|
#endif
|
||||||
let sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max))
|
|
||||||
|
|
||||||
let joinPayload = Promise<(String, UInt32)>()
|
let joinPayload = Promise<(String, UInt32)>()
|
||||||
let networkState = ValuePromise<NetworkState>(NetworkState(isConnected: false, isTransitioningFromBroadcastToRtc: false), ignoreRepeated: true)
|
let networkState = ValuePromise<NetworkState>(NetworkState(isConnected: false, isTransitioningFromBroadcastToRtc: false), ignoreRepeated: true)
|
||||||
@ -507,14 +506,9 @@ public final class OngoingGroupCallContext {
|
|||||||
self.tempStatsLogFile = EngineTempBox.shared.tempFile(fileName: "CallStats.json")
|
self.tempStatsLogFile = EngineTempBox.shared.tempFile(fileName: "CallStats.json")
|
||||||
let tempStatsLogPath = self.tempStatsLogFile.path
|
let tempStatsLogPath = self.tempStatsLogFile.path
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
if sharedAudioDevice == nil {
|
|
||||||
self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false)
|
|
||||||
} else {
|
|
||||||
self.audioDevice = sharedAudioDevice
|
self.audioDevice = sharedAudioDevice
|
||||||
}
|
|
||||||
let audioDevice = self.audioDevice
|
let audioDevice = self.audioDevice
|
||||||
#endif
|
|
||||||
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
|
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
|
||||||
var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)?
|
var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)?
|
||||||
var activityUpdatedImpl: (([UInt32]) -> Void)?
|
var activityUpdatedImpl: (([UInt32]) -> Void)?
|
||||||
@ -882,7 +876,7 @@ public final class OngoingGroupCallContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop(account: Account, reportCallId: CallId?) {
|
func stop(account: Account?, reportCallId: CallId?, debugLog: Promise<String?>) {
|
||||||
self.context.stop()
|
self.context.stop()
|
||||||
|
|
||||||
let logPath = self.logPath
|
let logPath = self.logPath
|
||||||
@ -892,16 +886,18 @@ public final class OngoingGroupCallContext {
|
|||||||
}
|
}
|
||||||
let tempStatsLogPath = self.tempStatsLogFile.path
|
let tempStatsLogPath = self.tempStatsLogFile.path
|
||||||
|
|
||||||
|
debugLog.set(.single(nil))
|
||||||
|
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
self.context.stop({
|
self.context.stop({
|
||||||
queue.async {
|
queue.async {
|
||||||
if !statsLogPath.isEmpty {
|
if !statsLogPath.isEmpty, let account {
|
||||||
let logsPath = callLogsPath(account: account)
|
let logsPath = callLogsPath(account: account)
|
||||||
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
||||||
let _ = try? FileManager.default.moveItem(atPath: tempStatsLogPath, toPath: statsLogPath)
|
let _ = try? FileManager.default.moveItem(atPath: tempStatsLogPath, toPath: statsLogPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let callId = reportCallId, !statsLogPath.isEmpty, let data = try? Data(contentsOf: URL(fileURLWithPath: statsLogPath)), let dataString = String(data: data, encoding: .utf8) {
|
if let callId = reportCallId, !statsLogPath.isEmpty, let data = try? Data(contentsOf: URL(fileURLWithPath: statsLogPath)), let dataString = String(data: data, encoding: .utf8), let account {
|
||||||
let engine = TelegramEngine(account: account)
|
let engine = TelegramEngine(account: account)
|
||||||
let _ = engine.calls.saveCallDebugLog(callId: callId, log: dataString).start(next: { result in
|
let _ = engine.calls.saveCallDebugLog(callId: callId, log: dataString).start(next: { result in
|
||||||
switch result {
|
switch result {
|
||||||
@ -1219,9 +1215,9 @@ public final class OngoingGroupCallContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stop(account: Account, reportCallId: CallId?) {
|
public func stop(account: Account?, reportCallId: CallId?, debugLog: Promise<String?>) {
|
||||||
self.impl.with { impl in
|
self.impl.with { impl in
|
||||||
impl.stop(account: account, reportCallId: reportCallId)
|
impl.stop(account: account, reportCallId: reportCallId, debugLog: debugLog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,6 @@ import SwiftSignalKit
|
|||||||
import CoreMedia
|
import CoreMedia
|
||||||
import ImageIO
|
import ImageIO
|
||||||
|
|
||||||
private struct PayloadDescription: Codable {
|
|
||||||
var id: UInt32
|
|
||||||
var timestamp: Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct JoinPayload: Codable {
|
private struct JoinPayload: Codable {
|
||||||
var id: UInt32
|
var id: UInt32
|
||||||
var string: String
|
var string: String
|
||||||
@ -18,11 +13,6 @@ private struct JoinResponsePayload: Codable {
|
|||||||
var string: String
|
var string: String
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct KeepaliveInfo: Codable {
|
|
||||||
var id: UInt32
|
|
||||||
var timestamp: Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct CutoffPayload: Codable {
|
private struct CutoffPayload: Codable {
|
||||||
var id: UInt32
|
var id: UInt32
|
||||||
var timestamp: Int32
|
var timestamp: Int32
|
||||||
@ -370,6 +360,16 @@ private final class MappedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class IpcGroupCallBufferAppContext {
|
public final class IpcGroupCallBufferAppContext {
|
||||||
|
struct KeepaliveInfo: Codable {
|
||||||
|
var id: UInt32
|
||||||
|
var timestamp: Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PayloadDescription: Codable {
|
||||||
|
var id: UInt32
|
||||||
|
var timestamp: Int32
|
||||||
|
}
|
||||||
|
|
||||||
private let basePath: String
|
private let basePath: String
|
||||||
private var audioServer: NamedPipeReader?
|
private var audioServer: NamedPipeReader?
|
||||||
|
|
||||||
@ -460,7 +460,7 @@ public final class IpcGroupCallBufferAppContext {
|
|||||||
|
|
||||||
private func updateCallIsActive() {
|
private func updateCallIsActive() {
|
||||||
let timestamp = Int32(Date().timeIntervalSince1970)
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||||
let payloadDescription = PayloadDescription(
|
let payloadDescription = IpcGroupCallBufferAppContext.PayloadDescription(
|
||||||
id: self.id,
|
id: self.id,
|
||||||
timestamp: timestamp
|
timestamp: timestamp
|
||||||
)
|
)
|
||||||
@ -477,7 +477,7 @@ public final class IpcGroupCallBufferAppContext {
|
|||||||
guard let keepaliveInfoData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
guard let keepaliveInfoData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let keepaliveInfo = try? JSONDecoder().decode(KeepaliveInfo.self, from: keepaliveInfoData) else {
|
guard let keepaliveInfo = try? JSONDecoder().decode(IpcGroupCallBufferAppContext.KeepaliveInfo.self, from: keepaliveInfoData) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if keepaliveInfo.id != self.id {
|
if keepaliveInfo.id != self.id {
|
||||||
@ -587,7 +587,7 @@ public final class IpcGroupCallBufferBroadcastContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let payloadDescription = try? JSONDecoder().decode(PayloadDescription.self, from: payloadDescriptionData) else {
|
guard let payloadDescription = try? JSONDecoder().decode(IpcGroupCallBufferAppContext.PayloadDescription.self, from: payloadDescriptionData) else {
|
||||||
self.statusPromise.set(.single(.finished(.error)))
|
self.statusPromise.set(.single(.finished(.error)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -646,7 +646,7 @@ public final class IpcGroupCallBufferBroadcastContext {
|
|||||||
guard let currentId = self.currentId else {
|
guard let currentId = self.currentId else {
|
||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
let keepaliveInfo = KeepaliveInfo(
|
let keepaliveInfo = IpcGroupCallBufferAppContext.KeepaliveInfo(
|
||||||
id: currentId,
|
id: currentId,
|
||||||
timestamp: Int32(Date().timeIntervalSince1970)
|
timestamp: Int32(Date().timeIntervalSince1970)
|
||||||
)
|
)
|
||||||
@ -795,3 +795,319 @@ public func deserializePixelBuffer(data: Data) -> CVPixelBuffer? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class IpcGroupCallEmbeddedAppContext {
|
||||||
|
public struct JoinPayload: Codable, Equatable {
|
||||||
|
public var id: UInt32
|
||||||
|
public var data: String
|
||||||
|
public var ssrc: UInt32
|
||||||
|
|
||||||
|
public init(id: UInt32, data: String, ssrc: UInt32) {
|
||||||
|
self.id = id
|
||||||
|
self.data = data
|
||||||
|
self.ssrc = ssrc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct JoinResponse: Codable, Equatable {
|
||||||
|
public var data: String
|
||||||
|
|
||||||
|
public init(data: String) {
|
||||||
|
self.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KeepaliveInfo: Codable {
|
||||||
|
var id: UInt32
|
||||||
|
var timestamp: Int32
|
||||||
|
var joinPayload: JoinPayload?
|
||||||
|
|
||||||
|
init(id: UInt32, timestamp: Int32, joinPayload: JoinPayload?) {
|
||||||
|
self.id = id
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.joinPayload = joinPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PayloadDescription: Codable {
|
||||||
|
var id: UInt32
|
||||||
|
var timestamp: Int32
|
||||||
|
var activeRequestId: UInt32?
|
||||||
|
var joinResponse: JoinResponse?
|
||||||
|
|
||||||
|
init(id: UInt32, timestamp: Int32, activeRequestId: UInt32?, joinResponse: JoinResponse?) {
|
||||||
|
self.id = id
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.activeRequestId = activeRequestId
|
||||||
|
self.joinResponse = joinResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let basePath: String
|
||||||
|
|
||||||
|
private let id: UInt32
|
||||||
|
|
||||||
|
private let isActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
|
public var isActive: Signal<Bool, NoError> {
|
||||||
|
return self.isActivePromise.get()
|
||||||
|
}
|
||||||
|
private var isActiveCheckTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
private var joinPayloadValue: JoinPayload? {
|
||||||
|
didSet {
|
||||||
|
if let joinPayload = self.joinPayloadValue, joinPayload != oldValue {
|
||||||
|
self.joinPayloadPromise.set(.single(joinPayload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let joinPayloadPromise = Promise<JoinPayload>()
|
||||||
|
public var joinPayload: Signal<JoinPayload, NoError> {
|
||||||
|
return self.joinPayloadPromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nextActiveRequestId: UInt32 = 0
|
||||||
|
private var activeRequestId: UInt32? {
|
||||||
|
didSet {
|
||||||
|
if self.activeRequestId != oldValue {
|
||||||
|
self.updateCallIsActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var joinResponse: JoinResponse? {
|
||||||
|
didSet {
|
||||||
|
if self.joinResponse != oldValue {
|
||||||
|
self.updateCallIsActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var callActiveInfoTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
public init(basePath: String) {
|
||||||
|
self.basePath = basePath
|
||||||
|
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
|
self.id = UInt32.random(in: 0 ..< UInt32.max)
|
||||||
|
|
||||||
|
self.updateCallIsActive()
|
||||||
|
|
||||||
|
let callActiveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||||
|
self?.updateCallIsActive()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
self.callActiveInfoTimer = callActiveInfoTimer
|
||||||
|
callActiveInfoTimer.start()
|
||||||
|
|
||||||
|
let isActiveCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||||
|
self?.updateKeepaliveInfo()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
self.isActiveCheckTimer = isActiveCheckTimer
|
||||||
|
isActiveCheckTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.callActiveInfoTimer?.invalidate()
|
||||||
|
self.isActiveCheckTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCallIsActive() {
|
||||||
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||||
|
let payloadDescription = IpcGroupCallEmbeddedAppContext.PayloadDescription(
|
||||||
|
id: self.id,
|
||||||
|
timestamp: timestamp,
|
||||||
|
activeRequestId: self.activeRequestId,
|
||||||
|
joinResponse: self.joinResponse
|
||||||
|
)
|
||||||
|
guard let payloadDescriptionData = try? JSONEncoder().encode(payloadDescription) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let _ = try? payloadDescriptionData.write(to: URL(fileURLWithPath: payloadDescriptionPath(basePath: self.basePath)), options: .atomic) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateKeepaliveInfo() {
|
||||||
|
let filePath = keepaliveInfoPath(basePath: self.basePath)
|
||||||
|
guard let keepaliveInfoData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let keepaliveInfo = try? JSONDecoder().decode(KeepaliveInfo.self, from: keepaliveInfoData) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if keepaliveInfo.id != self.id {
|
||||||
|
self.isActivePromise.set(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||||
|
if keepaliveInfo.timestamp < timestamp - Int32(keepaliveTimeout) {
|
||||||
|
self.isActivePromise.set(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isActivePromise.set(true)
|
||||||
|
|
||||||
|
self.joinPayloadValue = keepaliveInfo.joinPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startScreencast() -> UInt32? {
|
||||||
|
if self.activeRequestId == nil {
|
||||||
|
let id = self.nextActiveRequestId
|
||||||
|
self.nextActiveRequestId += 1
|
||||||
|
self.activeRequestId = id
|
||||||
|
return id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopScreencast() {
|
||||||
|
self.activeRequestId = nil
|
||||||
|
|
||||||
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||||
|
let cutoffPayload = CutoffPayload(
|
||||||
|
id: self.id,
|
||||||
|
timestamp: timestamp
|
||||||
|
)
|
||||||
|
guard let cutoffPayloadData = try? JSONEncoder().encode(cutoffPayload) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let _ = try? cutoffPayloadData.write(to: URL(fileURLWithPath: cutoffPayloadPath(basePath: self.basePath)), options: .atomic) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class IpcGroupCallEmbeddedBroadcastContext {
|
||||||
|
public enum Status {
|
||||||
|
public enum FinishReason {
|
||||||
|
case screencastEnded
|
||||||
|
case callEnded
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
case active(id: UInt32?, joinResponse: IpcGroupCallEmbeddedAppContext.JoinResponse?)
|
||||||
|
case finished(FinishReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let basePath: String
|
||||||
|
private var timer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
private let statusPromise = Promise<Status>()
|
||||||
|
public var status: Signal<Status, NoError> {
|
||||||
|
return self.statusPromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentId: UInt32?
|
||||||
|
|
||||||
|
private var callActiveInfoTimer: SwiftSignalKit.Timer?
|
||||||
|
private var keepaliveInfoTimer: SwiftSignalKit.Timer?
|
||||||
|
private var screencastCutoffTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
public var joinPayload: IpcGroupCallEmbeddedAppContext.JoinPayload? {
|
||||||
|
didSet {
|
||||||
|
if self.joinPayload != oldValue {
|
||||||
|
self.writeKeepaliveInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(basePath: String) {
|
||||||
|
self.basePath = basePath
|
||||||
|
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
|
let callActiveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||||
|
self?.updateCallIsActive()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
self.callActiveInfoTimer = callActiveInfoTimer
|
||||||
|
callActiveInfoTimer.start()
|
||||||
|
|
||||||
|
let screencastCutoffTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||||
|
self?.updateScreencastCutoff()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
self.screencastCutoffTimer = screencastCutoffTimer
|
||||||
|
screencastCutoffTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.endActiveIndication()
|
||||||
|
|
||||||
|
self.callActiveInfoTimer?.invalidate()
|
||||||
|
self.keepaliveInfoTimer?.invalidate()
|
||||||
|
self.screencastCutoffTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScreencastCutoff() {
|
||||||
|
let filePath = cutoffPayloadPath(basePath: self.basePath)
|
||||||
|
guard let cutoffPayloadData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let cutoffPayload = try? JSONDecoder().decode(CutoffPayload.self, from: cutoffPayloadData) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||||
|
if let currentId = self.currentId, currentId == cutoffPayload.id && cutoffPayload.timestamp > timestamp - 10 {
|
||||||
|
self.statusPromise.set(.single(.finished(.screencastEnded)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCallIsActive() {
|
||||||
|
let filePath = payloadDescriptionPath(basePath: self.basePath)
|
||||||
|
guard let payloadDescriptionData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
||||||
|
self.statusPromise.set(.single(.finished(.error)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let payloadDescription = try? JSONDecoder().decode(IpcGroupCallEmbeddedAppContext.PayloadDescription.self, from: payloadDescriptionData) else {
|
||||||
|
self.statusPromise.set(.single(.finished(.error)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||||
|
if payloadDescription.timestamp < timestamp - 4 {
|
||||||
|
self.statusPromise.set(.single(.finished(.callEnded)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentId = self.currentId {
|
||||||
|
if currentId != payloadDescription.id {
|
||||||
|
self.statusPromise.set(.single(.finished(.callEnded)))
|
||||||
|
} else {
|
||||||
|
self.statusPromise.set(.single(.active(id: payloadDescription.activeRequestId, joinResponse: payloadDescription.joinResponse)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.currentId = payloadDescription.id
|
||||||
|
|
||||||
|
self.writeKeepaliveInfo()
|
||||||
|
|
||||||
|
let keepaliveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||||
|
self?.writeKeepaliveInfo()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
self.keepaliveInfoTimer = keepaliveInfoTimer
|
||||||
|
keepaliveInfoTimer.start()
|
||||||
|
|
||||||
|
self.statusPromise.set(.single(.active(id: payloadDescription.activeRequestId, joinResponse: payloadDescription.joinResponse)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeKeepaliveInfo() {
|
||||||
|
guard let currentId = self.currentId else {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
let keepaliveInfo = IpcGroupCallEmbeddedAppContext.KeepaliveInfo(
|
||||||
|
id: currentId,
|
||||||
|
timestamp: Int32(Date().timeIntervalSince1970),
|
||||||
|
joinPayload: self.joinPayload
|
||||||
|
)
|
||||||
|
guard let keepaliveInfoData = try? JSONEncoder().encode(keepaliveInfo) else {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
guard let _ = try? keepaliveInfoData.write(to: URL(fileURLWithPath: keepaliveInfoPath(basePath: self.basePath)), options: .atomic) else {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endActiveIndication() {
|
||||||
|
let _ = try? FileManager.default.removeItem(atPath: keepaliveInfoPath(basePath: self.basePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,13 +7,13 @@ import TelegramUIPreferences
|
|||||||
import TgVoip
|
import TgVoip
|
||||||
import TgVoipWebrtc
|
import TgVoipWebrtc
|
||||||
|
|
||||||
private let debugUseLegacyVersionForReflectors: Bool = {
|
private func debugUseLegacyVersionForReflectors() -> Bool {
|
||||||
#if DEBUG && false
|
#if DEBUG && false
|
||||||
return true
|
return true
|
||||||
#else
|
#else
|
||||||
return false
|
return false
|
||||||
#endif
|
#endif
|
||||||
}()
|
}
|
||||||
|
|
||||||
private struct PeerTag: Hashable, CustomStringConvertible {
|
private struct PeerTag: Hashable, CustomStringConvertible {
|
||||||
var bytes: [UInt8] = Array<UInt8>(repeating: 0, count: 16)
|
var bytes: [UInt8] = Array<UInt8>(repeating: 0, count: 16)
|
||||||
@ -510,7 +510,7 @@ public final class OngoingCallVideoCapturer {
|
|||||||
self.impl.setIsVideoEnabled(value)
|
self.impl.setIsVideoEnabled(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func injectPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: CGImagePropertyOrientation) {
|
public func injectSampleBuffer(_ sampleBuffer: CMSampleBuffer, rotation: CGImagePropertyOrientation, completion: @escaping () -> Void) {
|
||||||
var videoRotation: OngoingCallVideoOrientation = .rotation0
|
var videoRotation: OngoingCallVideoOrientation = .rotation0
|
||||||
switch rotation {
|
switch rotation {
|
||||||
case .up:
|
case .up:
|
||||||
@ -524,7 +524,7 @@ public final class OngoingCallVideoCapturer {
|
|||||||
default:
|
default:
|
||||||
videoRotation = .rotation0
|
videoRotation = .rotation0
|
||||||
}
|
}
|
||||||
self.impl.submitPixelBuffer(pixelBuffer, rotation: videoRotation.orientation)
|
self.impl.submitSampleBuffer(sampleBuffer, rotation: videoRotation.orientation, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
|
public func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
|
||||||
@ -819,7 +819,7 @@ public final class OngoingCallContext {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if debugUseLegacyVersionForReflectors {
|
if debugUseLegacyVersionForReflectors() {
|
||||||
return [(OngoingCallThreadLocalContext.version(), true)]
|
return [(OngoingCallThreadLocalContext.version(), true)]
|
||||||
} else {
|
} else {
|
||||||
var result: [(version: String, supportsVideo: Bool)] = [(OngoingCallThreadLocalContext.version(), false)]
|
var result: [(version: String, supportsVideo: Bool)] = [(OngoingCallThreadLocalContext.version(), false)]
|
||||||
@ -860,9 +860,9 @@ public final class OngoingCallContext {
|
|||||||
var useModernImplementation = true
|
var useModernImplementation = true
|
||||||
var version = version
|
var version = version
|
||||||
var allowP2P = allowP2P
|
var allowP2P = allowP2P
|
||||||
if debugUseLegacyVersionForReflectors {
|
if debugUseLegacyVersionForReflectors() {
|
||||||
useModernImplementation = true
|
useModernImplementation = true
|
||||||
version = "5.0.0"
|
version = "12.0.0"
|
||||||
allowP2P = false
|
allowP2P = false
|
||||||
} else {
|
} else {
|
||||||
useModernImplementation = version != OngoingCallThreadLocalContext.version()
|
useModernImplementation = version != OngoingCallThreadLocalContext.version()
|
||||||
@ -879,7 +879,23 @@ public final class OngoingCallContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let unfilteredConnections = [connections.primary] + connections.alternatives
|
var unfilteredConnections: [CallSessionConnection]
|
||||||
|
unfilteredConnections = [connections.primary] + connections.alternatives
|
||||||
|
|
||||||
|
if version == "12.0.0" {
|
||||||
|
for connection in unfilteredConnections {
|
||||||
|
if case let .reflector(reflector) = connection {
|
||||||
|
unfilteredConnections.append(.reflector(CallSessionConnection.Reflector(
|
||||||
|
id: 123456,
|
||||||
|
ip: "91.108.9.38",
|
||||||
|
ipv6: "",
|
||||||
|
isTcp: true,
|
||||||
|
port: 595,
|
||||||
|
peerTag: reflector.peerTag
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var reflectorIdList: [Int64] = []
|
var reflectorIdList: [Int64] = []
|
||||||
for connection in unfilteredConnections {
|
for connection in unfilteredConnections {
|
||||||
@ -911,12 +927,18 @@ public final class OngoingCallContext {
|
|||||||
switch connection {
|
switch connection {
|
||||||
case let .reflector(reflector):
|
case let .reflector(reflector):
|
||||||
if reflector.isTcp {
|
if reflector.isTcp {
|
||||||
|
if version == "12.0.0" {
|
||||||
|
/*if signalingReflector == nil {
|
||||||
|
signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag))
|
||||||
|
}*/
|
||||||
|
} else {
|
||||||
if signalingReflector == nil {
|
if signalingReflector == nil {
|
||||||
signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag))
|
signalingReflector = OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag))
|
||||||
}
|
}
|
||||||
|
|
||||||
continue connectionsLoop
|
continue connectionsLoop
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case .webRtcReflector:
|
case .webRtcReflector:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -962,22 +984,37 @@ public final class OngoingCallContext {
|
|||||||
directConnection = nil
|
directConnection = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG && false
|
#if DEBUG && true
|
||||||
var customParameters = customParameters
|
var customParameters = customParameters
|
||||||
if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] {
|
if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] {
|
||||||
var customParametersValue: [String: Any]
|
var customParametersValue: [String: Any]
|
||||||
customParametersValue = initialCustomParameters
|
customParametersValue = initialCustomParameters
|
||||||
customParametersValue["network_standalone_reflectors"] = true as NSNumber
|
if version == "12.0.0" {
|
||||||
customParametersValue["network_use_mtproto"] = true as NSNumber
|
customParametersValue["network_use_tcponly"] = true as NSNumber
|
||||||
customParametersValue["network_skip_initial_ping"] = true as NSNumber
|
|
||||||
customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)!
|
customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)!
|
||||||
|
}
|
||||||
|
|
||||||
if let reflector = filteredConnections.first(where: { $0.username == "reflector" && $0.reflectorId == 1 }) {
|
if let value = customParametersValue["network_use_tcponly"] as? Bool, value {
|
||||||
filteredConnections = [reflector]
|
filteredConnections = filteredConnections.filter { connection in
|
||||||
|
if connection.hasTcp {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
allowP2P = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*#if DEBUG
|
||||||
|
if let initialCustomParameters = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] {
|
||||||
|
var customParametersValue: [String: Any]
|
||||||
|
customParametersValue = initialCustomParameters
|
||||||
|
customParametersValue["network_kcp_experiment"] = true as NSNumber
|
||||||
|
customParameters = String(data: try! JSONSerialization.data(withJSONObject: customParametersValue), encoding: .utf8)!
|
||||||
|
}
|
||||||
|
#endif*/
|
||||||
|
|
||||||
let context = OngoingCallThreadLocalContextWebrtc(
|
let context = OngoingCallThreadLocalContextWebrtc(
|
||||||
version: version,
|
version: version,
|
||||||
customParameters: customParameters,
|
customParameters: customParameters,
|
||||||
|
|||||||
@ -120,12 +120,15 @@ sources = glob([
|
|||||||
"tgcalls/tgcalls/v2/InstanceV2Impl.cpp",
|
"tgcalls/tgcalls/v2/InstanceV2Impl.cpp",
|
||||||
"tgcalls/tgcalls/v2/InstanceV2ReferenceImpl.cpp",
|
"tgcalls/tgcalls/v2/InstanceV2ReferenceImpl.cpp",
|
||||||
"tgcalls/tgcalls/v2/NativeNetworkingImpl.cpp",
|
"tgcalls/tgcalls/v2/NativeNetworkingImpl.cpp",
|
||||||
|
"tgcalls/tgcalls/v2/RawTcpSocket.cpp",
|
||||||
"tgcalls/tgcalls/v2/ReflectorPort.cpp",
|
"tgcalls/tgcalls/v2/ReflectorPort.cpp",
|
||||||
"tgcalls/tgcalls/v2/ReflectorRelayPortFactory.cpp",
|
"tgcalls/tgcalls/v2/ReflectorRelayPortFactory.cpp",
|
||||||
"tgcalls/tgcalls/v2/Signaling.cpp",
|
"tgcalls/tgcalls/v2/Signaling.cpp",
|
||||||
"tgcalls/tgcalls/v2/SignalingConnection.cpp",
|
"tgcalls/tgcalls/v2/SignalingConnection.cpp",
|
||||||
"tgcalls/tgcalls/v2/SignalingEncryption.cpp",
|
"tgcalls/tgcalls/v2/SignalingEncryption.cpp",
|
||||||
"tgcalls/tgcalls/v2/SignalingSctpConnection.cpp",
|
"tgcalls/tgcalls/v2/SignalingSctpConnection.cpp",
|
||||||
|
"tgcalls/tgcalls/v2/SignalingKcpConnection.cpp",
|
||||||
|
"tgcalls/tgcalls/v2/ikcp.cpp",
|
||||||
]
|
]
|
||||||
|
|
||||||
objc_library(
|
objc_library(
|
||||||
|
|||||||
@ -212,7 +212,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
|
|||||||
- (void)setOnIsActiveUpdated:(void (^ _Nonnull)(bool))onIsActiveUpdated;
|
- (void)setOnIsActiveUpdated:(void (^ _Nonnull)(bool))onIsActiveUpdated;
|
||||||
|
|
||||||
#if TARGET_OS_IOS
|
#if TARGET_OS_IOS
|
||||||
- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer rotation:(OngoingCallVideoOrientationWebrtc)rotation;
|
- (void)submitSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer rotation:(OngoingCallVideoOrientationWebrtc)rotation completion:(void (^_Nonnull)())completion;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
- (GroupCallDisposable * _Nonnull)addVideoOutput:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink;
|
- (GroupCallDisposable * _Nonnull)addVideoOutput:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink;
|
||||||
|
|||||||
@ -667,8 +667,11 @@ tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls:
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_OS_IOS
|
#if TARGET_OS_IOS
|
||||||
- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer rotation:(OngoingCallVideoOrientationWebrtc)rotation {
|
- (void)submitSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer rotation:(OngoingCallVideoOrientationWebrtc)rotation completion:(void (^_Nonnull)())completion {
|
||||||
if (!pixelBuffer) {
|
if (!sampleBuffer) {
|
||||||
|
if (completion) {
|
||||||
|
completion();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,19 +691,30 @@ tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isProcessingCustomSampleBuffer.value) {
|
/*if (_isProcessingCustomSampleBuffer.value) {
|
||||||
return;
|
if (completion) {
|
||||||
|
completion();
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}*/
|
||||||
_isProcessingCustomSampleBuffer.value = true;
|
_isProcessingCustomSampleBuffer.value = true;
|
||||||
|
|
||||||
tgcalls::StaticThreads::getThreads()->getMediaThread()->PostTask([interface = _interface, pixelBuffer = CFRetain(pixelBuffer), croppingBuffer = _croppingBuffer, videoRotation = videoRotation, isProcessingCustomSampleBuffer = _isProcessingCustomSampleBuffer]() {
|
void (^capturedCompletion)() = [completion copy];
|
||||||
|
|
||||||
|
tgcalls::StaticThreads::getThreads()->getMediaThread()->PostTask([interface = _interface, sampleBuffer = CFRetain(sampleBuffer), croppingBuffer = _croppingBuffer, videoRotation = videoRotation, isProcessingCustomSampleBuffer = _isProcessingCustomSampleBuffer, capturedCompletion]() {
|
||||||
auto capture = GetVideoCaptureAssumingSameThread(interface.get());
|
auto capture = GetVideoCaptureAssumingSameThread(interface.get());
|
||||||
auto source = capture->source();
|
auto source = capture->source();
|
||||||
if (source) {
|
if (source) {
|
||||||
[CustomExternalCapturer passPixelBuffer:(CVPixelBufferRef)pixelBuffer rotation:videoRotation toSource:source croppingBuffer:*croppingBuffer];
|
CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer((CMSampleBufferRef)sampleBuffer);
|
||||||
|
|
||||||
|
[CustomExternalCapturer passPixelBuffer:pixelBuffer sampleBufferReference:(CMSampleBufferRef)sampleBuffer rotation:videoRotation toSource:source croppingBuffer:*croppingBuffer];
|
||||||
}
|
}
|
||||||
CFRelease(pixelBuffer);
|
CFRelease(sampleBuffer);
|
||||||
isProcessingCustomSampleBuffer.value = false;
|
isProcessingCustomSampleBuffer.value = false;
|
||||||
|
|
||||||
|
if (capturedCompletion) {
|
||||||
|
capturedCompletion();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user