mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP]
This commit is contained in:
parent
169fece59c
commit
857bf2190e
BIN
Tests/CallUITest/Resources/test.png
Normal file
BIN
Tests/CallUITest/Resources/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 463 KiB |
BIN
Tests/CallUITest/Resources/test2.mp4
Normal file
BIN
Tests/CallUITest/Resources/test2.mp4
Normal file
Binary file not shown.
63
submodules/MetalEngine/BUILD
Normal file
63
submodules/MetalEngine/BUILD
Normal 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",
|
||||
],
|
||||
)
|
11
submodules/MetalEngine/Metal/MetalEngineShaders.metal
Normal file
11
submodules/MetalEngine/Metal/MetalEngineShaders.metal
Normal 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);
|
||||
}
|
@ -2,15 +2,9 @@ import Foundation
|
||||
import Metal
|
||||
import UIKit
|
||||
import IOSurface
|
||||
import Display
|
||||
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> {
|
||||
var contents: Resolved?
|
||||
}
|
||||
@ -110,16 +104,16 @@ public struct RenderLayerPlacement: Equatable {
|
||||
public protocol RenderToLayerState: AnyObject {
|
||||
var pipelineState: MTLRenderPipelineState { get }
|
||||
|
||||
init?(device: MTLDevice, library: MTLLibrary)
|
||||
init?(device: MTLDevice)
|
||||
}
|
||||
|
||||
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 surfaceAllocation: MetalContext.SurfaceAllocation?
|
||||
fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation?
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
@ -136,7 +130,7 @@ open class MetalSubjectLayer: SimpleLayer {
|
||||
}
|
||||
|
||||
override public func setNeedsDisplay() {
|
||||
if let subject = self as? MetalSubject {
|
||||
if let subject = self as? MetalEngineSubject {
|
||||
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 context.freeResourcesOnCompletion.contains(where: { $0 === self }) {
|
||||
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 {
|
||||
let commands: (MTLCommandBuffer) -> Void
|
||||
|
||||
@ -205,13 +199,13 @@ public final class MetalSubjectContext {
|
||||
fileprivate final class RenderToLayerOperation {
|
||||
let spec: RenderLayerSpec
|
||||
let state: RenderToLayerState
|
||||
weak var layer: MetalSubjectLayer?
|
||||
weak var layer: MetalEngineSubjectLayer?
|
||||
let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
|
||||
|
||||
init(
|
||||
spec: RenderLayerSpec,
|
||||
state: RenderToLayerState,
|
||||
layer: MetalSubjectLayer,
|
||||
layer: MetalEngineSubjectLayer,
|
||||
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
|
||||
) {
|
||||
self.spec = spec
|
||||
@ -222,13 +216,13 @@ public final class MetalSubjectContext {
|
||||
}
|
||||
|
||||
private let device: MTLDevice
|
||||
private let impl: MetalContext.Impl
|
||||
private let impl: MetalEngine.Impl
|
||||
|
||||
fileprivate var computeOperations: [ComputeOperation] = []
|
||||
fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
|
||||
fileprivate var freeResourcesOnCompletion: [PooledTexture.Texture] = []
|
||||
|
||||
fileprivate init(device: MTLDevice, impl: MetalContext.Impl) {
|
||||
fileprivate init(device: MTLDevice, impl: MetalEngine.Impl) {
|
||||
self.device = device
|
||||
self.impl = impl
|
||||
}
|
||||
@ -236,7 +230,7 @@ public final class MetalSubjectContext {
|
||||
public func renderToLayer<RenderToLayerStateType: RenderToLayerState, each Resolved>(
|
||||
spec: RenderLayerSpec,
|
||||
state: RenderToLayerStateType.Type,
|
||||
layer: MetalSubjectLayer,
|
||||
layer: MetalEngineSubjectLayer,
|
||||
inputs: repeat Placeholder<each Resolved>,
|
||||
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 {
|
||||
resolvedState = current
|
||||
} 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)")
|
||||
return
|
||||
}
|
||||
@ -278,7 +272,7 @@ public final class MetalSubjectContext {
|
||||
public func renderToLayer<RenderToLayerStateType: RenderToLayerState>(
|
||||
spec: RenderLayerSpec,
|
||||
state: RenderToLayerStateType.Type,
|
||||
layer: MetalSubjectLayer,
|
||||
layer: MetalEngineSubjectLayer,
|
||||
commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
|
||||
) {
|
||||
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 {
|
||||
resolvedState = current
|
||||
} 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)")
|
||||
return Placeholder()
|
||||
}
|
||||
@ -338,24 +332,24 @@ public final class MetalSubjectContext {
|
||||
}
|
||||
}
|
||||
|
||||
public final class MetalSubjectInternalData {
|
||||
public final class MetalEngineSubjectInternalData {
|
||||
var internalId: Int = -1
|
||||
var renderSurfaceAllocation: MetalContext.SurfaceAllocation?
|
||||
var renderSurfaceAllocation: MetalEngine.SurfaceAllocation?
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MetalSubject: AnyObject {
|
||||
var internalData: MetalSubjectInternalData? { get set }
|
||||
public protocol MetalEngineSubject: AnyObject {
|
||||
var internalData: MetalEngineSubjectInternalData? { get set }
|
||||
|
||||
func setNeedsUpdate()
|
||||
func update(context: MetalSubjectContext)
|
||||
func update(context: MetalEngineSubjectContext)
|
||||
}
|
||||
|
||||
public extension MetalSubject {
|
||||
public extension MetalEngineSubject {
|
||||
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 Phase {
|
||||
let subRect: CGRect
|
||||
@ -509,9 +503,9 @@ public final class MetalContext {
|
||||
}
|
||||
|
||||
private final class SubjectReference {
|
||||
weak var subject: MetalSubject?
|
||||
weak var subject: MetalEngineSubject?
|
||||
|
||||
init(subject: MetalSubject) {
|
||||
init(subject: MetalEngineSubject) {
|
||||
self.subject = subject
|
||||
}
|
||||
}
|
||||
@ -548,7 +542,7 @@ public final class MetalContext {
|
||||
|
||||
init?(device: MTLDevice) {
|
||||
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
|
||||
}
|
||||
guard let bundle = Bundle(path: path) else {
|
||||
@ -624,15 +618,15 @@ public final class MetalContext {
|
||||
let surfaceId = self.nextSurfaceId
|
||||
self.nextSurfaceId += 1
|
||||
|
||||
let surfaceWidth = max(1024, alignUp(size: Int(minSize.width), align: 64))
|
||||
let surfaceHeight = max(512, alignUp(size: Int(minSize.height), align: 64))
|
||||
let surfaceWidth = max(1024, alignUp(Int(minSize.width), alignment: 64))
|
||||
let surfaceHeight = max(512, alignUp(Int(minSize.height), alignment: 64))
|
||||
let surface = Surface(id: surfaceId, device: self.device, width: surfaceWidth, height: surfaceHeight)
|
||||
self.surfaces[surfaceId] = surface
|
||||
|
||||
return surface
|
||||
}
|
||||
|
||||
private func refreshLayerAllocation(layer: MetalSubjectLayer, renderSpec: RenderLayerSpec) {
|
||||
private func refreshLayerAllocation(layer: MetalEngineSubjectLayer, renderSpec: RenderLayerSpec) {
|
||||
var previousSurfaceId: Int?
|
||||
var updatedSurfaceId: Int?
|
||||
|
||||
@ -689,12 +683,12 @@ public final class MetalContext {
|
||||
}
|
||||
}
|
||||
|
||||
func addSubjectNeedsUpdate(subject: MetalSubject) {
|
||||
let internalData: MetalSubjectInternalData
|
||||
func addSubjectNeedsUpdate(subject: MetalEngineSubject) {
|
||||
let internalData: MetalEngineSubjectInternalData
|
||||
if let current = subject.internalData {
|
||||
internalData = current
|
||||
} else {
|
||||
internalData = MetalSubjectInternalData()
|
||||
internalData = MetalEngineSubjectInternalData()
|
||||
subject.internalData = internalData
|
||||
}
|
||||
|
||||
@ -751,7 +745,7 @@ public final class MetalContext {
|
||||
return
|
||||
}
|
||||
|
||||
let subjectContext = MetalSubjectContext(device: device, impl: self)
|
||||
let subjectContext = MetalEngineSubjectContext(device: device, impl: self)
|
||||
|
||||
for subjectReference in self.updatedSubjects {
|
||||
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
|
||||
|
@ -105,6 +105,7 @@ swift_library(
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
|
||||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||
"//submodules/TelegramUI/Components/Calls/CallScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -135,11 +135,11 @@ public final class CallController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
if self.call.isVideoPossible {
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
#if DEBUG
|
||||
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
|
||||
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.controllerNode.toggleMute = { [weak self] in
|
||||
|
@ -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]))
|
||||
}
|
||||
}
|
@ -410,6 +410,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
|
||||
"//submodules/TelegramUI/Components/ContextMenuScreen",
|
||||
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
|
||||
"//submodules/MetalEngine",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -10,7 +10,7 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "MetalSources",
|
||||
name = "CallScreenMetalSources",
|
||||
srcs = glob([
|
||||
"Metal/**/*.metal",
|
||||
]),
|
||||
@ -18,7 +18,7 @@ filegroup(
|
||||
)
|
||||
|
||||
plist_fragment(
|
||||
name = "MetalSourcesBundleInfoPlist",
|
||||
name = "CallScreenMetalSourcesBundleInfoPlist",
|
||||
extension = "plist",
|
||||
template =
|
||||
"""
|
||||
@ -27,20 +27,26 @@ plist_fragment(
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>AnimationCompression</string>
|
||||
<string>CallScreen</string>
|
||||
"""
|
||||
)
|
||||
|
||||
apple_resource_bundle(
|
||||
name = "MetalSourcesBundle",
|
||||
name = "CallScreenMetalSourcesBundle",
|
||||
infoplists = [
|
||||
":MetalSourcesBundleInfoPlist",
|
||||
":CallScreenMetalSourcesBundleInfoPlist",
|
||||
],
|
||||
resources = [
|
||||
":MetalSources",
|
||||
":CallScreenMetalSources",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "Assets",
|
||||
srcs = glob(["CallScreenAssets.xcassets/**"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "CallScreen",
|
||||
module_name = "CallScreen",
|
||||
@ -51,13 +57,14 @@ swift_library(
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
data = [
|
||||
":MetalSourcesBundle",
|
||||
":CallScreenMetalSourcesBundle",
|
||||
":Assets",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/MetalEngine",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/Utils/ShelfPack",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_audioairpods.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_audioairpodspro.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_airpodsmax.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_audiobt.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -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 |
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_flip (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_microphone.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_speaker.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_call_camera.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -21,14 +21,6 @@ struct QuadVertexOut {
|
||||
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(
|
||||
const device Rectangle &rect [[ buffer(0) ]],
|
||||
unsigned int vid [[ vertex_id ]]
|
@ -147,6 +147,18 @@ public final class AnimatedProperty<T: AnimationInterpolatable>: AnyAnimatedProp
|
||||
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() {
|
||||
if let animation = self.animation {
|
||||
self.value = animation.update(at: CACurrentMediaTime())
|
||||
|
@ -1,4 +1,28 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
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
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class AvatarLayer: SimpleLayer {
|
||||
var image: UIImage? {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class ButtonGroupView: UIView, ContentOverlayView {
|
||||
enum Key: Hashable {
|
||||
@ -12,6 +13,7 @@ final class ButtonGroupView: UIView, ContentOverlayView {
|
||||
let overlayMaskLayer: CALayer
|
||||
private var buttons: [Key: ContentOverlayButton] = [:]
|
||||
|
||||
var audioPressed: (() -> Void)?
|
||||
var toggleVideo: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@ -89,9 +91,15 @@ final class ButtonGroupView: UIView, ContentOverlayView {
|
||||
let image: UIImage?
|
||||
switch key {
|
||||
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:
|
||||
image = UIImage(named: "Call/ButtonVideo")
|
||||
image = UIImage(named: "Call/Video")
|
||||
button.action = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -99,9 +107,9 @@ final class ButtonGroupView: UIView, ContentOverlayView {
|
||||
self.toggleVideo?()
|
||||
}
|
||||
case .mic:
|
||||
image = UIImage(named: "Call/ButtonMicMuted")
|
||||
image = UIImage(named: "Call/Mute")
|
||||
case .close:
|
||||
image = UIImage(named: "Call/ButtonEnd")
|
||||
image = UIImage(named: "Call/End")
|
||||
}
|
||||
|
||||
button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import MetalKit
|
||||
import UIKit
|
||||
import MetalEngine
|
||||
|
||||
private func shiftArray(array: [SIMD2<Float>], offset: Int) -> [SIMD2<Float>] {
|
||||
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)
|
||||
}
|
||||
|
||||
final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
|
||||
var internalData: MetalSubjectInternalData?
|
||||
private struct ColorSet: Equatable, AnimationInterpolatable {
|
||||
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 {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@ -66,11 +82,44 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
|
||||
|
||||
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) {
|
||||
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()
|
||||
|
||||
self.didEnterHierarchy = { [weak self] in
|
||||
@ -81,8 +130,14 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.colorTransition.update()
|
||||
self.phaseAcceleration.update()
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
@ -95,6 +150,9 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
self.colorSets = []
|
||||
self.colorTransition = AnimatedProperty<ColorSet>(ColorSet(colors: []))
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
@ -102,7 +160,19 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -127,22 +197,9 @@ final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
|
||||
}
|
||||
encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout<SIMD2<Float>>.size, index: 0)
|
||||
|
||||
var colors: [[SIMD4<Float>]] = [
|
||||
[
|
||||
hexToFloat(0x568FD6),
|
||||
hexToFloat(0x626ED5),
|
||||
hexToFloat(0xA667D5),
|
||||
hexToFloat(0x7664DA)
|
||||
],
|
||||
[
|
||||
hexToFloat(0xACBD65),
|
||||
hexToFloat(0x459F8D),
|
||||
hexToFloat(0x53A4D1),
|
||||
hexToFloat(0x3E917A)
|
||||
]
|
||||
]
|
||||
var colors: [SIMD4<Float>] = self.colorTransition.value.colors
|
||||
|
||||
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 saturation: Float = isBlur ? 1.2 : 1.0
|
||||
encoder.setFragmentBytes(&brightness, length: 4, index: 2)
|
||||
|
@ -1,8 +1,9 @@
|
||||
import Foundation
|
||||
import MetalKit
|
||||
import MetalEngine
|
||||
|
||||
final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
|
||||
var internalData: MetalSubjectInternalData?
|
||||
final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
var internalData: MetalEngineSubjectInternalData?
|
||||
|
||||
struct Blob {
|
||||
var points: [Float]
|
||||
@ -48,10 +49,10 @@ final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
|
||||
|
||||
let pipelineState: MTLRenderPipelineState
|
||||
|
||||
required init?(
|
||||
device: MTLDevice,
|
||||
library: MTLLibrary
|
||||
) {
|
||||
required init?(device: MTLDevice) {
|
||||
guard let library = metalLibrary(device: device) else {
|
||||
return nil
|
||||
}
|
||||
guard let vertexFunction = library.makeFunction(name: "callBlobVertex"), let fragmentFunction = library.makeFunction(name: "callBlobFragment") else {
|
||||
return nil
|
||||
}
|
||||
@ -123,7 +124,7 @@ final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(context: MetalSubjectContext) {
|
||||
func update(context: MetalEngineSubjectContext) {
|
||||
if self.bounds.isEmpty {
|
||||
return
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class ContentOverlayButton: UIButton, ContentOverlayView {
|
||||
var overlayMaskLayer: CALayer {
|
||||
|
@ -1,7 +1,21 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import MetalEngine
|
||||
|
||||
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 avatarLayer: AvatarLayer
|
||||
private let titleView: TextView
|
||||
@ -14,8 +28,7 @@ final class ContentView: UIView {
|
||||
private var videoLayerMask: SimpleShapeLayer?
|
||||
private var blurredVideoLayerMask: SimpleShapeLayer?
|
||||
|
||||
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||
private var phase: CGFloat = 0.0
|
||||
private var params: Params?
|
||||
|
||||
private var isDisplayingVideo: Bool = false
|
||||
private var videoDisplayFraction = AnimatedProperty<CGFloat>(0.0)
|
||||
@ -54,8 +67,8 @@ final class ContentView: UIView {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let currentLayout = self.currentLayout {
|
||||
self.update(size: currentLayout.size, insets: currentLayout.insets)
|
||||
if let params = self.params {
|
||||
self.updateInternal(params: params)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,41 +77,59 @@ final class ContentView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets) {
|
||||
self.currentLayout = (size, insets)
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, state: PrivateCallScreen.State) {
|
||||
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 {
|
||||
let emojiView = KeyEmojiView(emoji: ["🐱", "🚂", "❄️", "🎨"])
|
||||
self.emojiView = emojiView
|
||||
self.addSubview(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") {
|
||||
self.videoInput = VideoInput(device: MetalContext.shared.device, url: url)
|
||||
self.videoInput = VideoInput(device: MetalEngine.shared.device, url: url)
|
||||
self.videoLayer.video = self.videoInput
|
||||
}
|
||||
|
||||
self.phase += 3.0 / 60.0
|
||||
self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
|
||||
var avatarScale: CGFloat = 0.05 * sin(CGFloat(self.phase) * CGFloat.pi)
|
||||
//self.phase += 3.0 / 60.0
|
||||
//self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
|
||||
var avatarScale: CGFloat = 0.05 * sin(CGFloat(0.0) * CGFloat.pi)
|
||||
avatarScale *= 1.0 - self.videoDisplayFraction.value
|
||||
|
||||
let avatarSize: CGFloat = 136.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 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 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: (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
|
||||
|
||||
let statusSize = self.statusView.update(state: .waiting(.requesting))
|
||||
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)
|
||||
let statusState: StatusView.State
|
||||
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))
|
||||
|
||||
@ -114,7 +145,7 @@ final class ContentView: UIView {
|
||||
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 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)
|
||||
|
||||
|
@ -3,6 +3,7 @@ import UIKit
|
||||
import MetalKit
|
||||
import MetalPerformanceShaders
|
||||
import Accelerate
|
||||
import MetalEngine
|
||||
|
||||
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 {
|
||||
@ -36,10 +37,10 @@ func imageToCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
|
||||
return pixelBuffer
|
||||
}
|
||||
|
||||
final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
|
||||
var internalData: MetalSubjectInternalData?
|
||||
final class MainVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
var internalData: MetalEngineSubjectInternalData?
|
||||
|
||||
let blurredLayer: MetalSubjectLayer
|
||||
let blurredLayer: MetalEngineSubjectLayer
|
||||
|
||||
final class BlurState: ComputeState {
|
||||
let computePipelineStateYUVToRGBA: MTLComputePipelineState
|
||||
@ -47,10 +48,10 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
|
||||
let computePipelineStateVertical: MTLComputePipelineState
|
||||
let downscaleKernel: MPSImageBilinearScale
|
||||
|
||||
required init?(
|
||||
device: MTLDevice,
|
||||
library: MTLLibrary
|
||||
) {
|
||||
required init?(device: MTLDevice) {
|
||||
guard let library = metalLibrary(device: device) else {
|
||||
return nil
|
||||
}
|
||||
guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
|
||||
return nil
|
||||
}
|
||||
@ -79,10 +80,10 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
|
||||
final class RenderState: RenderToLayerState {
|
||||
let pipelineState: MTLRenderPipelineState
|
||||
|
||||
required init?(
|
||||
device: MTLDevice,
|
||||
library: MTLLibrary
|
||||
) {
|
||||
required init?(device: MTLDevice) {
|
||||
guard let library = metalLibrary(device: device) else {
|
||||
return nil
|
||||
}
|
||||
guard let vertexFunction = library.makeFunction(name: "mainVideoVertex"), let fragmentFunction = library.makeFunction(name: "mainVideoFragment") else {
|
||||
return nil
|
||||
}
|
||||
@ -115,13 +116,13 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
|
||||
private var blurredVerticalTexture: PooledTexture?
|
||||
|
||||
override init() {
|
||||
self.blurredLayer = MetalSubjectLayer()
|
||||
self.blurredLayer = MetalEngineSubjectLayer()
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
self.blurredLayer = MetalSubjectLayer()
|
||||
self.blurredLayer = MetalEngineSubjectLayer()
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
@ -130,7 +131,7 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(context: MetalSubjectContext) {
|
||||
func update(context: MetalEngineSubjectContext) {
|
||||
if self.isHidden {
|
||||
return
|
||||
}
|
||||
@ -143,16 +144,16 @@ final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
|
||||
|
||||
let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized)
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
private final class AnimatedDotsLayer: SimpleLayer {
|
||||
private let dotLayers: [SimpleLayer]
|
||||
@ -76,8 +77,18 @@ final class StatusView: UIView {
|
||||
case ringing
|
||||
case generatingKeys
|
||||
}
|
||||
|
||||
struct ActiveState {
|
||||
var signalStrength: Double
|
||||
|
||||
init(signalStrength: Double) {
|
||||
self.signalStrength = signalStrength
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
case waiting(WaitingState)
|
||||
case active(ActiveState)
|
||||
}
|
||||
|
||||
private var textView: TextView
|
||||
@ -110,6 +121,9 @@ final class StatusView: UIView {
|
||||
case .generatingKeys:
|
||||
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)
|
||||
self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class ContentOverlayLayer: SimpleLayer {
|
||||
private struct Params: Equatable {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class MirroringLayer: SimpleLayer {
|
||||
var targetLayer: SimpleLayer?
|
||||
|
@ -1,7 +1,42 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import MetalEngine
|
||||
|
||||
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 contentOverlayLayer: ContentOverlayLayer
|
||||
private let contentOverlayContainer: ContentOverlayContainer
|
||||
@ -13,6 +48,18 @@ public final class PrivateCallScreen: UIView {
|
||||
|
||||
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) {
|
||||
self.blurContentsLayer = SimpleLayer()
|
||||
|
||||
@ -44,6 +91,26 @@ public final class PrivateCallScreen: UIView {
|
||||
|
||||
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
|
||||
guard let self else {
|
||||
return
|
||||
@ -65,9 +132,18 @@ public final class PrivateCallScreen: UIView {
|
||||
}
|
||||
|
||||
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 renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
|
||||
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.frame = visualBackgroundFrame
|
||||
|
||||
self.contentOverlayLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.contentOverlayLayer.update(size: size, contentInsets: UIEdgeInsets())
|
||||
let backgroundStateIndex: Int
|
||||
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.frame = visualBackgroundFrame
|
||||
self.blurBackgroundLayer.update(stateIndex: backgroundStateIndex, animated: animated)
|
||||
|
||||
self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.buttonGroupView.update(size: size)
|
||||
self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: params.size)
|
||||
self.buttonGroupView.update(size: params.size)
|
||||
|
||||
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.contentView.update(size: size, insets: insets)
|
||||
self.contentView.frame = CGRect(origin: CGPoint(), size: params.size)
|
||||
self.contentView.update(size: params.size, insets: params.insets, state: self.state)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ import DeviceProximity
|
||||
import MediaEditor
|
||||
import TelegramUIDeclareEncodables
|
||||
import ContextMenuScreen
|
||||
import MetalEngine
|
||||
|
||||
#if canImport(AppCenter)
|
||||
import AppCenter
|
||||
@ -341,6 +342,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
self.window = window
|
||||
self.nativeWindow = window
|
||||
|
||||
hostView.containerView.layer.addSublayer(MetalEngine.shared.rootLayer)
|
||||
|
||||
if !UIDevice.current.isBatteryMonitoringEnabled {
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user