This commit is contained in:
Ali 2023-11-10 10:46:57 +04:00
parent 169fece59c
commit 857bf2190e
48 changed files with 682 additions and 207 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

View File

@ -0,0 +1,63 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "MetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "MetalEngineMetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.MetalEngineMetalSources</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>MetalEngine</string>
"""
)
apple_resource_bundle(
name = "MetalEngineMetalSourcesBundle",
infoplists = [
":MetalEngineMetalSourcesBundleInfoPlist",
],
resources = [
":MetalSources",
],
)
swift_library(
name = "MetalEngine",
module_name = "MetalEngine",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":MetalEngineMetalSourcesBundle",
],
deps = [
"//submodules/Display",
"//submodules/Utils/ShelfPack",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,11 @@
#include <metal_stdlib>
using namespace metal;
vertex float4 clearVertex(const device float2* vertexArray [[ buffer(0) ]], unsigned int vid [[ vertex_id ]]) {
return float4(vertexArray[vid], 0.0, 1.0);
}
fragment half4 clearFragment(const device float4 &color [[ buffer(0) ]]) {
return half4(color);
}

View File

@ -2,15 +2,9 @@ import Foundation
import Metal import Metal
import UIKit import UIKit
import IOSurface import IOSurface
import Display
import ShelfPack import ShelfPack
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
public final class Placeholder<Resolved> { public final class Placeholder<Resolved> {
var contents: Resolved? var contents: Resolved?
} }
@ -110,16 +104,16 @@ public struct RenderLayerPlacement: Equatable {
public protocol RenderToLayerState: AnyObject { public protocol RenderToLayerState: AnyObject {
var pipelineState: MTLRenderPipelineState { get } var pipelineState: MTLRenderPipelineState { get }
init?(device: MTLDevice, library: MTLLibrary) init?(device: MTLDevice)
} }
public protocol ComputeState: AnyObject { public protocol ComputeState: AnyObject {
init?(device: MTLDevice, library: MTLLibrary) init?(device: MTLDevice)
} }
open class MetalSubjectLayer: SimpleLayer { open class MetalEngineSubjectLayer: SimpleLayer {
fileprivate var internalId: Int = -1 fileprivate var internalId: Int = -1
fileprivate var surfaceAllocation: MetalContext.SurfaceAllocation? fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation?
public override init() { public override init() {
super.init() super.init()
@ -136,7 +130,7 @@ open class MetalSubjectLayer: SimpleLayer {
} }
override public func setNeedsDisplay() { override public func setNeedsDisplay() {
if let subject = self as? MetalSubject { if let subject = self as? MetalEngineSubject {
subject.setNeedsUpdate() subject.setNeedsUpdate()
} }
} }
@ -171,7 +165,7 @@ public final class PooledTexture {
} }
} }
public func get(context: MetalSubjectContext) -> TexturePlaceholder? { public func get(context: MetalEngineSubjectContext) -> TexturePlaceholder? {
#if DEBUG #if DEBUG
if context.freeResourcesOnCompletion.contains(where: { $0 === self }) { if context.freeResourcesOnCompletion.contains(where: { $0 === self }) {
assertionFailure("Trying to get PooledTexture more than once per update cycle") assertionFailure("Trying to get PooledTexture more than once per update cycle")
@ -193,7 +187,7 @@ public final class PooledTexture {
} }
} }
public final class MetalSubjectContext { public final class MetalEngineSubjectContext {
fileprivate final class ComputeOperation { fileprivate final class ComputeOperation {
let commands: (MTLCommandBuffer) -> Void let commands: (MTLCommandBuffer) -> Void
@ -205,13 +199,13 @@ public final class MetalSubjectContext {
fileprivate final class RenderToLayerOperation { fileprivate final class RenderToLayerOperation {
let spec: RenderLayerSpec let spec: RenderLayerSpec
let state: RenderToLayerState let state: RenderToLayerState
weak var layer: MetalSubjectLayer? weak var layer: MetalEngineSubjectLayer?
let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
init( init(
spec: RenderLayerSpec, spec: RenderLayerSpec,
state: RenderToLayerState, state: RenderToLayerState,
layer: MetalSubjectLayer, layer: MetalEngineSubjectLayer,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
) { ) {
self.spec = spec self.spec = spec
@ -222,13 +216,13 @@ public final class MetalSubjectContext {
} }
private let device: MTLDevice private let device: MTLDevice
private let impl: MetalContext.Impl private let impl: MetalEngine.Impl
fileprivate var computeOperations: [ComputeOperation] = [] fileprivate var computeOperations: [ComputeOperation] = []
fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:] fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
fileprivate var freeResourcesOnCompletion: [PooledTexture.Texture] = [] fileprivate var freeResourcesOnCompletion: [PooledTexture.Texture] = []
fileprivate init(device: MTLDevice, impl: MetalContext.Impl) { fileprivate init(device: MTLDevice, impl: MetalEngine.Impl) {
self.device = device self.device = device
self.impl = impl self.impl = impl
} }
@ -236,7 +230,7 @@ public final class MetalSubjectContext {
public func renderToLayer<RenderToLayerStateType: RenderToLayerState, each Resolved>( public func renderToLayer<RenderToLayerStateType: RenderToLayerState, each Resolved>(
spec: RenderLayerSpec, spec: RenderLayerSpec,
state: RenderToLayerStateType.Type, state: RenderToLayerStateType.Type,
layer: MetalSubjectLayer, layer: MetalEngineSubjectLayer,
inputs: repeat Placeholder<each Resolved>, inputs: repeat Placeholder<each Resolved>,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement, repeat each Resolved) -> Void commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement, repeat each Resolved) -> Void
) { ) {
@ -245,7 +239,7 @@ public final class MetalSubjectContext {
if let current = self.impl.renderStates[stateTypeId] as? RenderToLayerStateType { if let current = self.impl.renderStates[stateTypeId] as? RenderToLayerStateType {
resolvedState = current resolvedState = current
} else { } else {
guard let value = RenderToLayerStateType(device: self.device, library: self.impl.library) else { guard let value = RenderToLayerStateType(device: self.device) else {
assertionFailure("Could not initialize render state \(state)") assertionFailure("Could not initialize render state \(state)")
return return
} }
@ -278,7 +272,7 @@ public final class MetalSubjectContext {
public func renderToLayer<RenderToLayerStateType: RenderToLayerState>( public func renderToLayer<RenderToLayerStateType: RenderToLayerState>(
spec: RenderLayerSpec, spec: RenderLayerSpec,
state: RenderToLayerStateType.Type, state: RenderToLayerStateType.Type,
layer: MetalSubjectLayer, layer: MetalEngineSubjectLayer,
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
) { ) {
self.renderToLayer(spec: spec, state: state, layer: layer, inputs: noInputPlaceholder, commands: { encoder, placement, _ in self.renderToLayer(spec: spec, state: state, layer: layer, inputs: noInputPlaceholder, commands: { encoder, placement, _ in
@ -296,7 +290,7 @@ public final class MetalSubjectContext {
if let current = self.impl.computeStates[stateTypeId] as? ComputeStateType { if let current = self.impl.computeStates[stateTypeId] as? ComputeStateType {
resolvedState = current resolvedState = current
} else { } else {
guard let value = ComputeStateType(device: self.device, library: self.impl.library) else { guard let value = ComputeStateType(device: self.device) else {
assertionFailure("Could not initialize compute state \(state)") assertionFailure("Could not initialize compute state \(state)")
return Placeholder() return Placeholder()
} }
@ -338,24 +332,24 @@ public final class MetalSubjectContext {
} }
} }
public final class MetalSubjectInternalData { public final class MetalEngineSubjectInternalData {
var internalId: Int = -1 var internalId: Int = -1
var renderSurfaceAllocation: MetalContext.SurfaceAllocation? var renderSurfaceAllocation: MetalEngine.SurfaceAllocation?
init() { init() {
} }
} }
public protocol MetalSubject: AnyObject { public protocol MetalEngineSubject: AnyObject {
var internalData: MetalSubjectInternalData? { get set } var internalData: MetalEngineSubjectInternalData? { get set }
func setNeedsUpdate() func setNeedsUpdate()
func update(context: MetalSubjectContext) func update(context: MetalEngineSubjectContext)
} }
public extension MetalSubject { public extension MetalEngineSubject {
func setNeedsUpdate() { func setNeedsUpdate() {
MetalContext.shared.impl.addSubjectNeedsUpdate(subject: self) MetalEngine.shared.impl.addSubjectNeedsUpdate(subject: self)
} }
} }
@ -370,7 +364,7 @@ private final class MetalEventLayer: CAMetalLayer {
} }
} }
public final class MetalContext { public final class MetalEngine {
struct SurfaceAllocation { struct SurfaceAllocation {
struct Phase { struct Phase {
let subRect: CGRect let subRect: CGRect
@ -509,9 +503,9 @@ public final class MetalContext {
} }
private final class SubjectReference { private final class SubjectReference {
weak var subject: MetalSubject? weak var subject: MetalEngineSubject?
init(subject: MetalSubject) { init(subject: MetalEngineSubject) {
self.subject = subject self.subject = subject
} }
} }
@ -548,7 +542,7 @@ public final class MetalContext {
init?(device: MTLDevice) { init?(device: MTLDevice) {
let mainBundle = Bundle(for: Impl.self) let mainBundle = Bundle(for: Impl.self)
guard let path = mainBundle.path(forResource: "MetalSourcesBundle", ofType: "bundle") else { guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else {
return nil return nil
} }
guard let bundle = Bundle(path: path) else { guard let bundle = Bundle(path: path) else {
@ -624,15 +618,15 @@ public final class MetalContext {
let surfaceId = self.nextSurfaceId let surfaceId = self.nextSurfaceId
self.nextSurfaceId += 1 self.nextSurfaceId += 1
let surfaceWidth = max(1024, alignUp(size: Int(minSize.width), align: 64)) let surfaceWidth = max(1024, alignUp(Int(minSize.width), alignment: 64))
let surfaceHeight = max(512, alignUp(size: Int(minSize.height), align: 64)) let surfaceHeight = max(512, alignUp(Int(minSize.height), alignment: 64))
let surface = Surface(id: surfaceId, device: self.device, width: surfaceWidth, height: surfaceHeight) let surface = Surface(id: surfaceId, device: self.device, width: surfaceWidth, height: surfaceHeight)
self.surfaces[surfaceId] = surface self.surfaces[surfaceId] = surface
return surface return surface
} }
private func refreshLayerAllocation(layer: MetalSubjectLayer, renderSpec: RenderLayerSpec) { private func refreshLayerAllocation(layer: MetalEngineSubjectLayer, renderSpec: RenderLayerSpec) {
var previousSurfaceId: Int? var previousSurfaceId: Int?
var updatedSurfaceId: Int? var updatedSurfaceId: Int?
@ -689,12 +683,12 @@ public final class MetalContext {
} }
} }
func addSubjectNeedsUpdate(subject: MetalSubject) { func addSubjectNeedsUpdate(subject: MetalEngineSubject) {
let internalData: MetalSubjectInternalData let internalData: MetalEngineSubjectInternalData
if let current = subject.internalData { if let current = subject.internalData {
internalData = current internalData = current
} else { } else {
internalData = MetalSubjectInternalData() internalData = MetalEngineSubjectInternalData()
subject.internalData = internalData subject.internalData = internalData
} }
@ -751,7 +745,7 @@ public final class MetalContext {
return return
} }
let subjectContext = MetalSubjectContext(device: device, impl: self) let subjectContext = MetalEngineSubjectContext(device: device, impl: self)
for subjectReference in self.updatedSubjects { for subjectReference in self.updatedSubjects {
guard let subject = subjectReference.subject else { guard let subject = subjectReference.subject else {
@ -899,7 +893,7 @@ public final class MetalContext {
} }
} }
public static let shared = MetalContext() public static let shared = MetalEngine()
fileprivate let impl: Impl fileprivate let impl: Impl

View File

@ -105,6 +105,7 @@ swift_library(
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen", "//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
"//submodules/PhoneNumberFormat:PhoneNumberFormat", "//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/TelegramUI/Components/Calls/CallScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -135,11 +135,11 @@ public final class CallController: ViewController {
} }
override public func loadDisplayNode() { override public func loadDisplayNode() {
if self.call.isVideoPossible { #if DEBUG
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
} else { #else
self.displayNode = LegacyCallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
} #endif
self.displayNodeDidLoad() self.displayNodeDidLoad()
self.controllerNode.toggleMute = { [weak self] in self.controllerNode.toggleMute = { [weak self] in

View File

@ -0,0 +1,91 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import TelegramAudio
import AccountContext
import TelegramPresentationData
import SwiftSignalKit
import CallScreen
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
private let sharedContext: SharedAccountContext
private let account: Account
private let presentationData: PresentationData
private let statusBar: StatusBar
private let call: PresentationCall
private let callScreen: PrivateCallScreen
var isMuted: Bool = false
var toggleMute: (() -> Void)?
var setCurrentAudioOutput: ((AudioSessionOutput) -> Void)?
var beginAudioOuputSelection: ((Bool) -> Void)?
var acceptCall: (() -> Void)?
var endCall: (() -> Void)?
var back: (() -> Void)?
var presentCallRating: ((CallId, Bool) -> Void)?
var present: ((ViewController) -> Void)?
var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)?
var dismissAllTooltips: (() -> Void)?
init(
sharedContext: SharedAccountContext,
account: Account,
presentationData: PresentationData,
statusBar: StatusBar,
debugInfo: Signal<(String, String), NoError>,
shouldStayHiddenUntilConnection: Bool = false,
easyDebugAccess: Bool,
call: PresentationCall
) {
self.sharedContext = sharedContext
self.account = account
self.presentationData = presentationData
self.statusBar = statusBar
self.call = call
self.callScreen = PrivateCallScreen()
super.init()
self.view.addSubview(self.callScreen)
}
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
}
func updateCallState(_ callState: PresentationCallState) {
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
if reportRating {
self.presentCallRating?(callId, self.call.isVideo)
}
self.callEnded?(reportRating)
}
}
func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) {
}
func animateIn() {
}
func animateOut(completion: @escaping () -> Void) {
}
func expandFromPipIfPossible() {
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size))
self.callScreen.update(size: layout.size, insets: layout.insets(options: [.statusBar]))
}
}

View File

@ -410,6 +410,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
"//submodules/TelegramUI/Components/ContextMenuScreen", "//submodules/TelegramUI/Components/ContextMenuScreen",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen", "//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/MetalEngine",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

@ -10,7 +10,7 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
) )
filegroup( filegroup(
name = "MetalSources", name = "CallScreenMetalSources",
srcs = glob([ srcs = glob([
"Metal/**/*.metal", "Metal/**/*.metal",
]), ]),
@ -18,7 +18,7 @@ filegroup(
) )
plist_fragment( plist_fragment(
name = "MetalSourcesBundleInfoPlist", name = "CallScreenMetalSourcesBundleInfoPlist",
extension = "plist", extension = "plist",
template = template =
""" """
@ -27,20 +27,26 @@ plist_fragment(
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>AnimationCompression</string> <string>CallScreen</string>
""" """
) )
apple_resource_bundle( apple_resource_bundle(
name = "MetalSourcesBundle", name = "CallScreenMetalSourcesBundle",
infoplists = [ infoplists = [
":MetalSourcesBundleInfoPlist", ":CallScreenMetalSourcesBundleInfoPlist",
], ],
resources = [ resources = [
":MetalSources", ":CallScreenMetalSources",
], ],
) )
filegroup(
name = "Assets",
srcs = glob(["CallScreenAssets.xcassets/**"]),
visibility = ["//visibility:public"],
)
swift_library( swift_library(
name = "CallScreen", name = "CallScreen",
module_name = "CallScreen", module_name = "CallScreen",
@ -51,13 +57,14 @@ swift_library(
"-warnings-as-errors", "-warnings-as-errors",
], ],
data = [ data = [
":MetalSourcesBundle", ":CallScreenMetalSourcesBundle",
":Assets",
], ],
deps = [ deps = [
"//submodules/Display", "//submodules/Display",
"//submodules/MetalEngine",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Utils/ShelfPack",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audioairpods.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audioairpodspro.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_airpodsmax.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audiobt.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "CallCancelIcon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "CallCancelIcon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_flip (1).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_microphone.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_speaker.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_camera.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -21,14 +21,6 @@ struct QuadVertexOut {
float2 uv; float2 uv;
}; };
vertex float4 clearVertex(const device float2* vertexArray [[ buffer(0) ]], unsigned int vid [[ vertex_id ]]) {
return float4(vertexArray[vid], 0.0, 1.0);
}
fragment half4 clearFragment(const device float4 &color [[ buffer(0) ]]) {
return half4(color);
}
vertex QuadVertexOut callBackgroundVertex( vertex QuadVertexOut callBackgroundVertex(
const device Rectangle &rect [[ buffer(0) ]], const device Rectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]] unsigned int vid [[ vertex_id ]]

View File

@ -147,6 +147,18 @@ public final class AnimatedProperty<T: AnimationInterpolatable>: AnyAnimatedProp
self.didStartAnimation?() self.didStartAnimation?()
} }
public func animate(from: T, to: T, duration: Double, curve: AnimationCurve) {
self.value = from
self.animation = Animation(from: from, to: to, duration: duration, curve: curve)
self.animation?.start()
self.didStartAnimation?()
}
public func set(to: T) {
self.animation = nil
self.value = to
}
override public func update() { override public func update() {
if let animation = self.animation { if let animation = self.animation {
self.value = animation.update(at: CACurrentMediaTime()) self.value = animation.update(at: CACurrentMediaTime())

View File

@ -1,4 +1,28 @@
import Foundation import Foundation
import UIKit import UIKit
import Display import Display
import MetalKit
private final class BundleMarker: NSObject {
}
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: BundleMarker.self)
guard let path = mainBundle.path(forResource: "CallScreenMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
final class AvatarLayer: SimpleLayer { final class AvatarLayer: SimpleLayer {
var image: UIImage? { var image: UIImage? {

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
final class ButtonGroupView: UIView, ContentOverlayView { final class ButtonGroupView: UIView, ContentOverlayView {
enum Key: Hashable { enum Key: Hashable {
@ -12,6 +13,7 @@ final class ButtonGroupView: UIView, ContentOverlayView {
let overlayMaskLayer: CALayer let overlayMaskLayer: CALayer
private var buttons: [Key: ContentOverlayButton] = [:] private var buttons: [Key: ContentOverlayButton] = [:]
var audioPressed: (() -> Void)?
var toggleVideo: (() -> Void)? var toggleVideo: (() -> Void)?
override init(frame: CGRect) { override init(frame: CGRect) {
@ -89,9 +91,15 @@ final class ButtonGroupView: UIView, ContentOverlayView {
let image: UIImage? let image: UIImage?
switch key { switch key {
case .audio: case .audio:
image = UIImage(named: "Call/ButtonSpeaker") image = UIImage(named: "Call/Speaker")
button.action = { [weak self] in
guard let self else {
return
}
self.audioPressed?()
}
case .video: case .video:
image = UIImage(named: "Call/ButtonVideo") image = UIImage(named: "Call/Video")
button.action = { [weak self] in button.action = { [weak self] in
guard let self else { guard let self else {
return return
@ -99,9 +107,9 @@ final class ButtonGroupView: UIView, ContentOverlayView {
self.toggleVideo?() self.toggleVideo?()
} }
case .mic: case .mic:
image = UIImage(named: "Call/ButtonMicMuted") image = UIImage(named: "Call/Mute")
case .close: case .close:
image = UIImage(named: "Call/ButtonEnd") image = UIImage(named: "Call/End")
} }
button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal) button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal)

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import MetalKit import MetalKit
import UIKit import UIKit
import MetalEngine
private func shiftArray(array: [SIMD2<Float>], offset: Int) -> [SIMD2<Float>] { private func shiftArray(array: [SIMD2<Float>], offset: Int) -> [SIMD2<Float>] {
var newArray = array var newArray = array
@ -28,13 +29,28 @@ private func hexToFloat(_ hex: Int) -> SIMD4<Float> {
return SIMD4<Float>(x: red, y: green, z: blue, w: 1.0) return SIMD4<Float>(x: red, y: green, z: blue, w: 1.0)
} }
final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject { private struct ColorSet: Equatable, AnimationInterpolatable {
var internalData: MetalSubjectInternalData? static let animationInterpolator = AnimationInterpolator<ColorSet> { from, to, fraction in
var result: [SIMD4<Float>] = []
for i in 0 ..< min(from.colors.count, to.colors.count) {
result.append(from.colors[i] * Float(1.0 - fraction) + to.colors[i] * Float(fraction))
}
return ColorSet(colors: result)
}
var colors: [SIMD4<Float>]
}
final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
final class RenderState: RenderToLayerState { final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState let pipelineState: MTLRenderPipelineState
init?(device: MTLDevice, library: MTLLibrary) { init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "callBackgroundVertex"), let fragmentFunction = library.makeFunction(name: "callBackgroundFragment") else { guard let vertexFunction = library.makeFunction(name: "callBackgroundVertex"), let fragmentFunction = library.makeFunction(name: "callBackgroundFragment") else {
return nil return nil
} }
@ -66,11 +82,44 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
private var displayLinkSubscription: SharedDisplayLink.Subscription? private var displayLinkSubscription: SharedDisplayLink.Subscription?
var renderSpec: RenderLayerSpec? var renderSpec: RenderLayerSpec? {
didSet {
if self.renderSpec != oldValue {
self.setNeedsUpdate()
}
}
}
private let colorSets: [ColorSet]
private let colorTransition: AnimatedProperty<ColorSet>
private var stateIndex: Int = 0
private let phaseAcceleration = AnimatedProperty<CGFloat>(0.0)
init(isBlur: Bool) { init(isBlur: Bool) {
self.isBlur = isBlur self.isBlur = isBlur
self.colorSets = [
ColorSet(colors: [
hexToFloat(0x568FD6),
hexToFloat(0x626ED5),
hexToFloat(0xA667D5),
hexToFloat(0x7664DA)
]),
ColorSet(colors: [
hexToFloat(0xACBD65),
hexToFloat(0x459F8D),
hexToFloat(0x53A4D1),
hexToFloat(0x3E917A)
]),
ColorSet(colors: [
hexToFloat(0xC0508D),
hexToFloat(0xF09536),
hexToFloat(0xCE5081),
hexToFloat(0xFC7C4C)
])
]
self.colorTransition = AnimatedProperty<ColorSet>(colorSets[0])
super.init() super.init()
self.didEnterHierarchy = { [weak self] in self.didEnterHierarchy = { [weak self] in
@ -81,8 +130,14 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
guard let self else { guard let self else {
return return
} }
self.colorTransition.update()
self.phaseAcceleration.update()
let stepCount = 8 let stepCount = 8
self.phase = (self.phase + 1.0 / 60.0).truncatingRemainder(dividingBy: Float(stepCount)) var phaseStep: CGFloat = 0.5 / 30.0
phaseStep += phaseStep * self.phaseAcceleration.value * 0.5
self.phase = (self.phase + Float(phaseStep)).truncatingRemainder(dividingBy: Float(stepCount))
self.setNeedsUpdate() self.setNeedsUpdate()
}) })
} }
@ -95,6 +150,9 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
} }
override init(layer: Any) { override init(layer: Any) {
self.colorSets = []
self.colorTransition = AnimatedProperty<ColorSet>(ColorSet(colors: []))
super.init(layer: layer) super.init(layer: layer)
} }
@ -102,7 +160,19 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func update(context: MetalSubjectContext) { func update(stateIndex: Int, animated: Bool) {
if self.stateIndex != stateIndex {
self.stateIndex = stateIndex
if animated {
self.phaseAcceleration.animate(from: 1.0, to: 0.0, duration: 2.0, curve: .easeInOut)
self.colorTransition.animate(to: self.colorSets[stateIndex % self.colorSets.count], duration: 0.3, curve: .easeInOut)
} else {
self.colorTransition.set(to: self.colorSets[stateIndex % self.colorSets.count])
}
}
}
func update(context: MetalEngineSubjectContext) {
guard let renderSpec = self.renderSpec else { guard let renderSpec = self.renderSpec else {
return return
} }
@ -127,22 +197,9 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
} }
encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout<SIMD2<Float>>.size, index: 0) encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout<SIMD2<Float>>.size, index: 0)
var colors: [[SIMD4<Float>]] = [ var colors: [SIMD4<Float>] = self.colorTransition.value.colors
[
hexToFloat(0x568FD6),
hexToFloat(0x626ED5),
hexToFloat(0xA667D5),
hexToFloat(0x7664DA)
],
[
hexToFloat(0xACBD65),
hexToFloat(0x459F8D),
hexToFloat(0x53A4D1),
hexToFloat(0x3E917A)
]
]
encoder.setFragmentBytes(&colors[0], length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1) encoder.setFragmentBytes(&colors, length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
var brightness: Float = isBlur ? 1.1 : 1.0 var brightness: Float = isBlur ? 1.1 : 1.0
var saturation: Float = isBlur ? 1.2 : 1.0 var saturation: Float = isBlur ? 1.2 : 1.0
encoder.setFragmentBytes(&brightness, length: 4, index: 2) encoder.setFragmentBytes(&brightness, length: 4, index: 2)

View File

@ -1,8 +1,9 @@
import Foundation import Foundation
import MetalKit import MetalKit
import MetalEngine
final class CallBlobsLayer: MetalSubjectLayer, MetalSubject { final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalSubjectInternalData? var internalData: MetalEngineSubjectInternalData?
struct Blob { struct Blob {
var points: [Float] var points: [Float]
@ -48,10 +49,10 @@ final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
let pipelineState: MTLRenderPipelineState let pipelineState: MTLRenderPipelineState
required init?( required init?(device: MTLDevice) {
device: MTLDevice, guard let library = metalLibrary(device: device) else {
library: MTLLibrary return nil
) { }
guard let vertexFunction = library.makeFunction(name: "callBlobVertex"), let fragmentFunction = library.makeFunction(name: "callBlobFragment") else { guard let vertexFunction = library.makeFunction(name: "callBlobVertex"), let fragmentFunction = library.makeFunction(name: "callBlobFragment") else {
return nil return nil
} }
@ -123,7 +124,7 @@ final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func update(context: MetalSubjectContext) { func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty { if self.bounds.isEmpty {
return return
} }

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
final class ContentOverlayButton: UIButton, ContentOverlayView { final class ContentOverlayButton: UIButton, ContentOverlayView {
var overlayMaskLayer: CALayer { var overlayMaskLayer: CALayer {

View File

@ -1,7 +1,21 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
import MetalEngine
final class ContentView: UIView { final class ContentView: UIView {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var state: PrivateCallScreen.State
init(size: CGSize, insets: UIEdgeInsets, state: PrivateCallScreen.State) {
self.size = size
self.insets = insets
self.state = state
}
}
private let blobLayer: CallBlobsLayer private let blobLayer: CallBlobsLayer
private let avatarLayer: AvatarLayer private let avatarLayer: AvatarLayer
private let titleView: TextView private let titleView: TextView
@ -14,8 +28,7 @@ final class ContentView: UIView {
private var videoLayerMask: SimpleShapeLayer? private var videoLayerMask: SimpleShapeLayer?
private var blurredVideoLayerMask: SimpleShapeLayer? private var blurredVideoLayerMask: SimpleShapeLayer?
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)? private var params: Params?
private var phase: CGFloat = 0.0
private var isDisplayingVideo: Bool = false private var isDisplayingVideo: Bool = false
private var videoDisplayFraction = AnimatedProperty<CGFloat>(0.0) private var videoDisplayFraction = AnimatedProperty<CGFloat>(0.0)
@ -54,8 +67,8 @@ final class ContentView: UIView {
guard let self else { guard let self else {
return return
} }
if let currentLayout = self.currentLayout { if let params = self.params {
self.update(size: currentLayout.size, insets: currentLayout.insets) self.updateInternal(params: params)
} }
} }
} }
@ -64,41 +77,59 @@ final class ContentView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func update(size: CGSize, insets: UIEdgeInsets) { func update(size: CGSize, insets: UIEdgeInsets, state: PrivateCallScreen.State) {
self.currentLayout = (size, insets) let params = Params(size: size, insets: insets, state: state)
if self.params == params {
return
}
self.params = params
self.updateInternal(params: params)
}
private func updateInternal(params: Params) {
if self.emojiView == nil { if self.emojiView == nil {
let emojiView = KeyEmojiView(emoji: ["🐱", "🚂", "❄️", "🎨"]) let emojiView = KeyEmojiView(emoji: ["🐱", "🚂", "❄️", "🎨"])
self.emojiView = emojiView self.emojiView = emojiView
self.addSubview(emojiView) self.addSubview(emojiView)
} }
if let emojiView = self.emojiView { if let emojiView = self.emojiView {
emojiView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - emojiView.size.width, y: insets.top + 27.0), size: emojiView.size) emojiView.frame = CGRect(origin: CGPoint(x: params.size.width - 12.0 - emojiView.size.width, y: params.insets.top + 27.0), size: emojiView.size)
} }
if self.videoInput == nil, let url = Bundle.main.url(forResource: "test2", withExtension: "mp4") { if self.videoInput == nil, let url = Bundle.main.url(forResource: "test2", withExtension: "mp4") {
self.videoInput = VideoInput(device: MetalContext.shared.device, url: url) self.videoInput = VideoInput(device: MetalEngine.shared.device, url: url)
self.videoLayer.video = self.videoInput self.videoLayer.video = self.videoInput
} }
self.phase += 3.0 / 60.0 //self.phase += 3.0 / 60.0
self.phase = self.phase.truncatingRemainder(dividingBy: 1.0) //self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
var avatarScale: CGFloat = 0.05 * sin(CGFloat(self.phase) * CGFloat.pi) var avatarScale: CGFloat = 0.05 * sin(CGFloat(0.0) * CGFloat.pi)
avatarScale *= 1.0 - self.videoDisplayFraction.value avatarScale *= 1.0 - self.videoDisplayFraction.value
let avatarSize: CGFloat = 136.0 let avatarSize: CGFloat = 136.0
let blobSize: CGFloat = 176.0 let blobSize: CGFloat = 176.0
let expandedVideoRadius: CGFloat = sqrt(pow(size.width * 0.5, 2.0) + pow(size.height * 0.5, 2.0)) let expandedVideoRadius: CGFloat = sqrt(pow(params.size.width * 0.5, 2.0) + pow(params.size.height * 0.5, 2.0))
let avatarFrame = CGRect(origin: CGPoint(x: floor((size.width - avatarSize) * 0.5), y: CGFloat.animationInterpolator.interpolate(from: 222.0, to: floor((size.height - avatarSize) * 0.5), fraction: self.videoDisplayFraction.value)), size: CGSize(width: avatarSize, height: avatarSize)) let avatarFrame = CGRect(origin: CGPoint(x: floor((params.size.width - avatarSize) * 0.5), y: CGFloat.animationInterpolator.interpolate(from: 222.0, to: floor((params.size.height - avatarSize) * 0.5), fraction: self.videoDisplayFraction.value)), size: CGSize(width: avatarSize, height: avatarSize))
let titleSize = self.titleView.update(string: "Emma Walters", fontSize: CGFloat.animationInterpolator.interpolate(from: 28.0, to: 17.0, fraction: self.videoDisplayFraction.value), fontWeight: CGFloat.animationInterpolator.interpolate(from: 0.0, to: 0.25, fraction: self.videoDisplayFraction.value), constrainedWidth: size.width - 16.0 * 2.0) let titleSize = self.titleView.update(string: "Emma Walters", fontSize: CGFloat.animationInterpolator.interpolate(from: 28.0, to: 17.0, fraction: self.videoDisplayFraction.value), fontWeight: CGFloat.animationInterpolator.interpolate(from: 0.0, to: 0.25, fraction: self.videoDisplayFraction.value), constrainedWidth: params.size.width - 16.0 * 2.0)
let titleFrame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) * 0.5, y: CGFloat.animationInterpolator.interpolate(from: avatarFrame.maxY + 39.0, to: insets.top + 17.0, fraction: self.videoDisplayFraction.value)), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: (params.size.width - titleSize.width) * 0.5, y: CGFloat.animationInterpolator.interpolate(from: avatarFrame.maxY + 39.0, to: params.insets.top + 17.0, fraction: self.videoDisplayFraction.value)), size: titleSize)
self.titleView.frame = titleFrame self.titleView.frame = titleFrame
let statusSize = self.statusView.update(state: .waiting(.requesting)) let statusState: StatusView.State
self.statusView.frame = CGRect(origin: CGPoint(x: (size.width - statusSize.width) * 0.5, y: titleFrame.maxY + CGFloat.animationInterpolator.interpolate(from: 4.0, to: 0.0, fraction: self.videoDisplayFraction.value)), size: statusSize) switch params.state.lifecycleState {
case .connecting:
statusState = .waiting(.requesting)
case .ringing:
statusState = .waiting(.ringing)
case .exchangingKeys:
statusState = .waiting(.generatingKeys)
case let .active(_, signalInfo):
statusState = .active(StatusView.ActiveState(signalStrength: signalInfo.quality))
}
let statusSize = self.statusView.update(state: statusState)
self.statusView.frame = CGRect(origin: CGPoint(x: (params.size.width - statusSize.width) * 0.5, y: titleFrame.maxY + CGFloat.animationInterpolator.interpolate(from: 4.0, to: 0.0, fraction: self.videoDisplayFraction.value)), size: statusSize)
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize)) let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
@ -114,7 +145,7 @@ final class ContentView: UIView {
self.blobLayer.transform = CATransform3DMakeScale(1.0 + avatarScale * 2.0, 1.0 + avatarScale * 2.0, 1.0) self.blobLayer.transform = CATransform3DMakeScale(1.0 + avatarScale * 2.0, 1.0 + avatarScale * 2.0, 1.0)
let videoResolution = CGSize(width: 400.0, height: 400.0) let videoResolution = CGSize(width: 400.0, height: 400.0)
let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: params.size)
let videoRenderingSize = CGSize(width: videoResolution.width * 2.0, height: videoResolution.height * 2.0) let videoRenderingSize = CGSize(width: videoResolution.width * 2.0, height: videoResolution.height * 2.0)

View File

@ -3,6 +3,7 @@ import UIKit
import MetalKit import MetalKit
import MetalPerformanceShaders import MetalPerformanceShaders
import Accelerate import Accelerate
import MetalEngine
func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? { func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
guard let cgImage = image.cgImage, let data = cgImage.dataProvider?.data, let bytes = CFDataGetBytePtr(data), let colorSpace = cgImage.colorSpace, case .rgb = colorSpace.model, cgImage.bitsPerPixel / cgImage.bitsPerComponent == 4 else { guard let cgImage = image.cgImage, let data = cgImage.dataProvider?.data, let bytes = CFDataGetBytePtr(data), let colorSpace = cgImage.colorSpace, case .rgb = colorSpace.model, cgImage.bitsPerPixel / cgImage.bitsPerComponent == 4 else {
@ -36,10 +37,10 @@ func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
return pixelBuffer return pixelBuffer
} }
final class MainVideoLayer: MetalSubjectLayer, MetalSubject { final class MainVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalSubjectInternalData? var internalData: MetalEngineSubjectInternalData?
let blurredLayer: MetalSubjectLayer let blurredLayer: MetalEngineSubjectLayer
final class BlurState: ComputeState { final class BlurState: ComputeState {
let computePipelineStateYUVToRGBA: MTLComputePipelineState let computePipelineStateYUVToRGBA: MTLComputePipelineState
@ -47,10 +48,10 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
let computePipelineStateVertical: MTLComputePipelineState let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale let downscaleKernel: MPSImageBilinearScale
required init?( required init?(device: MTLDevice) {
device: MTLDevice, guard let library = metalLibrary(device: device) else {
library: MTLLibrary return nil
) { }
guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else { guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
return nil return nil
} }
@ -79,10 +80,10 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
final class RenderState: RenderToLayerState { final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState let pipelineState: MTLRenderPipelineState
required init?( required init?(device: MTLDevice) {
device: MTLDevice, guard let library = metalLibrary(device: device) else {
library: MTLLibrary return nil
) { }
guard let vertexFunction = library.makeFunction(name: "mainVideoVertex"), let fragmentFunction = library.makeFunction(name: "mainVideoFragment") else { guard let vertexFunction = library.makeFunction(name: "mainVideoVertex"), let fragmentFunction = library.makeFunction(name: "mainVideoFragment") else {
return nil return nil
} }
@ -115,13 +116,13 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
private var blurredVerticalTexture: PooledTexture? private var blurredVerticalTexture: PooledTexture?
override init() { override init() {
self.blurredLayer = MetalSubjectLayer() self.blurredLayer = MetalEngineSubjectLayer()
super.init() super.init()
} }
override init(layer: Any) { override init(layer: Any) {
self.blurredLayer = MetalSubjectLayer() self.blurredLayer = MetalEngineSubjectLayer()
super.init(layer: layer) super.init(layer: layer)
} }
@ -130,7 +131,7 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func update(context: MetalSubjectContext) { func update(context: MetalEngineSubjectContext) {
if self.isHidden { if self.isHidden {
return return
} }
@ -143,16 +144,16 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized) let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized)
if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec { if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalContext.shared.pooledTexture(spec: rgbaTextureSpec) self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec)
} }
if self.downscaledTexture == nil { if self.downscaledTexture == nil {
self.downscaledTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized)) self.downscaledTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
} }
if self.blurredHorizontalTexture == nil { if self.blurredHorizontalTexture == nil {
self.blurredHorizontalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized)) self.blurredHorizontalTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
} }
if self.blurredVerticalTexture == nil { if self.blurredVerticalTexture == nil {
self.blurredVerticalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized)) self.blurredVerticalTexture = MetalEngine.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
} }
guard let rgbaTexture = self.rgbaTexture?.get(context: context) else { guard let rgbaTexture = self.rgbaTexture?.get(context: context) else {

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
private final class AnimatedDotsLayer: SimpleLayer { private final class AnimatedDotsLayer: SimpleLayer {
private let dotLayers: [SimpleLayer] private let dotLayers: [SimpleLayer]
@ -76,8 +77,18 @@ final class StatusView: UIView {
case ringing case ringing
case generatingKeys case generatingKeys
} }
struct ActiveState {
var signalStrength: Double
init(signalStrength: Double) {
self.signalStrength = signalStrength
}
}
enum State { enum State {
case waiting(WaitingState) case waiting(WaitingState)
case active(ActiveState)
} }
private var textView: TextView private var textView: TextView
@ -110,6 +121,9 @@ final class StatusView: UIView {
case .generatingKeys: case .generatingKeys:
textString = "Exchanging encryption keys" textString = "Exchanging encryption keys"
} }
case let .active(activeState):
textString = "0:00"
let _ = activeState
} }
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 200.0) let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 200.0)
self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize) self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
final class ContentOverlayLayer: SimpleLayer { final class ContentOverlayLayer: SimpleLayer {
private struct Params: Equatable { private struct Params: Equatable {

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
final class MirroringLayer: SimpleLayer { final class MirroringLayer: SimpleLayer {
var targetLayer: SimpleLayer? var targetLayer: SimpleLayer?

View File

@ -1,7 +1,42 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
import MetalEngine
public final class PrivateCallScreen: UIView { public final class PrivateCallScreen: UIView {
public struct State: Equatable {
public struct SignalInfo: Equatable {
public var quality: Double
public init(quality: Double) {
self.quality = quality
}
}
public enum LifecycleState: Equatable {
case connecting
case ringing
case exchangingKeys
case active(startTime: Double, signalInfo: SignalInfo)
}
public var lifecycleState: LifecycleState
public init(lifecycleState: LifecycleState) {
self.lifecycleState = lifecycleState
}
}
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
init(size: CGSize, insets: UIEdgeInsets) {
self.size = size
self.insets = insets
}
}
private let backgroundLayer: CallBackgroundLayer private let backgroundLayer: CallBackgroundLayer
private let contentOverlayLayer: ContentOverlayLayer private let contentOverlayLayer: ContentOverlayLayer
private let contentOverlayContainer: ContentOverlayContainer private let contentOverlayContainer: ContentOverlayContainer
@ -13,6 +48,18 @@ public final class PrivateCallScreen: UIView {
private let buttonGroupView: ButtonGroupView private let buttonGroupView: ButtonGroupView
public var state: State = State(lifecycleState: .connecting) {
didSet {
if self.state != oldValue {
if let params = self.params {
self.updateInternal(params: params, animated: true)
}
}
}
}
private var params: Params?
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.blurContentsLayer = SimpleLayer() self.blurContentsLayer = SimpleLayer()
@ -44,6 +91,26 @@ public final class PrivateCallScreen: UIView {
self.contentOverlayContainer.addSubview(self.buttonGroupView) self.contentOverlayContainer.addSubview(self.buttonGroupView)
self.buttonGroupView.audioPressed = { [weak self] in
guard let self else {
return
}
var state = self.state
switch state.lifecycleState {
case .connecting, .ringing, .exchangingKeys:
state.lifecycleState = .active(startTime: CFAbsoluteTimeGetCurrent(), signalInfo: State.SignalInfo(quality: 1.0))
case let .active(startTime, signalInfo):
if signalInfo.quality == 1.0 {
state.lifecycleState = .active(startTime: startTime, signalInfo: State.SignalInfo(quality: 0.2))
} else if signalInfo.quality == 0.2 {
state.lifecycleState = .connecting
}
}
self.state = state
}
self.buttonGroupView.toggleVideo = { [weak self] in self.buttonGroupView.toggleVideo = { [weak self] in
guard let self else { guard let self else {
return return
@ -65,9 +132,18 @@ public final class PrivateCallScreen: UIView {
} }
public func update(size: CGSize, insets: UIEdgeInsets) { public func update(size: CGSize, insets: UIEdgeInsets) {
let backgroundFrame = CGRect(origin: CGPoint(), size: size) let params = Params(size: size, insets: insets)
if self.params == params {
return
}
self.params = params
self.updateInternal(params: params, animated: false)
}
let aspect: CGFloat = size.width / size.height private func updateInternal(params: Params, animated: Bool) {
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
let aspect: CGFloat = params.size.width / params.size.height
let sizeNorm: CGFloat = 64.0 let sizeNorm: CGFloat = 64.0
let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm) let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
let edgeSize: Int = 2 let edgeSize: Int = 2
@ -77,18 +153,36 @@ public final class PrivateCallScreen: UIView {
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2)) self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
self.backgroundLayer.frame = visualBackgroundFrame self.backgroundLayer.frame = visualBackgroundFrame
self.contentOverlayLayer.frame = CGRect(origin: CGPoint(), size: size) let backgroundStateIndex: Int
self.contentOverlayLayer.update(size: size, contentInsets: UIEdgeInsets()) switch self.state.lifecycleState {
case .connecting:
backgroundStateIndex = 0
case .ringing:
backgroundStateIndex = 0
case .exchangingKeys:
backgroundStateIndex = 0
case let .active(_, signalInfo):
if signalInfo.quality <= 0.2 {
backgroundStateIndex = 2
} else {
backgroundStateIndex = 1
}
}
self.backgroundLayer.update(stateIndex: backgroundStateIndex, animated: animated)
self.contentOverlayContainer.frame = CGRect(origin: CGPoint(), size: size) self.contentOverlayLayer.frame = CGRect(origin: CGPoint(), size: params.size)
self.contentOverlayLayer.update(size: params.size, contentInsets: UIEdgeInsets())
self.contentOverlayContainer.frame = CGRect(origin: CGPoint(), size: params.size)
self.blurBackgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2)) self.blurBackgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
self.blurBackgroundLayer.frame = visualBackgroundFrame self.blurBackgroundLayer.frame = visualBackgroundFrame
self.blurBackgroundLayer.update(stateIndex: backgroundStateIndex, animated: animated)
self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: size) self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: params.size)
self.buttonGroupView.update(size: size) self.buttonGroupView.update(size: params.size)
self.contentView.frame = CGRect(origin: CGPoint(), size: size) self.contentView.frame = CGRect(origin: CGPoint(), size: params.size)
self.contentView.update(size: size, insets: insets) self.contentView.update(size: params.size, insets: params.insets, state: self.state)
} }
} }

View File

@ -1,67 +0,0 @@
import Foundation
import UIKit
public final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
public let nullAction = NullActionClass()
open class SimpleLayer: CALayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
open class SimpleShapeLayer: CAShapeLayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public private(set) var isInHierarchy: Bool = false
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
override public init() {
super.init()
}
override public init(layer: Any) {
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -41,6 +41,7 @@ import DeviceProximity
import MediaEditor import MediaEditor
import TelegramUIDeclareEncodables import TelegramUIDeclareEncodables
import ContextMenuScreen import ContextMenuScreen
import MetalEngine
#if canImport(AppCenter) #if canImport(AppCenter)
import AppCenter import AppCenter
@ -341,6 +342,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.window = window self.window = window
self.nativeWindow = window self.nativeWindow = window
hostView.containerView.layer.addSublayer(MetalEngine.shared.rootLayer)
if !UIDevice.current.isBatteryMonitoringEnabled { if !UIDevice.current.isBatteryMonitoringEnabled {
UIDevice.current.isBatteryMonitoringEnabled = true UIDevice.current.isBatteryMonitoringEnabled = true
} }