From 169fece59ce7f87b9a6fdcdd045b49036966e4b1 Mon Sep 17 00:00:00 2001
From: Ali <>
Date: Thu, 9 Nov 2023 22:28:02 +0400
Subject: [PATCH] Move code
---
Tests/CallUITest/BUILD | 44 +-
Tests/CallUITest/Sources/ViewController.swift | 13 +-
build-system/Make/ProjectGeneration.py | 21 +-
.../Components/Calls/CallScreen/BUILD | 45 +
.../Calls/CallScreen/Metal/Shaders.metal | 349 +++++++
.../Sources/Animation/Animation.swift | 158 +++
.../Sources/Animation/AnimationManager.swift | 50 +
.../CallScreen/Sources/Animation/Bezier.swift | 53 +
.../Sources/Components/AvatarLayer.swift | 40 +
.../Sources/Components/ButtonGroupView.swift | 126 +++
.../Components/CallBackgroundLayer.swift | 154 +++
.../Sources/Components/CallBlobsLayer.swift | 154 +++
.../Components/ContentOverlayButton.swift | 111 +++
.../Sources/Components/ContentView.swift | 176 ++++
.../Sources/Components/KeyEmojiView.swift | 43 +
.../Sources/Components/MainVideoLayer.swift | 256 +++++
.../Sources/Components/StatusView.swift | 140 +++
.../Sources/Components/TitleView.swift | 63 ++
.../Sources/ContentOverlayContainer.swift | 60 ++
.../Sources/ContentOverlayLayer.swift | 71 ++
.../CallScreen/Sources/Media/VideoInput.swift | 118 +++
.../CallScreen/Sources/MirroringLayer.swift | 97 ++
.../Sources/PrivateCallScreen.swift | 94 ++
.../Sources/Rendering/MetalLayerContext.swift | 921 ++++++++++++++++++
.../CallScreen/Sources/SimpleLayer.swift | 67 ++
.../CallScreen/Sources/Utils/AlignUp.swift | 8 +
.../Sources/Utils/Interpolate.swift | 9 +
.../Sources/Utils/SharedDisplayLink.swift | 102 ++
submodules/Utils/ShelfPack/BUILD | 24 +
.../PublicHeaders/ShelfPack/ShelfPack.h | 31 +
.../Utils/ShelfPack/Sources/ShelfPack.mm | 51 +
.../Utils/ShelfPack/Sources/shelf-pack.hpp | 535 ++++++++++
32 files changed, 4173 insertions(+), 11 deletions(-)
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/SimpleLayer.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/AlignUp.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/Interpolate.swift
create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Utils/SharedDisplayLink.swift
create mode 100644 submodules/Utils/ShelfPack/BUILD
create mode 100644 submodules/Utils/ShelfPack/PublicHeaders/ShelfPack/ShelfPack.h
create mode 100644 submodules/Utils/ShelfPack/Sources/ShelfPack.mm
create mode 100644 submodules/Utils/ShelfPack/Sources/shelf-pack.hpp
diff --git a/Tests/CallUITest/BUILD b/Tests/CallUITest/BUILD
index 8e537a5bb8..5191d4bfc2 100644
--- a/Tests/CallUITest/BUILD
+++ b/Tests/CallUITest/BUILD
@@ -10,6 +10,21 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
+load(
+ "@rules_xcodeproj//xcodeproj:defs.bzl",
+ "top_level_target",
+ "top_level_targets",
+ "xcodeproj",
+ "xcode_provisioning_profile",
+)
+
+load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")
+
+load(
+ "@build_configuration//:variables.bzl",
+ "telegram_bazel_path",
+)
+
filegroup(
name = "AppResources",
srcs = glob([
@@ -72,7 +87,7 @@ plist_fragment(
CFBundleDisplayName
Test
CFBundleIdentifier
- org.telegram.CallUITest
+ org.telegram.Telegram-iOS
CFBundleName
Telegram
CFBundlePackageType
@@ -129,7 +144,7 @@ plist_fragment(
ios_application(
name = "CallUITest",
- bundle_id = "org.telegram.TelegramiOS",
+ bundle_id = "org.telegram.Telegram-iOS",
families = ["iphone", "ipad"],
minimum_os_version = "12.0",
provisioning_profile = "@build_configuration//provisioning:Telegram.mobileprovision",
@@ -147,4 +162,29 @@ ios_application(
"//Tests/Common:Main",
":Lib",
],
+ visibility = ["//visibility:public"],
+)
+
+xcodeproj(
+ name = "CallUITest_xcodeproj",
+ build_mode = "bazel",
+ bazel_path = telegram_bazel_path,
+ project_name = "CallUITest",
+ tags = ["manual"],
+ top_level_targets = top_level_targets(
+ labels = [
+ ":CallUITest",
+ ],
+ target_environments = ["device", "simulator"],
+ ),
+ xcode_configurations = {
+ "Debug": {
+ "//command_line_option:compilation_mode": "dbg",
+ },
+ "Release": {
+ "//command_line_option:compilation_mode": "opt",
+ },
+ },
+ default_xcode_configuration = "Debug"
+
)
diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift
index 11a15bca30..ac6a8c57bd 100644
--- a/Tests/CallUITest/Sources/ViewController.swift
+++ b/Tests/CallUITest/Sources/ViewController.swift
@@ -1,8 +1,19 @@
import Foundation
import UIKit
+import CallScreen
-public final class ViewController: UIViewController {
+public final class ViewController: UIViewController {
override public func viewDidLoad() {
super.viewDidLoad()
+
+ let privateCallScreen = PrivateCallScreen(frame: CGRect())
+ self.view.addSubview(privateCallScreen)
+
+ privateCallScreen.frame = self.view.bounds
+ privateCallScreen.update(size: self.view.bounds.size, insets: UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0))
+
+ let context = MetalContext.shared
+ self.view.layer.addSublayer(context.rootLayer)
+ context.rootLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -101.0), size: CGSize(width: 100.0, height: 100.0))
}
}
diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py
index 37e2ca22bc..a8759a89e2 100644
--- a/build-system/Make/ProjectGeneration.py
+++ b/build-system/Make/ProjectGeneration.py
@@ -20,19 +20,24 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
app_target_clean = app_target.replace('/', '_')
bazel_generate_arguments = [build_environment.bazel_path]
- bazel_generate_arguments += ['run', '//Telegram:Telegram_xcodeproj']
+
+ bazel_generate_arguments += ['run', '//{}_xcodeproj'.format(app_target_spec)]
bazel_generate_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
- if disable_extensions:
- bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
- bazel_generate_arguments += ['--//{}:disableStripping'.format('Telegram')]
+
+ if target_name == 'Telegram':
+ if disable_extensions:
+ bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
+ bazel_generate_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments = []
for argument in bazel_app_arguments:
project_bazel_arguments.append(argument)
project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
- if disable_extensions:
- project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
- project_bazel_arguments += ['--//{}:disableStripping'.format('Telegram')]
+
+ if target_name == 'Telegram':
+ if disable_extensions:
+ project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
+ project_bazel_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments += ['--features=-swift.debug_prefix_map']
@@ -45,7 +50,7 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
call_executable(bazel_generate_arguments)
- xcodeproj_path = 'Telegram/Telegram.xcodeproj'
+ xcodeproj_path = '{}.xcodeproj'.format(app_target_spec.replace(':', '/'))
call_executable(['open', xcodeproj_path])
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD
index f218434db1..059d137551 100644
--- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD
@@ -1,5 +1,46 @@
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 = "MetalSourcesBundleInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleIdentifier
+ org.telegram.CallScreenMetalSources
+ CFBundleDevelopmentRegion
+ en
+ CFBundleName
+ AnimationCompression
+ """
+)
+
+apple_resource_bundle(
+ name = "MetalSourcesBundle",
+ infoplists = [
+ ":MetalSourcesBundleInfoPlist",
+ ],
+ resources = [
+ ":MetalSources",
+ ],
+)
+
swift_library(
name = "CallScreen",
module_name = "CallScreen",
@@ -9,10 +50,14 @@ swift_library(
copts = [
"-warnings-as-errors",
],
+ data = [
+ ":MetalSourcesBundle",
+ ],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
+ "//submodules/Utils/ShelfPack",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal
new file mode 100644
index 0000000000..7bf649b0fc
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/Shaders.metal
@@ -0,0 +1,349 @@
+#include
+
+using namespace metal;
+
+struct Rectangle {
+ float2 origin;
+ float2 size;
+};
+
+constant static float2 quadVertices[6] = {
+ float2(0.0, 0.0),
+ float2(1.0, 0.0),
+ float2(0.0, 1.0),
+ float2(1.0, 0.0),
+ float2(0.0, 1.0),
+ float2(1.0, 1.0)
+};
+
+struct QuadVertexOut {
+ float4 position [[position]];
+ 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 ]]
+) {
+ float2 quadVertex = quadVertices[vid];
+
+ QuadVertexOut out;
+
+ out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
+ out.position.x = -1.0 + out.position.x * 2.0;
+ out.position.y = -1.0 + out.position.y * 2.0;
+
+ out.uv = quadVertex;
+
+ return out;
+}
+
+half4 rgb2hsv(half4 c) {
+ half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+ half4 p = mix(half4(c.bg, K.wz), half4(c.gb, K.xy), step(c.b, c.g));
+ half4 q = mix(half4(p.xyw, c.r), half4(c.r, p.yzx), step(p.x, c.r));
+
+ float d = q.x - min(q.w, q.y);
+ float e = 1.0e-10;
+ return half4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
+}
+
+half4 hsv2rgb(half4 c) {
+ half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ half3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ return half4(c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y), c.a);
+}
+
+fragment half4 callBackgroundFragment(
+ QuadVertexOut in [[stage_in]],
+ const device float2 *positions [[ buffer(0) ]],
+ const device float4 *colors [[ buffer(1) ]],
+ const device float &brightness [[ buffer(2) ]],
+ const device float &saturation [[ buffer(3) ]]
+) {
+ half centerDistanceX = in.uv.x - 0.5;
+ half centerDistanceY = in.uv.y - 0.5;
+ half centerDistance = distance(half2(in.uv), half2(0.5, 0.5));
+ half swirlFactor = 0.35 * centerDistance;
+ half theta = swirlFactor * swirlFactor * 0.8 * 8.0;
+ half sinTheta = sin(theta);
+ half cosTheta = cos(theta);
+
+ half pixelX = max(0.0, min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta));
+ half pixelY = max(0.0, min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta));
+
+ half distanceSum = 0.0;
+
+ half r = 0.0;
+ half g = 0.0;
+ half b = 0.0;
+
+ for (int i = 0; i < 4; i++) {
+ half4 color = half4(colors[i]);
+
+ half2 colorXY = half2(positions[i]);
+ half2 distanceXY = half2(pixelX - colorXY.x, pixelY - colorXY.y);
+
+ half distance = max(0.0, 0.92 - sqrt(distanceXY.x * distanceXY.x + distanceXY.y * distanceXY.y));
+ distance = distance * distance * distance;
+ distanceSum += distance;
+
+ r = r + distance * color.r;
+ g = g + distance * color.g;
+ b = b + distance * color.b;
+ }
+
+ if (distanceSum < 0.00001) {
+ distanceSum = 0.00001;
+ }
+
+ half pixelB = b / distanceSum;
+ half pixelG = g / distanceSum;
+ half pixelR = r / distanceSum;
+
+ half4 color(pixelR, pixelG, pixelB, 1.0);
+ color = rgb2hsv(color);
+ color.b = clamp(color.b * brightness, 0.0, 1.0);
+ color.g = clamp(color.g * saturation, 0.0, 1.0);
+ color = hsv2rgb(color);
+
+ return color;
+}
+
+struct BlobVertexOut {
+ float4 position [[position]];
+};
+
+float2 blobVertex(float2 center, float angle, float radius) {
+ return float2(center.x + radius * cos(angle), center.y + radius * sin(angle));
+}
+
+float2 mapPointInRect(Rectangle rect, half2 point) {
+ half2 out(rect.origin.x + rect.size.x * point.x, rect.origin.y + rect.size.y * point.y);
+ out.x = -1.0 + out.x * 2.0;
+ out.y = -1.0 + out.y * 2.0;
+ return float2(out);
+}
+
+struct SmoothPoint {
+ half2 point;
+ half inAngle;
+ half inLength;
+ half outAngle;
+ half outLength;
+
+ half2 smoothIn() {
+ return smooth(inAngle, inLength);
+ }
+
+ half2 smoothOut() {
+ return smooth(outAngle, outLength);
+ }
+
+private:
+ half2 smooth(half angle, half length) {
+ return half2(
+ point.x + length * cos(angle),
+ point.y + length * sin(angle)
+ );
+ }
+};
+
+half2 evaluateBlobPoint(const device Rectangle &rect, const device float *positions, int index, int count, int subdivisions) {
+ float position = positions[index];
+ float segmentAngle = float(index) / float(count) * 2.0 * 3.1415926;
+ return half2(blobVertex(float2(0.5, 0.5), segmentAngle, 0.45 + 0.05 * position));
+}
+
+SmoothPoint evaluateSmoothBlobPoint(const device Rectangle &rect, const device float *positions, int index, int count, int subdivisions) {
+ int prevIndex = (index - 1) < 0 ? (count - 1) : (index - 1);
+ int nextIndex = (index + 1) % count;
+
+ half2 prev = evaluateBlobPoint(rect, positions, prevIndex, count, subdivisions);
+ half2 curr = evaluateBlobPoint(rect, positions, index, count, subdivisions);
+ half2 next = evaluateBlobPoint(rect, positions, nextIndex, count, subdivisions);
+
+ float dx = next.x - prev.x;
+ float dy = -next.y + prev.y;
+ float angle = atan2(dy, dx);
+ if (angle < 0.0) {
+ angle = abs(angle);
+ } else {
+ angle = 2 * 3.1415926 - angle;
+ }
+
+ float smoothAngle = (3.1415926 * 2.0) / float(count);
+ float smoothness = ((4.0 / 3.0) * tan(smoothAngle / 4.0)) / sin(smoothAngle / 2.0) / 2.0;
+
+ SmoothPoint point;
+ point.point = curr;
+ point.inAngle = angle + 3.1415926;
+ point.inLength = smoothness * distance(curr, prev);
+ point.outAngle = angle;
+ point.outLength = smoothness * distance(curr, next);
+
+ return point;
+}
+
+half2 evaluateBezierBlobPoint(thread SmoothPoint &curr, thread SmoothPoint &next, half t) {
+ half oneMinusT = 1.0 - t;
+
+ half2 p0 = curr.point;
+ half2 p1 = curr.smoothOut();
+ half2 p2 = next.smoothIn();
+ half2 p3 = next.point;
+
+ return oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3;
+}
+
+vertex BlobVertexOut callBlobVertex(
+ const device Rectangle &rect [[ buffer(0) ]],
+ const device float *positions [[ buffer(1) ]],
+ const device int &count [[ buffer(2) ]],
+ unsigned int vid [[ vertex_id ]]
+) {
+ const int subdivisions = 8;
+
+ int triangleIndex = vid / 3;
+
+ int segmentIndex = triangleIndex / subdivisions;
+ int nextIndex = (segmentIndex + 1) % count;
+
+ half innerPosition = half(triangleIndex - segmentIndex * subdivisions) / half(subdivisions);
+ half nextInnerPosition = half(triangleIndex + 1 - segmentIndex * subdivisions) / half(subdivisions);
+
+ SmoothPoint curr = evaluateSmoothBlobPoint(rect, positions, segmentIndex, count, subdivisions);
+ SmoothPoint next = evaluateSmoothBlobPoint(rect, positions, nextIndex, count, subdivisions);
+
+ half2 triangle[3];
+ triangle[0] = half2(0.5, 0.5);
+ triangle[1] = evaluateBezierBlobPoint(curr, next, innerPosition);
+ triangle[2] = evaluateBezierBlobPoint(curr, next, nextInnerPosition);
+
+ BlobVertexOut out;
+ out.position = float4(float2(mapPointInRect(rect, triangle[vid % 3])), 0.0, 1.0);
+
+ return out;
+}
+
+fragment half4 callBlobFragment(
+ BlobVertexOut in [[stage_in]]
+) {
+ half alpha = 0.15;
+ return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha);
+}
+
+kernel void videoYUVToRGBA(
+ texture2d inTextureY [[ texture(0) ]],
+ texture2d inTextureUV [[ texture(1) ]],
+ texture2d outTexture [[ texture(2) ]],
+ uint2 threadPosition [[ thread_position_in_grid ]]
+) {
+ half y = inTextureY.read(threadPosition).r;
+ half2 uv = inTextureUV.read(uint2(threadPosition.x / 2, threadPosition.y / 2)).rg - half2(0.5, 0.5);
+
+ half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
+ outTexture.write(color, threadPosition);
+}
+
+vertex QuadVertexOut mainVideoVertex(
+ const device Rectangle &rect [[ buffer(0) ]],
+ unsigned int vid [[ vertex_id ]]
+) {
+ float2 quadVertex = quadVertices[vid];
+
+ QuadVertexOut out;
+
+ out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
+ out.position.x = -1.0 + out.position.x * 2.0;
+ out.position.y = -1.0 + out.position.y * 2.0;
+
+ out.uv = float2(quadVertex.x, 1.0 - quadVertex.y);
+
+ return out;
+}
+
+fragment half4 mainVideoFragment(
+ QuadVertexOut in [[stage_in]],
+ texture2d texture [[ texture(0) ]],
+ const device float &brightness [[ buffer(0) ]],
+ const device float &saturation [[ buffer(1) ]]
+) {
+ constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
+ half4 color = texture.sample(sampler, in.uv);
+ color = rgb2hsv(color);
+ color.b = clamp(color.b * brightness, 0.0, 1.0);
+ color.g = clamp(color.g * saturation, 0.0, 1.0);
+ color = hsv2rgb(color);
+
+ return half4(color.r, color.g, color.b, color.a);
+}
+
+constant int BLUR_SAMPLE_COUNT = 7;
+constant float BLUR_OFFSETS[BLUR_SAMPLE_COUNT] = {
+ -5.227545617192816,
+ -3.3147990233346842,
+ -1.4174297935376852,
+ 0.47225076494548685,
+ 2.364576440741639,
+ 4.268941421369995,
+ 6
+};
+
+constant float BLUR_WEIGHTS[BLUR_SAMPLE_COUNT] = {
+ 0.015167713616041436,
+ 0.10117053983645591,
+ 0.2894431725427234,
+ 0.3570581167968804,
+ 0.19014435646109845,
+ 0.0435647539906345,
+ 0.0034513467561660305
+};
+
+static void gaussianBlur(
+ texture2d inTexture,
+ texture2d outTexture,
+ float2 offset,
+ uint2 gid
+) {
+ constexpr sampler sampler(coord::normalized, address::clamp_to_edge, filter::linear);
+
+ uint2 textureDim(outTexture.get_width(), outTexture.get_height());
+ if(all(gid < textureDim)) {
+ float3 outColor(0.0);
+
+ float2 size(inTexture.get_width(), inTexture.get_height());
+
+ float2 baseTexCoord = float2(gid);
+
+ for (int i = 0; i < BLUR_SAMPLE_COUNT; i++) {
+ outColor += float3(inTexture.sample(sampler, (baseTexCoord + offset * BLUR_OFFSETS[i]) / size).rgb) * BLUR_WEIGHTS[i];
+ }
+
+ outTexture.write(half4(half3(outColor), 1.0), gid);
+ }
+}
+
+kernel void gaussianBlurHorizontal(
+ texture2d inTexture [[ texture(0) ]],
+ texture2d outTexture [[ texture(1) ]],
+ uint2 gid [[ thread_position_in_grid ]]
+) {
+ gaussianBlur(inTexture, outTexture, float2(1, 0), gid);
+}
+
+kernel void gaussianBlurVertical(
+ texture2d inTexture [[ texture(0) ]],
+ texture2d outTexture [[ texture(1) ]],
+ uint2 gid [[ thread_position_in_grid ]]
+) {
+ gaussianBlur(inTexture, outTexture, float2(0, 1), gid);
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift
new file mode 100644
index 0000000000..531f7de1bf
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Animation.swift
@@ -0,0 +1,158 @@
+import Foundation
+import UIKit
+
+public enum AnimationCurve {
+ case linear
+ case easeInOut
+ case spring
+}
+
+extension AnimationCurve {
+ func map(_ fraction: CGFloat) -> CGFloat {
+ switch self {
+ case .linear:
+ return fraction
+ case .easeInOut:
+ return bezierPoint(0.42, 0.0, 0.58, 1.0, fraction)
+ case .spring:
+ return bezierPoint(0.23, 1.0, 0.32, 1.0, fraction)
+ }
+ }
+}
+
+open class AnyAnimation {
+}
+
+open class AnimationInterpolator {
+ private let impl: (T, T, CGFloat) -> T
+
+ init(_ impl: @escaping (T, T, CGFloat) -> T) {
+ self.impl = impl
+ }
+
+ public func interpolate(from: T, to: T, fraction: CGFloat) -> T {
+ return self.impl(from, to, fraction)
+ }
+}
+
+public protocol AnimationInterpolatable {
+ static var animationInterpolator: AnimationInterpolator { get }
+}
+
+private let CGFloatInterpolator = AnimationInterpolator { from, to, fraction in
+ return from * (1.0 - fraction) + to * fraction
+}
+extension CGFloat: AnimationInterpolatable {
+ public static var animationInterpolator: AnimationInterpolator {
+ return CGFloatInterpolator
+ }
+}
+
+private let CGPointInterpolator = AnimationInterpolator { from, to, fraction in
+ return CGPoint(
+ x: CGFloatInterpolator.interpolate(from: from.x, to: to.x, fraction: fraction),
+ y: CGFloatInterpolator.interpolate(from: from.y, to: to.y, fraction: fraction)
+ )
+}
+extension CGPoint: AnimationInterpolatable {
+ public static var animationInterpolator: AnimationInterpolator {
+ return CGPointInterpolator
+ }
+}
+
+#if targetEnvironment(simulator)
+@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
+#endif
+
+public final class Animation: AnyAnimation {
+ private let from: T
+ private let to: T
+ private let duration: Double
+ private let curve: AnimationCurve
+ private let interpolator: AnimationInterpolator
+
+ private var startTime: Double?
+ public private(set) var isFinished: Bool = false
+
+ var didStart: (() -> Void)?
+
+ public init(from: T, to: T, duration: Double, curve: AnimationCurve) {
+ self.from = from
+ self.to = to
+ #if targetEnvironment(simulator)
+ self.duration = duration * Double(UIAnimationDragCoefficient())
+ #else
+ self.duration = duration
+ #endif
+ self.curve = curve
+ self.interpolator = T.animationInterpolator
+ }
+
+ func start() {
+ self.startTime = CACurrentMediaTime()
+ }
+
+ func update(at timestamp: Double) -> T {
+ guard let startTime = self.startTime else {
+ return self.from
+ }
+ if self.isFinished {
+ return self.to
+ }
+ let fraction = max(0.0, min(1.0, (timestamp - startTime) / self.duration))
+ if timestamp > startTime + self.duration {
+ self.isFinished = true
+ }
+ if fraction >= 1.0 {
+ return self.to
+ }
+ return self.interpolator.interpolate(from: self.from, to: self.to, fraction: self.curve.map(fraction))
+ }
+}
+
+public class AnyAnimatedProperty {
+ var didStartAnimation: (() -> Void)?
+ var hasRunningAnimation: Bool {
+ return false
+ }
+
+ public func update() {
+ }
+}
+
+public final class AnimatedProperty: AnyAnimatedProperty {
+ private var animation: Animation?
+
+ override var hasRunningAnimation: Bool {
+ return self.animation != nil
+ }
+
+ public private(set) var value: T
+
+ public init(_ value: T) {
+ self.value = value
+ }
+
+ public func animate(to: T, duration: Double, curve: AnimationCurve) {
+ let timestamp = CACurrentMediaTime()
+
+ let fromValue: T
+ if let animation = self.animation {
+ fromValue = animation.update(at: timestamp)
+ } else {
+ fromValue = self.value
+ }
+ self.animation = Animation(from: fromValue, to: to, duration: duration, curve: curve)
+ self.animation?.start()
+ self.didStartAnimation?()
+ }
+
+ override public func update() {
+ if let animation = self.animation {
+ self.value = animation.update(at: CACurrentMediaTime())
+ if animation.isFinished {
+ self.animation = nil
+ }
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift
new file mode 100644
index 0000000000..0830499b95
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/AnimationManager.swift
@@ -0,0 +1,50 @@
+import Foundation
+import UIKit
+
+public final class ManagedAnimations {
+ private var displayLinkSubscription: SharedDisplayLink.Subscription?
+
+ private var properties: [AnyAnimatedProperty] = []
+
+ public var updated: (() -> Void)?
+
+ public init() {
+ }
+
+ public func add(property: AnyAnimatedProperty) {
+ self.properties.append(property)
+ property.didStartAnimation = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.updateNeedAnimations()
+ }
+ }
+
+ private func updateNeedAnimations() {
+ if self.displayLinkSubscription == nil {
+ self.displayLinkSubscription = SharedDisplayLink.shared.add { [weak self] in
+ guard let self else {
+ return
+ }
+ self.update()
+ }
+ }
+ }
+
+ private func update() {
+ var hasRunningAnimations = false
+ for property in self.properties {
+ property.update()
+ if property.hasRunningAnimation {
+ hasRunningAnimations = true
+ }
+ }
+
+ if !hasRunningAnimations {
+ self.displayLinkSubscription = nil
+ }
+
+ self.updated?()
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift
new file mode 100644
index 0000000000..4cc8562b9e
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Animation/Bezier.swift
@@ -0,0 +1,53 @@
+import Foundation
+
+private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat
+{
+ return 1.0 - 3.0 * a2 + 3.0 * a1
+}
+
+private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat
+{
+ return 3.0 * a2 - 6.0 * a1
+}
+
+private func c(_ a1: CGFloat) -> CGFloat
+{
+ return 3.0 * a1
+}
+
+private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat
+{
+ return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t
+}
+
+private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat
+{
+ return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
+}
+
+private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat {
+ var t = x
+ var i = 0
+ while i < 4 {
+ let currentSlope = calcSlope(t, x1, x2)
+ if currentSlope == 0.0 {
+ return t
+ } else {
+ let currentX = calcBezier(t, x1, x2) - x
+ t -= currentX / currentSlope
+ }
+
+ i += 1
+ }
+
+ return t
+}
+
+public func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat
+{
+ var value = calcBezier(getTForX(x, x1, x2), y1, y2)
+ if value >= 0.997 {
+ value = 1.0
+ }
+ return value
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift
new file mode 100644
index 0000000000..b0a316f966
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/AvatarLayer.swift
@@ -0,0 +1,40 @@
+import Foundation
+import UIKit
+
+final class AvatarLayer: SimpleLayer {
+ var image: UIImage? {
+ didSet {
+ if let image = self.image {
+ let imageSize = CGSize(width: 136.0, height: 136.0)
+ let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: imageSize), format: .preferred())
+ let image = renderer.image { context in
+ context.cgContext.addEllipse(in: CGRect(origin: CGPoint(), size: imageSize))
+ context.cgContext.clip()
+
+ context.cgContext.translateBy(x: imageSize.width * 0.5, y: imageSize.height * 0.5)
+ context.cgContext.scaleBy(x: 1.0, y: -1.0)
+ context.cgContext.translateBy(x: -imageSize.width * 0.5, y: -imageSize.height * 0.5)
+ context.cgContext.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: imageSize))
+ }
+ self.contents = image.cgImage
+ } else {
+ self.contents = nil
+ }
+ }
+ }
+
+ override init() {
+ super.init()
+ }
+
+ override init(layer: Any) {
+ super.init(layer: layer)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(size: CGSize) {
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift
new file mode 100644
index 0000000000..0d4b159d91
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift
@@ -0,0 +1,126 @@
+import Foundation
+import UIKit
+
+final class ButtonGroupView: UIView, ContentOverlayView {
+ enum Key: Hashable {
+ case audio
+ case video
+ case mic
+ case close
+ }
+
+ let overlayMaskLayer: CALayer
+ private var buttons: [Key: ContentOverlayButton] = [:]
+
+ var toggleVideo: (() -> Void)?
+
+ override init(frame: CGRect) {
+ self.overlayMaskLayer = SimpleLayer()
+
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func addSubview(_ view: UIView) {
+ super.addSubview(view)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func insertSubview(_ view: UIView, at index: Int) {
+ super.insertSubview(view, at: index)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
+ super.insertSubview(view, aboveSubview: siblingSubview)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
+ super.insertSubview(view, belowSubview: siblingSubview)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayMaskLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func willRemoveSubview(_ subview: UIView) {
+ super.willRemoveSubview(subview)
+
+ if let view = subview as? ContentOverlayView {
+ view.overlayMaskLayer.removeFromSuperlayer()
+ }
+ }
+
+ func update(size: CGSize) {
+ var keys: [Key] = []
+ keys.append(.audio)
+ keys.append(.video)
+ keys.append(.mic)
+ keys.append(.close)
+
+ let buttonSize: CGFloat = 56.0
+ let buttonSpacing: CGFloat = 36.0
+
+ let buttonY: CGFloat = size.height - 86.0 - buttonSize
+ var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(keys.count) - buttonSpacing * CGFloat(keys.count - 1)) * 0.5)
+
+ for key in keys {
+ let button: ContentOverlayButton
+ if let current = self.buttons[key] {
+ button = current
+ } else {
+ button = ContentOverlayButton(frame: CGRect())
+ self.addSubview(button)
+ self.buttons[key] = button
+
+ let image: UIImage?
+ switch key {
+ case .audio:
+ image = UIImage(named: "Call/ButtonSpeaker")
+ case .video:
+ image = UIImage(named: "Call/ButtonVideo")
+ button.action = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.toggleVideo?()
+ }
+ case .mic:
+ image = UIImage(named: "Call/ButtonMicMuted")
+ case .close:
+ image = UIImage(named: "Call/ButtonEnd")
+ }
+
+ button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal)
+ button.imageView?.tintColor = .white
+ }
+
+ button.frame = CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize))
+ buttonX += buttonSize + buttonSpacing
+ }
+
+ var removeKeys: [Key] = []
+ for (key, button) in self.buttons {
+ if !keys.contains(key) {
+ removeKeys.append(key)
+ button.removeFromSuperview()
+ }
+ }
+ for key in removeKeys {
+ self.buttons.removeValue(forKey: key)
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift
new file mode 100644
index 0000000000..c6c1b5f497
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift
@@ -0,0 +1,154 @@
+import Foundation
+import MetalKit
+import UIKit
+
+private func shiftArray(array: [SIMD2], offset: Int) -> [SIMD2] {
+ var newArray = array
+ var offset = offset
+ while offset > 0 {
+ let element = newArray.removeFirst()
+ newArray.append(element)
+ offset -= 1
+ }
+ return newArray
+}
+
+private func gatherPositions(_ list: [SIMD2]) -> [SIMD2] {
+ var result: [SIMD2] = []
+ for i in 0 ..< list.count / 2 {
+ result.append(list[i * 2])
+ }
+ return result
+}
+
+private func hexToFloat(_ hex: Int) -> SIMD4 {
+ let red = Float((hex >> 16) & 0xFF) / 255.0
+ let green = Float((hex >> 8) & 0xFF) / 255.0
+ let blue = Float((hex >> 0) & 0xFF) / 255.0
+ return SIMD4(x: red, y: green, z: blue, w: 1.0)
+}
+
+final class CallBackgroundLayer: MetalSubjectLayer, MetalSubject {
+ var internalData: MetalSubjectInternalData?
+
+ final class RenderState: RenderToLayerState {
+ let pipelineState: MTLRenderPipelineState
+
+ init?(device: MTLDevice, library: MTLLibrary) {
+ guard let vertexFunction = library.makeFunction(name: "callBackgroundVertex"), let fragmentFunction = library.makeFunction(name: "callBackgroundFragment") else {
+ return nil
+ }
+
+ let pipelineDescriptor = MTLRenderPipelineDescriptor()
+ pipelineDescriptor.vertexFunction = vertexFunction
+ pipelineDescriptor.fragmentFunction = fragmentFunction
+ pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+ guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
+ return nil
+ }
+ self.pipelineState = pipelineState
+ }
+ }
+
+ private static var basePositions: [SIMD2] = [
+ SIMD2(x: 0.80, y: 0.10),
+ SIMD2(x: 0.60, y: 0.20),
+ SIMD2(x: 0.35, y: 0.25),
+ SIMD2(x: 0.25, y: 0.60),
+ SIMD2(x: 0.20, y: 0.90),
+ SIMD2(x: 0.40, y: 0.80),
+ SIMD2(x: 0.65, y: 0.75),
+ SIMD2(x: 0.75, y: 0.40)
+ ]
+
+ private var isBlur: Bool = false
+ private var phase: Float = 0.0
+
+ private var displayLinkSubscription: SharedDisplayLink.Subscription?
+
+ var renderSpec: RenderLayerSpec?
+
+ init(isBlur: Bool) {
+ self.isBlur = isBlur
+
+ super.init()
+
+ self.didEnterHierarchy = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.displayLinkSubscription = SharedDisplayLink.shared.add(framesPerSecond: .fps(30.0), { [weak self] in
+ guard let self else {
+ return
+ }
+ let stepCount = 8
+ self.phase = (self.phase + 1.0 / 60.0).truncatingRemainder(dividingBy: Float(stepCount))
+ self.setNeedsUpdate()
+ })
+ }
+ self.didExitHierarchy = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.displayLinkSubscription = nil
+ }
+ }
+
+ override init(layer: Any) {
+ super.init(layer: layer)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(context: MetalSubjectContext) {
+ guard let renderSpec = self.renderSpec else {
+ return
+ }
+
+ let isBlur = self.isBlur
+ let phase = self.phase
+
+ context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, commands: { encoder, placement in
+ let effectiveRect = placement.effectiveRect
+
+ var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
+ encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
+
+ let baseStep = floor(phase)
+ let nextStepInterpolation = phase - floor(phase)
+
+ let positions0 = gatherPositions(shiftArray(array: CallBackgroundLayer.basePositions, offset: Int(baseStep)))
+ let positions1 = gatherPositions(shiftArray(array: CallBackgroundLayer.basePositions, offset: Int(baseStep) + 1))
+ var positions = Array>(repeating: SIMD2(), count: 4)
+ for i in 0 ..< 4 {
+ positions[i] = interpolatePoints(positions0[i], positions1[i], at: nextStepInterpolation)
+ }
+ encoder.setFragmentBytes(&positions, length: 4 * MemoryLayout>.size, index: 0)
+
+ var colors: [[SIMD4]] = [
+ [
+ hexToFloat(0x568FD6),
+ hexToFloat(0x626ED5),
+ hexToFloat(0xA667D5),
+ hexToFloat(0x7664DA)
+ ],
+ [
+ hexToFloat(0xACBD65),
+ hexToFloat(0x459F8D),
+ hexToFloat(0x53A4D1),
+ hexToFloat(0x3E917A)
+ ]
+ ]
+
+ encoder.setFragmentBytes(&colors[0], length: 4 * MemoryLayout>.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)
+ encoder.setFragmentBytes(&saturation, length: 4, index: 3)
+
+ encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
+ })
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift
new file mode 100644
index 0000000000..0d6732c4f2
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift
@@ -0,0 +1,154 @@
+import Foundation
+import MetalKit
+
+final class CallBlobsLayer: MetalSubjectLayer, MetalSubject {
+ var internalData: MetalSubjectInternalData?
+
+ struct Blob {
+ var points: [Float]
+ var nextPoints: [Float]
+
+ init(count: Int) {
+ self.points = (0 ..< count).map { _ in
+ Float.random(in: 0.0 ... 1.0)
+ }
+ self.nextPoints = (0 ..< count).map { _ in
+ Float.random(in: 0.0 ... 1.0)
+ }
+ }
+
+ func interpolate(at t: Float) -> [Float] {
+ var points: [Float] = Array(repeating: 0.0, count: self.points.count)
+ for i in 0 ..< self.points.count {
+ points[i] = interpolateFloat(self.points[i], self.nextPoints[i], at: t)
+ }
+ return points
+ }
+
+ mutating func advance() {
+ self.points = self.nextPoints
+ self.nextPoints = (0 ..< self.points.count).map { _ in
+ Float.random(in: 0.0 ... 1.0)
+ }
+ }
+ }
+
+ final class RenderState: RenderToLayerState {
+ final class Input {
+ let rect: CGRect
+ let blobs: [Blob]
+ let phase: Float
+
+ init(rect: CGRect, blobs: [Blob], phase: Float) {
+ self.rect = rect
+ self.blobs = blobs
+ self.phase = phase
+ }
+ }
+
+ let pipelineState: MTLRenderPipelineState
+
+ required init?(
+ device: MTLDevice,
+ library: MTLLibrary
+ ) {
+ guard let vertexFunction = library.makeFunction(name: "callBlobVertex"), let fragmentFunction = library.makeFunction(name: "callBlobFragment") else {
+ return nil
+ }
+
+ let pipelineDescriptor = MTLRenderPipelineDescriptor()
+ pipelineDescriptor.vertexFunction = vertexFunction
+ pipelineDescriptor.fragmentFunction = fragmentFunction
+ pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+ pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
+ pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
+ pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
+ pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
+ pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
+ pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
+ pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
+
+ guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
+ return nil
+ }
+ self.pipelineState = pipelineState
+ }
+ }
+
+ private var phase: Float = 0.0
+
+ private var blobs: [Blob] = []
+
+ private var displayLinkSubscription: SharedDisplayLink.Subscription?
+
+ override init() {
+ super.init()
+
+ self.didEnterHierarchy = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.displayLinkSubscription = SharedDisplayLink.shared.add(framesPerSecond: .fps(30.0), { [weak self] in
+ guard let self else {
+ return
+ }
+ self.phase += 3.0 / 60.0
+ if self.phase >= 1.0 {
+ for i in 0 ..< self.blobs.count {
+ self.blobs[i].advance()
+ }
+ }
+ self.phase = self.phase.truncatingRemainder(dividingBy: 1.0)
+ self.setNeedsUpdate()
+ })
+ }
+ self.didExitHierarchy = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.displayLinkSubscription = nil
+ }
+
+ self.isOpaque = false
+ self.blobs = (0 ..< 2).map { _ in
+ Blob(count: 8)
+ }
+ }
+
+ override init(layer: Any) {
+ super.init(layer: layer)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(context: MetalSubjectContext) {
+ if self.bounds.isEmpty {
+ return
+ }
+
+ let phase = self.phase
+ let blobs = self.blobs
+
+ context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0))), state: RenderState.self, layer: self, commands: { encoder, placement in
+ let rect = placement.effectiveRect
+
+ for i in 0 ..< blobs.count {
+ var points = blobs[i].interpolate(at: phase)
+ var count: Int32 = Int32(points.count)
+
+ let insetFraction: CGFloat = CGFloat(i) * 0.1
+
+ let blobRect = rect.insetBy(dx: insetFraction * 0.5 * rect.width, dy: insetFraction * 0.5 * rect.height)
+ var rect = SIMD4(Float(blobRect.minX), Float(blobRect.minY), Float(blobRect.width), Float(blobRect.height))
+
+ encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
+ encoder.setVertexBytes(&points, length: MemoryLayout.size * points.count, index: 1)
+ encoder.setVertexBytes(&count, length: MemoryLayout.size, index: 2)
+
+ encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3 * 8 * points.count)
+ }
+ })
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift
new file mode 100644
index 0000000000..880a6a41fd
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift
@@ -0,0 +1,111 @@
+import Foundation
+import UIKit
+
+final class ContentOverlayButton: UIButton, ContentOverlayView {
+ var overlayMaskLayer: CALayer {
+ return self.overlayBackgroundLayer
+ }
+
+ override static var layerClass: AnyClass {
+ return MirroringLayer.self
+ }
+
+ private let overlayBackgroundLayer: SimpleLayer
+ private let backgroundLayer: SimpleLayer
+
+ private var internalHighlighted = false
+
+ private var internalHighligthedChanged: (Bool) -> Void = { _ in }
+ var highligthedChanged: (Bool) -> Void = { _ in }
+
+ var action: (() -> Void)?
+
+ override init(frame: CGRect) {
+ self.overlayBackgroundLayer = SimpleLayer()
+ self.backgroundLayer = SimpleLayer()
+
+ super.init(frame: frame)
+
+ self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
+
+ let size: CGFloat = 56.0
+ let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)))
+ self.overlayBackgroundLayer.contents = renderer.image { context in
+ UIGraphicsPushContext(context.cgContext)
+ context.cgContext.setFillColor(UIColor.white.cgColor)
+ context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)))
+ UIGraphicsPopContext()
+ }.cgImage
+
+ self.backgroundLayer.contents = renderer.image { context in
+ UIGraphicsPushContext(context.cgContext)
+ context.cgContext.setFillColor(UIColor.white.withAlphaComponent(0.2).cgColor)
+ context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)))
+ UIGraphicsPopContext()
+ }.cgImage
+ self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: size, height: size))
+
+ (self.layer as? MirroringLayer)?.targetLayer = self.overlayBackgroundLayer
+
+ self.layer.addSublayer(self.backgroundLayer)
+
+ self.internalHighligthedChanged = { [weak self] highlighted in
+ guard let self else {
+ return
+ }
+ if highlighted {
+ self.alpha = 0.5
+ } else {
+ self.alpha = 1.0
+ }
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func buttonPressed() {
+ self.action?()
+ }
+
+ override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
+ if !self.internalHighlighted {
+ self.internalHighlighted = true
+ self.highligthedChanged(true)
+ self.internalHighligthedChanged(true)
+ }
+
+ return super.beginTracking(touch, with: event)
+ }
+
+ override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
+ if self.internalHighlighted {
+ self.internalHighlighted = false
+ self.highligthedChanged(false)
+ self.internalHighligthedChanged(false)
+ }
+
+ super.endTracking(touch, with: event)
+ }
+
+ override func cancelTracking(with event: UIEvent?) {
+ if self.internalHighlighted {
+ self.internalHighlighted = false
+ self.highligthedChanged(false)
+ self.internalHighligthedChanged(false)
+ }
+
+ super.cancelTracking(with: event)
+ }
+
+ override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
+ if self.internalHighlighted {
+ self.internalHighlighted = false
+ self.highligthedChanged(false)
+ self.internalHighligthedChanged(false)
+ }
+
+ super.touchesCancelled(touches, with: event)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift
new file mode 100644
index 0000000000..b735b03a43
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentView.swift
@@ -0,0 +1,176 @@
+import Foundation
+import UIKit
+
+final class ContentView: UIView {
+ private let blobLayer: CallBlobsLayer
+ private let avatarLayer: AvatarLayer
+ private let titleView: TextView
+ private let statusView: StatusView
+ private var emojiView: KeyEmojiView?
+
+ let blurContentsLayer: SimpleLayer
+
+ private let videoLayer: MainVideoLayer
+ private var videoLayerMask: SimpleShapeLayer?
+ private var blurredVideoLayerMask: SimpleShapeLayer?
+
+ private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?
+ private var phase: CGFloat = 0.0
+
+ private var isDisplayingVideo: Bool = false
+ private var videoDisplayFraction = AnimatedProperty(0.0)
+
+ private var videoInput: VideoInput?
+
+ private let managedAnimations: ManagedAnimations
+
+ override init(frame: CGRect) {
+ self.blobLayer = CallBlobsLayer()
+ self.avatarLayer = AvatarLayer()
+
+ self.titleView = TextView()
+ self.statusView = StatusView()
+
+ self.blurContentsLayer = SimpleLayer()
+
+ self.videoLayer = MainVideoLayer()
+
+ self.managedAnimations = ManagedAnimations()
+
+ super.init(frame: frame)
+
+ self.layer.addSublayer(self.blobLayer)
+ self.layer.addSublayer(self.avatarLayer)
+ self.layer.addSublayer(self.videoLayer)
+ self.blurContentsLayer.addSublayer(self.videoLayer.blurredLayer)
+
+ self.addSubview(self.titleView)
+ self.addSubview(self.statusView)
+
+ self.avatarLayer.image = UIImage(named: "test")
+
+ self.managedAnimations.add(property: self.videoDisplayFraction)
+ self.managedAnimations.updated = { [weak self] in
+ guard let self else {
+ return
+ }
+ if let currentLayout = self.currentLayout {
+ self.update(size: currentLayout.size, insets: currentLayout.insets)
+ }
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(size: CGSize, insets: UIEdgeInsets) {
+ self.currentLayout = (size, insets)
+
+ 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)
+ }
+
+ if self.videoInput == nil, let url = Bundle.main.url(forResource: "test2", withExtension: "mp4") {
+ self.videoInput = VideoInput(device: MetalContext.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)
+ 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 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 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)
+ 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 blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
+
+ self.avatarLayer.position = CGPoint(x: avatarFrame.midX, y: avatarFrame.midY)
+ self.avatarLayer.bounds = CGRect(origin: CGPoint(), size: avatarFrame.size)
+
+ let visibleAvatarScale = CGFloat.animationInterpolator.interpolate(from: 1.0 + avatarScale, to: expandedVideoRadius * 2.0 / avatarSize, fraction: self.videoDisplayFraction.value)
+ self.avatarLayer.transform = CATransform3DMakeScale(visibleAvatarScale, visibleAvatarScale, 1.0)
+ self.avatarLayer.opacity = Float(1.0 - self.videoDisplayFraction.value)
+
+ self.blobLayer.position = CGPoint(x: blobFrame.midX, y: blobFrame.midY)
+ self.blobLayer.bounds = CGRect(origin: CGPoint(), size: blobFrame.size)
+ 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 videoRenderingSize = CGSize(width: videoResolution.width * 2.0, height: videoResolution.height * 2.0)
+
+ self.videoLayer.frame = videoFrame
+ self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoRenderingSize.width), height: Int(videoRenderingSize.height)))
+
+ self.videoLayer.blurredLayer.frame = videoFrame
+
+ let videoDisplayFraction = self.videoDisplayFraction.value
+
+ self.videoLayer.isHidden = videoDisplayFraction == 0.0
+ self.videoLayer.opacity = Float(videoDisplayFraction)
+
+ self.videoLayer.blurredLayer.isHidden = videoDisplayFraction == 0.0
+
+ if videoDisplayFraction != 0.0 && videoDisplayFraction != 1.0 {
+ let videoLayerMask: SimpleShapeLayer
+ if let current = self.videoLayerMask {
+ videoLayerMask = current
+ } else {
+ videoLayerMask = SimpleShapeLayer()
+ self.videoLayerMask = videoLayerMask
+ self.videoLayer.mask = videoLayerMask
+ }
+
+ let blurredVideoLayerMask: SimpleShapeLayer
+ if let current = self.blurredVideoLayerMask {
+ blurredVideoLayerMask = current
+ } else {
+ blurredVideoLayerMask = SimpleShapeLayer()
+ self.blurredVideoLayerMask = blurredVideoLayerMask
+ self.videoLayer.blurredLayer.mask = blurredVideoLayerMask
+ }
+
+ let fromRadius: CGFloat = avatarSize * 0.5
+ let toRadius = expandedVideoRadius
+
+ let maskPosition = CGPoint(x: avatarFrame.midX, y: avatarFrame.midY)
+ let maskRadius = CGFloat.animationInterpolator.interpolate(from: fromRadius, to: toRadius, fraction: videoDisplayFraction)
+
+ videoLayerMask.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: maskPosition.x - maskRadius, y: maskPosition.y - maskRadius), size: CGSize(width: maskRadius * 2.0, height: maskRadius * 2.0))).cgPath
+ blurredVideoLayerMask.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: maskPosition.x - maskRadius, y: maskPosition.y - maskRadius), size: CGSize(width: maskRadius * 2.0, height: maskRadius * 2.0))).cgPath
+ } else {
+ if let videoLayerMask = self.videoLayerMask {
+ self.videoLayerMask = nil
+ videoLayerMask.removeFromSuperlayer()
+ }
+ if let blurredVideoLayerMask = self.blurredVideoLayerMask {
+ self.blurredVideoLayerMask = nil
+ blurredVideoLayerMask.removeFromSuperlayer()
+ }
+ }
+ }
+
+ func toggleDisplayVideo() {
+ self.isDisplayingVideo = !self.isDisplayingVideo
+ self.videoDisplayFraction.animate(to: self.isDisplayingVideo ? 1.0 : 0.0, duration: 0.4, curve: .spring)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift
new file mode 100644
index 0000000000..8654f96a73
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/KeyEmojiView.swift
@@ -0,0 +1,43 @@
+import Foundation
+import UIKit
+
+final class KeyEmojiView: UIView {
+ private let emojiViews: [TextView]
+
+ let size: CGSize
+
+ init(emoji: [String]) {
+ self.emojiViews = emoji.map { emoji in
+ TextView()
+ }
+
+ let itemSpacing: CGFloat = 3.0
+
+ var height: CGFloat = 0.0
+ var nextX = 0.0
+ for i in 0 ..< self.emojiViews.count {
+ if nextX != 0.0 {
+ nextX += itemSpacing
+ }
+ let emojiView = self.emojiViews[i]
+ let itemSize = emojiView.update(string: emoji[i], fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 100.0)
+ if height == 0.0 {
+ height = itemSize.height
+ }
+ emojiView.frame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize)
+ nextX += itemSize.width
+ }
+
+ self.size = CGSize(width: nextX, height: height)
+
+ super.init(frame: CGRect())
+
+ for emojiView in self.emojiViews {
+ self.addSubview(emojiView)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift
new file mode 100644
index 0000000000..c360549739
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MainVideoLayer.swift
@@ -0,0 +1,256 @@
+import Foundation
+import UIKit
+import MetalKit
+import MetalPerformanceShaders
+import Accelerate
+
+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 {
+ return nil
+ }
+
+ let width = cgImage.width
+ let height = cgImage.width
+
+ var pixelBuffer: CVPixelBuffer? = nil
+ let _ = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, [
+ kCVPixelBufferIOSurfacePropertiesKey: NSDictionary()
+ ] as CFDictionary, &pixelBuffer)
+ guard let pixelBuffer else {
+ return nil
+ }
+
+ CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
+ defer {
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
+ }
+ guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
+ return nil
+ }
+
+ var srcBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bytes), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: cgImage.bytesPerRow)
+ var dstBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddress), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: CVPixelBufferGetBytesPerRow(pixelBuffer))
+
+ vImageCopyBuffer(&srcBuffer, &dstBuffer, 4, vImage_Flags(kvImageDoNotTile))
+
+ return pixelBuffer
+}
+
+final class MainVideoLayer: MetalSubjectLayer, MetalSubject {
+ var internalData: MetalSubjectInternalData?
+
+ let blurredLayer: MetalSubjectLayer
+
+ final class BlurState: ComputeState {
+ let computePipelineStateYUVToRGBA: MTLComputePipelineState
+ let computePipelineStateHorizontal: MTLComputePipelineState
+ let computePipelineStateVertical: MTLComputePipelineState
+ let downscaleKernel: MPSImageBilinearScale
+
+ required init?(
+ device: MTLDevice,
+ library: MTLLibrary
+ ) {
+ guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
+ return nil
+ }
+ guard let computePipelineStateYUVToRGBA = try? device.makeComputePipelineState(function: functionVideoYUVToRGBA) else {
+ return nil
+ }
+ self.computePipelineStateYUVToRGBA = computePipelineStateYUVToRGBA
+
+ guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else {
+ return nil
+ }
+ guard let computePipelineStateHorizontal = try? device.makeComputePipelineState(function: gaussianBlurHorizontal) else {
+ return nil
+ }
+ self.computePipelineStateHorizontal = computePipelineStateHorizontal
+
+ guard let computePipelineStateVertical = try? device.makeComputePipelineState(function: gaussianBlurVertical) else {
+ return nil
+ }
+ self.computePipelineStateVertical = computePipelineStateVertical
+
+ self.downscaleKernel = MPSImageBilinearScale(device: device)
+ }
+ }
+
+ final class RenderState: RenderToLayerState {
+ let pipelineState: MTLRenderPipelineState
+
+ required init?(
+ device: MTLDevice,
+ library: MTLLibrary
+ ) {
+ guard let vertexFunction = library.makeFunction(name: "mainVideoVertex"), let fragmentFunction = library.makeFunction(name: "mainVideoFragment") else {
+ return nil
+ }
+
+ let pipelineDescriptor = MTLRenderPipelineDescriptor()
+ pipelineDescriptor.vertexFunction = vertexFunction
+ pipelineDescriptor.fragmentFunction = fragmentFunction
+ pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+ guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
+ return nil
+ }
+ self.pipelineState = pipelineState
+ }
+ }
+
+ var video: VideoInput? {
+ didSet {
+ self.video?.updated = { [weak self] in
+ self?.setNeedsUpdate()
+ }
+ self.setNeedsUpdate()
+ }
+ }
+
+ var renderSpec: RenderLayerSpec?
+
+ private var rgbaTexture: PooledTexture?
+ private var downscaledTexture: PooledTexture?
+ private var blurredHorizontalTexture: PooledTexture?
+ private var blurredVerticalTexture: PooledTexture?
+
+ override init() {
+ self.blurredLayer = MetalSubjectLayer()
+
+ super.init()
+ }
+
+ override init(layer: Any) {
+ self.blurredLayer = MetalSubjectLayer()
+
+ super.init(layer: layer)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(context: MetalSubjectContext) {
+ if self.isHidden {
+ return
+ }
+ guard let renderSpec = self.renderSpec else {
+ return
+ }
+ guard let videoTextures = self.video?.currentOutput else {
+ return
+ }
+
+ 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)
+ }
+ if self.downscaledTexture == nil {
+ self.downscaledTexture = MetalContext.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))
+ }
+ if self.blurredVerticalTexture == nil {
+ self.blurredVerticalTexture = MetalContext.shared.pooledTexture(spec: TextureSpec(width: 128, height: 128, pixelFormat: .rgba8UnsignedNormalized))
+ }
+
+ guard let rgbaTexture = self.rgbaTexture?.get(context: context) else {
+ return
+ }
+
+ let _ = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture in
+ guard let rgbaTexture else {
+ return
+ }
+ guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
+ return
+ }
+
+ let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
+ let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
+
+ computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVToRGBA)
+ computeEncoder.setTexture(videoTextures.y, index: 0)
+ computeEncoder.setTexture(videoTextures.uv, index: 1)
+ computeEncoder.setTexture(rgbaTexture, index: 2)
+ computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
+
+ computeEncoder.endEncoding()
+ })
+
+ if !self.blurredLayer.isHidden {
+ guard let downscaledTexture = self.downscaledTexture?.get(context: context), let blurredHorizontalTexture = self.blurredHorizontalTexture?.get(context: context), let blurredVerticalTexture = self.blurredVerticalTexture?.get(context: context) else {
+ return
+ }
+
+ let blurredTexture = context.compute(state: BlurState.self, inputs: rgbaTexture.placeholer, downscaledTexture.placeholer, blurredHorizontalTexture.placeholer, blurredVerticalTexture.placeholer, commands: { commandBuffer, blurState, rgbaTexture, downscaledTexture, blurredHorizontalTexture, blurredVerticalTexture -> MTLTexture? in
+ guard let rgbaTexture, let downscaledTexture, let blurredHorizontalTexture, let blurredVerticalTexture else {
+ return nil
+ }
+
+ blurState.downscaleKernel.encode(commandBuffer: commandBuffer, sourceTexture: rgbaTexture, destinationTexture: downscaledTexture)
+
+ do {
+ guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
+ return nil
+ }
+
+ let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
+ let threadgroupCount = MTLSize(width: (downscaledTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (downscaledTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
+
+ computeEncoder.setComputePipelineState(blurState.computePipelineStateHorizontal)
+ computeEncoder.setTexture(downscaledTexture, index: 0)
+ computeEncoder.setTexture(blurredHorizontalTexture, index: 1)
+ computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
+
+ computeEncoder.setComputePipelineState(blurState.computePipelineStateVertical)
+ computeEncoder.setTexture(blurredHorizontalTexture, index: 0)
+ computeEncoder.setTexture(blurredVerticalTexture, index: 1)
+ computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
+
+ computeEncoder.endEncoding()
+ }
+
+ return blurredVerticalTexture
+ })
+
+ context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self.blurredLayer, inputs: blurredTexture, commands: { encoder, placement, blurredTexture in
+ guard let blurredTexture else {
+ return
+ }
+ let effectiveRect = placement.effectiveRect
+
+ var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
+ encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
+ encoder.setFragmentTexture(blurredTexture, index: 0)
+
+ var brightness: Float = 1.4
+ var saturation: Float = 1.1
+ encoder.setFragmentBytes(&brightness, length: 4, index: 0)
+ encoder.setFragmentBytes(&saturation, length: 4, index: 1)
+
+ encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
+ })
+ }
+
+ context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: rgbaTexture.placeholer, commands: { encoder, placement, rgbaTexture in
+ guard let rgbaTexture else {
+ return
+ }
+
+ let effectiveRect = placement.effectiveRect
+
+ var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
+ encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
+ encoder.setFragmentTexture(rgbaTexture, index: 0)
+
+ var brightness: Float = 1.0
+ var saturation: Float = 1.0
+ encoder.setFragmentBytes(&brightness, length: 4, index: 0)
+ encoder.setFragmentBytes(&saturation, length: 4, index: 1)
+
+ encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
+ })
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift
new file mode 100644
index 0000000000..24b6f5ba7d
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift
@@ -0,0 +1,140 @@
+import Foundation
+import UIKit
+
+private final class AnimatedDotsLayer: SimpleLayer {
+ private let dotLayers: [SimpleLayer]
+
+ let size: CGSize
+
+ override init() {
+ self.dotLayers = (0 ..< 3).map { _ in
+ SimpleLayer()
+ }
+
+ let dotSpacing: CGFloat = 1.0
+ let dotSize = CGSize(width: 5.0, height: 5.0)
+
+ self.size = CGSize(width: CGFloat(self.dotLayers.count) * dotSize.width + CGFloat(self.dotLayers.count - 1) * dotSpacing, height: dotSize.height)
+
+ super.init()
+
+ let dotImage = UIGraphicsImageRenderer(size: dotSize).image(actions: { context in
+ context.cgContext.setFillColor(UIColor.white.cgColor)
+ context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: dotSize))
+ })
+
+ var nextX: CGFloat = 0.0
+ for dotLayer in self.dotLayers {
+ dotLayer.contents = dotImage.cgImage
+ dotLayer.frame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: dotSize)
+ nextX += dotSpacing + dotSize.width
+ self.addSublayer(dotLayer)
+ }
+
+ self.didEnterHierarchy = { [weak self] in
+ self?.updateAnimations()
+ }
+ }
+
+ override init(layer: Any) {
+ self.dotLayers = []
+ self.size = CGSize()
+
+ super.init(layer: layer)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func updateAnimations() {
+ if self.dotLayers[0].animation(forKey: "dotAnimation") != nil {
+ return
+ }
+
+ let animationDuration: Double = 0.6
+ for i in 0 ..< self.dotLayers.count {
+ let dotLayer = self.dotLayers[i]
+
+ let animation = CABasicAnimation(keyPath: "transform.scale")
+ animation.duration = animationDuration
+ animation.fromValue = 0.3
+ animation.toValue = 1.0
+ animation.timingFunction = CAMediaTimingFunction(name: .linear)
+ animation.autoreverses = true
+ animation.repeatCount = .infinity
+ animation.timeOffset = CGFloat(self.dotLayers.count - 1 - i) * animationDuration * 0.33
+
+ dotLayer.add(animation, forKey: "dotAnimation")
+ }
+ }
+}
+
+final class StatusView: UIView {
+ enum WaitingState {
+ case requesting
+ case ringing
+ case generatingKeys
+ }
+ enum State {
+ case waiting(WaitingState)
+ }
+
+ private var textView: TextView
+ private var dotsLayer: AnimatedDotsLayer?
+
+ override init(frame: CGRect) {
+ self.textView = TextView()
+
+ super.init(frame: frame)
+
+ self.addSubview(self.textView)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(state: State) -> CGSize {
+ let textString: String
+ var needsDots = false
+ switch state {
+ case let .waiting(waitingState):
+ needsDots = true
+
+ switch waitingState {
+ case .requesting:
+ textString = "Requesting"
+ case .ringing:
+ textString = "Ringing"
+ case .generatingKeys:
+ textString = "Exchanging encryption keys"
+ }
+ }
+ 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)
+
+ var contentSize = textSize
+
+ let dotsSpacing: CGFloat = 6.0
+
+ if needsDots {
+ let dotsLayer: AnimatedDotsLayer
+ if let current = self.dotsLayer {
+ dotsLayer = current
+ } else {
+ dotsLayer = AnimatedDotsLayer()
+ self.dotsLayer = dotsLayer
+ self.layer.addSublayer(dotsLayer)
+ }
+
+ dotsLayer.frame = CGRect(origin: CGPoint(x: textSize.width + dotsSpacing, y: 1.0 + floor((textSize.height - dotsLayer.size.height) * 0.5)), size: dotsLayer.size)
+ contentSize.width += dotsSpacing + dotsLayer.size.width
+ } else if let dotsLayer = self.dotsLayer {
+ self.dotsLayer = nil
+ dotsLayer.removeFromSuperlayer()
+ }
+
+ return contentSize
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift
new file mode 100644
index 0000000000..d3ba5c58e1
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView.swift
@@ -0,0 +1,63 @@
+import Foundation
+import UIKit
+
+final class TextView: UIView {
+ private struct Params: Equatable {
+ var string: String
+ var fontSize: CGFloat
+ var fontWeight: CGFloat
+ var constrainedWidth: CGFloat
+ }
+
+ private struct LayoutState: Equatable {
+ var params: Params
+ var size: CGSize
+ var attributedString: NSAttributedString
+ }
+
+ private var layoutState: LayoutState?
+
+ override init(frame: CGRect) {
+ super.init(frame: CGRect())
+
+ self.isOpaque = false
+ self.backgroundColor = nil
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(string: String, fontSize: CGFloat, fontWeight: CGFloat, constrainedWidth: CGFloat) -> CGSize {
+ let params = Params(string: string, fontSize: fontSize, fontWeight: fontWeight, constrainedWidth: constrainedWidth)
+ if let layoutState = self.layoutState, layoutState.params == params {
+ return layoutState.size
+ }
+
+ let font = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight(fontWeight))
+
+ let attributedString = NSAttributedString(string: string, attributes: [
+ .font: font,
+ .foregroundColor: UIColor.white
+ ])
+ let stringBounds = attributedString.boundingRect(with: CGSize(width: constrainedWidth, height: 200.0), options: .usesLineFragmentOrigin, context: nil)
+ let stringSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
+ let size = CGSize(width: min(constrainedWidth, stringSize.width), height: stringSize.height)
+
+ let layoutState = LayoutState(params: params, size: size, attributedString: attributedString)
+ if self.layoutState != layoutState {
+ self.layoutState = layoutState
+ self.setNeedsDisplay()
+ }
+
+ return size
+ }
+
+ override func draw(_ rect: CGRect) {
+ guard let layoutState = self.layoutState else {
+ return
+ }
+
+ layoutState.attributedString.draw(with: rect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], context: nil)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift
new file mode 100644
index 0000000000..460f003478
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayContainer.swift
@@ -0,0 +1,60 @@
+import Foundation
+import UIKit
+
+protocol ContentOverlayView: UIView {
+ var overlayMaskLayer: CALayer { get }
+}
+
+final class ContentOverlayContainer: UIView {
+ private let overlayLayer: ContentOverlayLayer
+
+ init(overlayLayer: ContentOverlayLayer) {
+ self.overlayLayer = overlayLayer
+
+ super.init(frame: CGRect())
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func addSubview(_ view: UIView) {
+ super.addSubview(view)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func insertSubview(_ view: UIView, at index: Int) {
+ super.insertSubview(view, at: index)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
+ super.insertSubview(view, aboveSubview: siblingSubview)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
+ super.insertSubview(view, belowSubview: siblingSubview)
+
+ if let view = view as? ContentOverlayView {
+ self.overlayLayer.maskContentLayer.addSublayer(view.overlayMaskLayer)
+ }
+ }
+
+ override func willRemoveSubview(_ subview: UIView) {
+ super.willRemoveSubview(subview)
+
+ if let view = subview as? ContentOverlayView {
+ view.overlayMaskLayer.removeFromSuperlayer()
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift
new file mode 100644
index 0000000000..7e29a77a13
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/ContentOverlayLayer.swift
@@ -0,0 +1,71 @@
+import Foundation
+import UIKit
+
+final class ContentOverlayLayer: SimpleLayer {
+ private struct Params: Equatable {
+ var size: CGSize
+ var contentInsets: UIEdgeInsets
+
+ init(size: CGSize, contentInsets: UIEdgeInsets) {
+ self.size = size
+ self.contentInsets = contentInsets
+ }
+ }
+
+ var contentsLayer: CALayer? {
+ didSet {
+ if self.contentsLayer !== oldValue {
+ oldValue?.mask = nil
+ oldValue?.removeFromSuperlayer()
+
+ if let contentsLayer = self.contentsLayer {
+ contentsLayer.mask = self.maskContentLayer
+ self.addSublayer(contentsLayer)
+
+ if let params = self.params {
+ let size = params.size
+ let contentInsets = params.contentInsets
+ self.params = nil
+ self.update(size: size, contentInsets: contentInsets)
+ }
+ }
+ }
+ }
+ }
+
+ let maskContentLayer: SimpleLayer
+
+ private var params: Params?
+
+ override init() {
+ self.maskContentLayer = SimpleLayer()
+
+ super.init()
+ }
+
+ override init(layer: Any) {
+ self.maskContentLayer = SimpleLayer()
+
+ super.init(layer: layer)
+ }
+
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(size: CGSize, contentInsets: UIEdgeInsets) {
+ let params = Params(size: size, contentInsets: contentInsets)
+ if self.params == params {
+ return
+ }
+ self.params = params
+
+ let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
+
+ self.maskContentLayer.frame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: size)
+
+ if let contentsLayer = self.contentsLayer {
+ contentsLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width - (contentInsets.left + contentInsets.right), height: size.height - (contentInsets.top + contentInsets.bottom)))
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift
new file mode 100644
index 0000000000..c00a5ebe2d
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift
@@ -0,0 +1,118 @@
+import AVFoundation
+import Metal
+import CoreVideo
+
+class VideoInput {
+ final class Output {
+ let y: MTLTexture
+ let uv: MTLTexture
+
+ init(y: MTLTexture, uv: MTLTexture) {
+ self.y = y
+ self.uv = uv
+ }
+ }
+
+ private let playerLooper: AVPlayerLooper
+ private let queuePlayer: AVQueuePlayer
+
+ private var videoOutput: AVPlayerItemVideoOutput
+ private var device: MTLDevice
+ private var textureCache: CVMetalTextureCache?
+
+ private var targetItem: AVPlayerItem?
+
+ private(set) var currentOutput: Output?
+ var updated: (() -> Void)?
+
+ private var displayLink: SharedDisplayLink.Subscription?
+
+ init?(device: MTLDevice, url: URL) {
+ self.device = device
+ CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
+
+ let playerItem = AVPlayerItem(url: url)
+ self.queuePlayer = AVQueuePlayer(playerItem: playerItem)
+ self.playerLooper = AVPlayerLooper(player: self.queuePlayer, templateItem: playerItem)
+
+ let outputSettings: [String: Any] = [
+ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
+ kCVPixelBufferMetalCompatibilityKey as String: true
+ ]
+ self.videoOutput = AVPlayerItemVideoOutput(outputSettings: outputSettings)
+
+ self.queuePlayer.play()
+
+ self.displayLink = SharedDisplayLink.shared.add(framesPerSecond: .fps(60.0), { [weak self] in
+ guard let self else {
+ return
+ }
+ if self.updateOutput() {
+ self.updated?()
+ }
+ })
+ }
+
+ private func updateOutput() -> Bool {
+ if self.targetItem !== self.queuePlayer.currentItem {
+ self.targetItem?.remove(self.videoOutput)
+ self.targetItem = self.queuePlayer.currentItem
+ if let targetItem = self.targetItem {
+ targetItem.add(self.videoOutput)
+ }
+ }
+
+ guard let currentItem = self.targetItem else {
+ return false
+ }
+
+ let currentTime = currentItem.currentTime()
+ guard self.videoOutput.hasNewPixelBuffer(forItemTime: currentTime) else {
+ return false
+ }
+
+ var pixelBuffer: CVPixelBuffer?
+ pixelBuffer = self.videoOutput.copyPixelBuffer(forItemTime: currentTime, itemTimeForDisplay: nil)
+
+ guard let buffer = pixelBuffer else {
+ return false
+ }
+
+ let width = CVPixelBufferGetWidth(buffer)
+ let height = CVPixelBufferGetHeight(buffer)
+
+ var cvMetalTextureY: CVMetalTexture?
+ var status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache!, buffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY)
+ guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else {
+ return false
+ }
+ var cvMetalTextureUV: CVMetalTexture?
+ status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache!, buffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV)
+ guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else {
+ return false
+ }
+
+ self.currentOutput = Output(y: yTexture, uv: uvTexture)
+ return true
+ }
+}
+
+class ControlVideoInput {
+ private let playerLooper: AVPlayerLooper
+ private let queuePlayer: AVQueuePlayer
+
+ private let playerLayer: AVPlayerLayer
+
+ private var targetItem: AVPlayerItem?
+
+ init(url: URL, playerLayer: AVPlayerLayer) {
+ let playerItem = AVPlayerItem(url: url)
+ self.queuePlayer = AVQueuePlayer(playerItem: playerItem)
+ self.playerLooper = AVPlayerLooper(player: self.queuePlayer, templateItem: playerItem)
+
+ self.playerLayer = playerLayer
+ playerLayer.player = self.queuePlayer
+
+ self.queuePlayer.play()
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift
new file mode 100644
index 0000000000..410c8ddb37
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/MirroringLayer.swift
@@ -0,0 +1,97 @@
+import Foundation
+import UIKit
+
+final class MirroringLayer: SimpleLayer {
+ var targetLayer: SimpleLayer?
+
+ override init() {
+ super.init()
+ }
+
+ override init(layer: Any) {
+ super.init(layer: layer)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override var position: CGPoint {
+ get {
+ return super.position
+ } set(value) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.position = value
+ }
+ super.position = value
+ }
+ }
+
+ override var bounds: CGRect {
+ get {
+ return super.bounds
+ } set(value) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.bounds = value
+ }
+ super.bounds = value
+ }
+ }
+
+ override var opacity: Float {
+ get {
+ return super.opacity
+ } set(value) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.opacity = value
+ }
+ super.opacity = value
+ }
+ }
+
+ override public var sublayerTransform: CATransform3D {
+ get {
+ return super.sublayerTransform
+ } set(value) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.sublayerTransform = value
+ }
+ super.sublayerTransform = value
+ }
+ }
+
+ override public var transform: CATransform3D {
+ get {
+ return super.transform
+ } set(value) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.transform = value
+ }
+ super.transform = value
+ }
+ }
+
+ override public func add(_ animation: CAAnimation, forKey key: String?) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.add(animation, forKey: key)
+ }
+
+ super.add(animation, forKey: key)
+ }
+
+ override public func removeAllAnimations() {
+ if let targetLayer = self.targetLayer {
+ targetLayer.removeAllAnimations()
+ }
+
+ super.removeAllAnimations()
+ }
+
+ override public func removeAnimation(forKey: String) {
+ if let targetLayer = self.targetLayer {
+ targetLayer.removeAnimation(forKey: forKey)
+ }
+
+ super.removeAnimation(forKey: forKey)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift
new file mode 100644
index 0000000000..1653cbb78f
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift
@@ -0,0 +1,94 @@
+import Foundation
+import UIKit
+
+public final class PrivateCallScreen: UIView {
+ private let backgroundLayer: CallBackgroundLayer
+ private let contentOverlayLayer: ContentOverlayLayer
+ private let contentOverlayContainer: ContentOverlayContainer
+
+ private let blurContentsLayer: SimpleLayer
+ private let blurBackgroundLayer: CallBackgroundLayer
+
+ private let contentView: ContentView
+
+ private let buttonGroupView: ButtonGroupView
+
+ public override init(frame: CGRect) {
+ self.blurContentsLayer = SimpleLayer()
+
+ self.backgroundLayer = CallBackgroundLayer(isBlur: false)
+
+ self.contentOverlayLayer = ContentOverlayLayer()
+ self.contentOverlayContainer = ContentOverlayContainer(overlayLayer: self.contentOverlayLayer)
+
+ self.blurBackgroundLayer = CallBackgroundLayer(isBlur: true)
+
+ self.contentView = ContentView(frame: CGRect())
+
+ self.buttonGroupView = ButtonGroupView()
+
+ super.init(frame: frame)
+
+ self.contentOverlayLayer.contentsLayer = self.blurContentsLayer
+
+ self.layer.addSublayer(self.backgroundLayer)
+
+ self.blurContentsLayer.addSublayer(self.blurBackgroundLayer)
+
+ self.addSubview(self.contentView)
+ self.blurContentsLayer.addSublayer(self.contentView.blurContentsLayer)
+
+ self.layer.addSublayer(self.contentOverlayLayer)
+
+ self.addSubview(self.contentOverlayContainer)
+
+ self.contentOverlayContainer.addSubview(self.buttonGroupView)
+
+ self.buttonGroupView.toggleVideo = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.contentView.toggleDisplayVideo()
+ }
+ }
+
+ public required init?(coder: NSCoder) {
+ fatalError()
+ }
+
+ override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ guard let result = super.hitTest(point, with: event) else {
+ return nil
+ }
+
+ return result
+ }
+
+ public func update(size: CGSize, insets: UIEdgeInsets) {
+ let backgroundFrame = CGRect(origin: CGPoint(), size: size)
+
+ let aspect: CGFloat = size.width / size.height
+ let sizeNorm: CGFloat = 64.0
+ let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
+ let edgeSize: Int = 2
+
+ let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(edgeSize) / renderingSize.width * backgroundFrame.width, dy: -CGFloat(edgeSize) / renderingSize.height * backgroundFrame.height)
+
+ 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())
+
+ self.contentOverlayContainer.frame = CGRect(origin: CGPoint(), size: size)
+
+ self.blurBackgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
+ self.blurBackgroundLayer.frame = visualBackgroundFrame
+
+ self.buttonGroupView.frame = CGRect(origin: CGPoint(), size: size)
+ self.buttonGroupView.update(size: size)
+
+ self.contentView.frame = CGRect(origin: CGPoint(), size: size)
+ self.contentView.update(size: size, insets: insets)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift
new file mode 100644
index 0000000000..03b45b95a9
--- /dev/null
+++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Rendering/MetalLayerContext.swift
@@ -0,0 +1,921 @@
+import Foundation
+import Metal
+import UIKit
+import IOSurface
+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 {
+ var contents: Resolved?
+}
+
+private let noInputPlaceholder: Placeholder = {
+ let value = Placeholder()
+ value.contents = Void()
+ return value
+}()
+
+private struct PlaceholderResolveError: Error {
+}
+
+private func resolvePlaceholder(_ value: Placeholder) throws -> T {
+ guard let contents = value.contents else {
+ throw PlaceholderResolveError()
+ }
+ return contents
+}
+
+public struct TextureSpec: Equatable {
+ public enum PixelFormat {
+ case r8UnsignedNormalized
+ case rgba8UnsignedNormalized
+ }
+
+ public var width: Int
+ public var height: Int
+ public var pixelFormat: PixelFormat
+
+ public init(width: Int, height: Int, pixelFormat: PixelFormat) {
+ self.width = width
+ self.height = height
+ self.pixelFormat = pixelFormat
+ }
+}
+
+extension TextureSpec.PixelFormat {
+ var metalFormat: MTLPixelFormat {
+ switch self {
+ case .r8UnsignedNormalized:
+ return .r8Unorm
+ case .rgba8UnsignedNormalized:
+ return .rgba8Unorm
+ }
+ }
+}
+
+public final class TexturePlaceholder {
+ public let placeholer: Placeholder
+ public let spec: TextureSpec
+
+ init(placeholer: Placeholder, spec: TextureSpec) {
+ self.placeholer = placeholer
+ self.spec = spec
+ }
+}
+
+public struct RenderSize: Equatable {
+ public var width: Int
+ public var height: Int
+
+ public init(width: Int, height: Int) {
+ self.width = width
+ self.height = height
+ }
+}
+
+public struct RenderLayerSpec: Equatable {
+ public var size: RenderSize
+ public var edgeInset: Int
+
+ public init(size: RenderSize, edgeInset: Int = 0) {
+ self.size = size
+ self.edgeInset = edgeInset
+ }
+}
+
+private extension RenderLayerSpec {
+ var allocationWidth: Int {
+ return self.size.width + self.edgeInset * 2
+ }
+
+ var allocationHeight: Int {
+ return self.size.height + self.edgeInset * 2
+ }
+}
+
+public struct RenderLayerPlacement: Equatable {
+ public var effectiveRect: CGRect
+
+ public init(effectiveRect: CGRect) {
+ self.effectiveRect = effectiveRect
+ }
+}
+
+public protocol RenderToLayerState: AnyObject {
+ var pipelineState: MTLRenderPipelineState { get }
+
+ init?(device: MTLDevice, library: MTLLibrary)
+}
+
+public protocol ComputeState: AnyObject {
+ init?(device: MTLDevice, library: MTLLibrary)
+}
+
+open class MetalSubjectLayer: SimpleLayer {
+ fileprivate var internalId: Int = -1
+ fileprivate var surfaceAllocation: MetalContext.SurfaceAllocation?
+
+ public override init() {
+ super.init()
+
+ self.setNeedsDisplay()
+ }
+
+ override public init(layer: Any) {
+ super.init(layer: layer)
+ }
+
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override public func setNeedsDisplay() {
+ if let subject = self as? MetalSubject {
+ subject.setNeedsUpdate()
+ }
+ }
+}
+
+public final class PooledTexture {
+ final class Texture {
+ let value: MTLTexture
+ var isInUse: Bool = false
+
+ init(value: MTLTexture) {
+ self.value = value
+ }
+ }
+
+ public let spec: TextureSpec
+
+ private let textures: [Texture]
+
+ init(device: MTLDevice, spec: TextureSpec) {
+ self.spec = spec
+
+ self.textures = (0 ..< 3).compactMap { _ -> Texture? in
+ let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: spec.pixelFormat.metalFormat, width: spec.width, height: spec.height, mipmapped: false)
+ descriptor.storageMode = .private
+ descriptor.usage = [.shaderRead, .shaderWrite]
+
+ guard let texture = device.makeTexture(descriptor: descriptor) else {
+ return nil
+ }
+ return Texture(value: texture)
+ }
+ }
+
+ public func get(context: MetalSubjectContext) -> TexturePlaceholder? {
+ #if DEBUG
+ if context.freeResourcesOnCompletion.contains(where: { $0 === self }) {
+ assertionFailure("Trying to get PooledTexture more than once per update cycle")
+ }
+ #endif
+
+ for texture in self.textures {
+ if !texture.isInUse {
+ texture.isInUse = true
+ let placeholder = Placeholder()
+ placeholder.contents = texture.value
+ context.freeResourcesOnCompletion.append(texture)
+ return TexturePlaceholder(placeholer: placeholder, spec: self.spec)
+ }
+ }
+
+ print("PooledTexture: all textures are in use")
+ return nil
+ }
+}
+
+public final class MetalSubjectContext {
+ fileprivate final class ComputeOperation {
+ let commands: (MTLCommandBuffer) -> Void
+
+ init(commands: @escaping (MTLCommandBuffer) -> Void) {
+ self.commands = commands
+ }
+ }
+
+ fileprivate final class RenderToLayerOperation {
+ let spec: RenderLayerSpec
+ let state: RenderToLayerState
+ weak var layer: MetalSubjectLayer?
+ let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
+
+ init(
+ spec: RenderLayerSpec,
+ state: RenderToLayerState,
+ layer: MetalSubjectLayer,
+ commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
+ ) {
+ self.spec = spec
+ self.state = state
+ self.layer = layer
+ self.commands = commands
+ }
+ }
+
+ private let device: MTLDevice
+ private let impl: MetalContext.Impl
+
+ fileprivate var computeOperations: [ComputeOperation] = []
+ fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
+ fileprivate var freeResourcesOnCompletion: [PooledTexture.Texture] = []
+
+ fileprivate init(device: MTLDevice, impl: MetalContext.Impl) {
+ self.device = device
+ self.impl = impl
+ }
+
+ public func renderToLayer(
+ spec: RenderLayerSpec,
+ state: RenderToLayerStateType.Type,
+ layer: MetalSubjectLayer,
+ inputs: repeat Placeholder,
+ commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement, repeat each Resolved) -> Void
+ ) {
+ let stateTypeId = ObjectIdentifier(state)
+ let resolvedState: RenderToLayerStateType
+ if let current = self.impl.renderStates[stateTypeId] as? RenderToLayerStateType {
+ resolvedState = current
+ } else {
+ guard let value = RenderToLayerStateType(device: self.device, library: self.impl.library) else {
+ assertionFailure("Could not initialize render state \(state)")
+ return
+ }
+ resolvedState = value
+ self.impl.renderStates[stateTypeId] = resolvedState
+ }
+
+ let operation = RenderToLayerOperation(
+ spec: spec,
+ state: resolvedState,
+ layer: layer,
+ commands: { encoder, placement in
+ let resolvedInputs: (repeat each Resolved)
+ do {
+ resolvedInputs = (repeat try resolvePlaceholder(each inputs))
+ } catch {
+ print("Could not resolve renderToLayer inputs")
+ return
+ }
+ commands(encoder, placement, repeat each resolvedInputs)
+ }
+ )
+ if self.renderToLayerOperationsGroupedByState[stateTypeId] == nil {
+ self.renderToLayerOperationsGroupedByState[stateTypeId] = [operation]
+ } else {
+ self.renderToLayerOperationsGroupedByState[stateTypeId]?.append(operation)
+ }
+ }
+
+ public func renderToLayer(
+ spec: RenderLayerSpec,
+ state: RenderToLayerStateType.Type,
+ layer: MetalSubjectLayer,
+ commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void
+ ) {
+ self.renderToLayer(spec: spec, state: state, layer: layer, inputs: noInputPlaceholder, commands: { encoder, placement, _ in
+ commands(encoder, placement)
+ })
+ }
+
+ public func compute(
+ state: ComputeStateType.Type,
+ inputs: repeat Placeholder,
+ commands: @escaping (MTLCommandBuffer, ComputeStateType, repeat each Resolved) -> Output
+ ) -> Placeholder