[WIP] Video chats

This commit is contained in:
Isaac 2024-08-27 21:18:57 +08:00
parent b1c3aeda82
commit f21f7f66f7
8 changed files with 633 additions and 95 deletions

View File

@ -335,14 +335,14 @@ public enum PresentationGroupCallTone {
case recordingStarted
}
public struct PresentationGroupCallRequestedVideo {
public struct PresentationGroupCallRequestedVideo: Equatable {
public enum Quality {
case thumbnail
case medium
case full
}
public struct SsrcGroup {
public struct SsrcGroup: Equatable {
public var semantics: String
public var ssrcs: [UInt32]
}
@ -441,6 +441,7 @@ public protocol PresentationGroupCall: AnyObject {
func updateDefaultParticipantsAreMuted(isMuted: Bool)
func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool)
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
func setSuspendVideoChannelRequests(_ value: Bool)
func setCurrentAudioOutput(_ output: AudioSessionOutput)
func playTone(_ tone: PresentationGroupCallTone)

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import Display
public final class RoundedRectangle: Component {
public enum GradientDirection: Equatable {
@ -117,3 +118,136 @@ public final class RoundedRectangle: Component {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
public final class FilledRoundedRectangleComponent: Component {
public let color: UIColor
public let cornerRadius: CGFloat
public let smoothCorners: Bool
public init(
color: UIColor,
cornerRadius: CGFloat,
smoothCorners: Bool
) {
self.color = color
self.cornerRadius = cornerRadius
self.smoothCorners = smoothCorners
}
public static func ==(lhs: FilledRoundedRectangleComponent, rhs: FilledRoundedRectangleComponent) -> Bool {
if lhs === rhs {
return true
}
if lhs.color != rhs.color {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.smoothCorners != rhs.smoothCorners {
return false
}
return true
}
public final class View: UIImageView {
private var component: FilledRoundedRectangleComponent?
private var currentCornerRadius: CGFloat?
private var cornerImage: UIImage?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func applyStaticCornerRadius() {
guard let component = self.component else {
return
}
guard let cornerRadius = self.currentCornerRadius else {
return
}
if cornerRadius == 0.0 {
if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 {
} else {
self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
}
} else {
if component.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
context.setFillColor(UIColor.white.cgColor)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
}
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
}
}
}
self.image = self.cornerImage
self.clipsToBounds = false
self.backgroundColor = nil
self.layer.cornerRadius = 0.0
}
func update(component: FilledRoundedRectangleComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
transition.setTintColor(view: self, color: component.color)
if self.currentCornerRadius != component.cornerRadius {
let previousCornerRadius = self.currentCornerRadius
self.currentCornerRadius = component.cornerRadius
if transition.animation.isImmediate {
self.applyStaticCornerRadius()
} else {
self.image = nil
self.clipsToBounds = true
self.backgroundColor = component.color
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = previousCornerRadius
}
if #available(iOS 13.0, *) {
if component.smoothCorners {
self.layer.cornerCurve = .continuous
} else {
self.layer.cornerCurve = .circular
}
}
transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.applyStaticCornerRadius()
})
}
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -664,6 +664,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var ssrcMapping: [UInt32: SsrcMapping] = [:]
private var requestedVideoChannels: [OngoingGroupCallContext.VideoChannel] = []
private var suspendVideoChannelRequests: Bool = false
private var pendingVideoSubscribers = Bag<(String, MetaDisposable, (OngoingGroupCallContext.VideoFrameData) -> Void)>()
private var summaryInfoState = Promise<SummaryInfoState?>(nil)
@ -1699,7 +1700,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.genericCallContext = genericCallContext
self.stateVersionValue += 1
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels)
self.connectPendingVideoSubscribers()
}
@ -3090,11 +3091,21 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
maxQuality: mappedMaxQuality
)
}
if let genericCallContext = self.genericCallContext {
if let genericCallContext = self.genericCallContext, !self.suspendVideoChannelRequests {
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
}
}
public func setSuspendVideoChannelRequests(_ value: Bool) {
if self.suspendVideoChannelRequests != value {
self.suspendVideoChannelRequests = value
if let genericCallContext = self.genericCallContext {
genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels)
}
}
}
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
guard self.currentSelectedAudioOutputValue != output else {
return

View File

@ -34,13 +34,16 @@ final class VideoChatActionButtonComponent: Component {
let content: Content
let microphoneState: MicrophoneState
let isCollapsed: Bool
init(
content: Content,
microphoneState: MicrophoneState
microphoneState: MicrophoneState,
isCollapsed: Bool
) {
self.content = content
self.microphoneState = microphoneState
self.isCollapsed = isCollapsed
}
static func ==(lhs: VideoChatActionButtonComponent, rhs: VideoChatActionButtonComponent) -> Bool {
@ -50,6 +53,9 @@ final class VideoChatActionButtonComponent: Component {
if lhs.microphoneState != rhs.microphoneState {
return false
}
if lhs.isCollapsed != rhs.isCollapsed {
return false
}
return true
}
@ -80,6 +86,8 @@ final class VideoChatActionButtonComponent: Component {
let previousComponent = self.component
self.component = component
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let titleText: String
let backgroundColor: UIColor
let iconDiameter: CGFloat
@ -138,9 +146,10 @@ final class VideoChatActionButtonComponent: Component {
let _ = self.background.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
component: AnyComponent(FilledRoundedRectangleComponent(
color: backgroundColor,
cornerRadius: nil
cornerRadius: size.width * 0.5,
smoothCorners: false
)),
environment: {},
containerSize: size
@ -159,6 +168,7 @@ final class VideoChatActionButtonComponent: Component {
}
transition.setPosition(view: titleView, position: titleFrame.center)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
}
let iconSize = self.icon.update(

View File

@ -14,17 +14,23 @@ final class VideoChatMicButtonComponent: Component {
}
let content: Content
let isCollapsed: Bool
init(
content: Content
content: Content,
isCollapsed: Bool
) {
self.content = content
self.isCollapsed = isCollapsed
}
static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.isCollapsed != rhs.isCollapsed {
return false
}
return true
}
@ -52,6 +58,8 @@ final class VideoChatMicButtonComponent: Component {
self.component = component
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let titleText: String
let backgroundColor: UIColor
switch component.content {
@ -79,9 +87,10 @@ final class VideoChatMicButtonComponent: Component {
let _ = self.background.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
component: AnyComponent(FilledRoundedRectangleComponent(
color: backgroundColor,
cornerRadius: nil
cornerRadius: size.width * 0.5,
smoothCorners: false
)),
environment: {},
containerSize: size
@ -100,6 +109,7 @@ final class VideoChatMicButtonComponent: Component {
}
transition.setPosition(view: titleView, position: titleFrame.center)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
}
let iconSize = self.icon.update(
@ -118,7 +128,9 @@ final class VideoChatMicButtonComponent: Component {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
transition.setPosition(view: iconView, position: iconFrame.center)
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setScale(view: iconView, scale: component.isCollapsed ? ((iconSize.width - 24.0) / iconSize.width) : 1.0)
}
return size

View File

@ -12,18 +12,32 @@ import AccountContext
import SwiftSignalKit
final class VideoChatParticipantVideoComponent: Component {
struct ExpandedState: Equatable {
var isPinned: Bool
init(isPinned: Bool) {
self.isPinned = isPinned
}
}
let call: PresentationGroupCall
let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool
let expandedState: ExpandedState?
let action: (() -> Void)?
init(
call: PresentationGroupCall,
participant: GroupCallParticipantsContext.Participant,
isPresentation: Bool
isPresentation: Bool,
expandedState: ExpandedState?,
action: (() -> Void)?
) {
self.call = call
self.participant = participant
self.isPresentation = isPresentation
self.expandedState = expandedState
self.action = action
}
static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool {
@ -33,6 +47,12 @@ final class VideoChatParticipantVideoComponent: Component {
if lhs.isPresentation != rhs.isPresentation {
return false
}
if lhs.expandedState != rhs.expandedState {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
@ -46,9 +66,9 @@ final class VideoChatParticipantVideoComponent: Component {
}
}
final class View: UIView {
final class View: HighlightTrackingButton {
private var component: VideoChatParticipantVideoComponent?
private weak var state: EmptyComponentState?
private weak var componentState: EmptyComponentState?
private var isUpdating: Bool = false
private let title = ComponentView<Empty>()
@ -62,8 +82,11 @@ final class VideoChatParticipantVideoComponent: Component {
override init(frame: CGRect) {
super.init(frame: frame)
//TODO:release optimize
self.clipsToBounds = true
self.layer.cornerRadius = 10.0
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
@ -74,6 +97,13 @@ final class VideoChatParticipantVideoComponent: Component {
self.videoDisposable?.dispose()
}
@objc private func pressed() {
guard let component = self.component, let action = component.action else {
return
}
action()
}
func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -81,7 +111,7 @@ final class VideoChatParticipantVideoComponent: Component {
}
self.component = component
self.state = state
self.componentState = state
let nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
@ -146,14 +176,14 @@ final class VideoChatParticipantVideoComponent: Component {
if self.videoSpec != videoSpec {
self.videoSpec = videoSpec
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
self.componentState?.updated(transition: .immediate, isLocal: true)
}
}
} else {
if self.videoSpec != nil {
self.videoSpec = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
self.componentState?.updated(transition: .immediate, isLocal: true)
}
}
}

View File

@ -12,36 +12,93 @@ import TelegramPresentationData
import PeerListItemComponent
final class VideoChatParticipantsComponent: Component {
struct VideoParticipantKey: Hashable {
var id: EnginePeer.Id
var isPresentation: Bool
init(id: EnginePeer.Id, isPresentation: Bool) {
self.id = id
self.isPresentation = isPresentation
}
}
final class ExpandedVideoState: Equatable {
let mainParticipant: VideoParticipantKey
let isMainParticipantPinned: Bool
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool) {
self.mainParticipant = mainParticipant
self.isMainParticipantPinned = isMainParticipantPinned
}
static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool {
if lhs === rhs {
return true
}
if lhs.mainParticipant != rhs.mainParticipant {
return false
}
if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned {
return false
}
return true
}
}
let call: PresentationGroupCall
let members: PresentationGroupCallMembers?
let expandedVideoState: ExpandedVideoState?
let theme: PresentationTheme
let strings: PresentationStrings
let collapsedContainerInsets: UIEdgeInsets
let expandedContainerInsets: UIEdgeInsets
let sideInset: CGFloat
let updateMainParticipant: (VideoParticipantKey?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void
init(
call: PresentationGroupCall,
members: PresentationGroupCallMembers?,
expandedVideoState: ExpandedVideoState?,
theme: PresentationTheme,
strings: PresentationStrings,
sideInset: CGFloat
collapsedContainerInsets: UIEdgeInsets,
expandedContainerInsets: UIEdgeInsets,
sideInset: CGFloat,
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void
) {
self.call = call
self.members = members
self.expandedVideoState = expandedVideoState
self.theme = theme
self.strings = strings
self.collapsedContainerInsets = collapsedContainerInsets
self.expandedContainerInsets = expandedContainerInsets
self.sideInset = sideInset
self.updateMainParticipant = updateMainParticipant
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
}
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
if lhs.members != rhs.members {
return false
}
if lhs.expandedVideoState != rhs.expandedVideoState {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets {
return false
}
if lhs.expandedContainerInsets != rhs.expandedContainerInsets {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
@ -116,6 +173,20 @@ final class VideoChatParticipantsComponent: Component {
}
}
struct ExpandedGrid {
let containerSize: CGSize
let containerInsets: UIEdgeInsets
init(containerSize: CGSize, containerInsets: UIEdgeInsets) {
self.containerSize = containerSize
self.containerInsets = containerInsets
}
func itemContainerFrame() -> CGRect {
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top - containerInsets.bottom))
}
}
struct List {
let containerSize: CGSize
let sideInset: CGFloat
@ -168,19 +239,24 @@ final class VideoChatParticipantsComponent: Component {
let containerSize: CGSize
let sideInset: CGFloat
let grid: Grid
let expandedGrid: ExpandedGrid
let list: List
let spacing: CGFloat
let gridOffsetY: CGFloat
let listOffsetY: CGFloat
init(containerSize: CGSize, sideInset: CGFloat, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
init(containerSize: CGSize, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
self.containerSize = containerSize
self.sideInset = sideInset
self.grid = Grid(containerSize: containerSize, sideInset: sideInset, itemCount: gridItemCount)
self.list = List(containerSize: containerSize, sideInset: sideInset, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.grid = Grid(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
self.expandedGrid = ExpandedGrid(containerSize: containerSize, containerInsets: expandedContainerInsets)
self.list = List(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.spacing = 4.0
var listOffsetY: CGFloat = 0.0
self.gridOffsetY = collapsedContainerInsets.top
var listOffsetY: CGFloat = self.gridOffsetY
if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing
@ -199,42 +275,40 @@ final class VideoChatParticipantsComponent: Component {
}
func visibleGridItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
return self.grid.visibleItemRange(for: rect)
return self.grid.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.gridOffsetY))
}
func gridItemFrame(at index: Int) -> CGRect {
return self.grid.frame(at: index)
}
func gridItemContainerFrame() -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight()))
}
func visibleListItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
return self.list.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.listOffsetY))
}
func listItemFrame(at index: Int) -> CGRect {
return self.list.frame(at: index).offsetBy(dx: 0.0, dy: self.listOffsetY)
return self.list.frame(at: index)
}
func listItemContainerFrame() -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight()))
}
func listTrailingItemFrame() -> CGRect {
return self.list.trailingItemFrame().offsetBy(dx: 0.0, dy: self.listOffsetY)
return self.list.trailingItemFrame()
}
}
private final class VideoParticipant: Equatable {
struct Key: Hashable {
var id: EnginePeer.Id
var isPresentation: Bool
init(id: EnginePeer.Id, isPresentation: Bool) {
self.id = id
self.isPresentation = isPresentation
}
}
let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool
var key: Key {
return Key(id: self.participant.peer.id, isPresentation: self.isPresentation)
var key: VideoParticipantKey {
return VideoParticipantKey(id: self.participant.peer.id, isPresentation: self.isPresentation)
}
init(participant: GroupCallParticipantsContext.Participant, isPresentation: Bool) {
@ -252,8 +326,19 @@ final class VideoChatParticipantsComponent: Component {
return true
}
}
private final class GridItem {
let key: VideoParticipantKey
let view = ComponentView<Empty>()
var isCollapsing: Bool = false
init(key: VideoParticipantKey) {
self.key = key
}
}
final class View: UIView, UIScrollViewDelegate {
private let scollViewClippingContainer: UIView
private let scrollView: ScrollView
private var component: VideoChatParticipantsComponent?
@ -267,16 +352,35 @@ final class VideoChatParticipantsComponent: Component {
private let measureListItemView = ComponentView<Empty>()
private let inviteListItemView = ComponentView<Empty>()
private var gridItemViews: [VideoParticipant.Key: ComponentView<Empty>] = [:]
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
private let gridItemViewContainer: UIView
private let expandedGridItemContainer: UIView
private var expandedGridItemView: GridItem?
private var listItemViews: [EnginePeer.Id: ComponentView<Empty>] = [:]
private let listItemViewContainer: UIView
private let listItemsBackround = ComponentView<Empty>()
private var itemLayout: ItemLayout?
private var appliedGridIsEmpty: Bool = true
override init(frame: CGRect) {
self.scollViewClippingContainer = UIView()
self.scollViewClippingContainer.clipsToBounds = true
self.scrollView = ScrollView()
self.gridItemViewContainer = UIView()
self.gridItemViewContainer.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.listItemViewContainer = UIView()
self.listItemViewContainer.clipsToBounds = true
self.expandedGridItemContainer = UIView()
self.expandedGridItemContainer.clipsToBounds = true
super.init(frame: frame)
self.scrollView.delaysContentTouches = false
@ -294,13 +398,38 @@ final class VideoChatParticipantsComponent: Component {
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
self.scollViewClippingContainer.addSubview(self.scrollView)
self.addSubview(self.scollViewClippingContainer)
self.scrollView.addSubview(self.listItemViewContainer)
self.scrollView.addSubview(self.gridItemViewContainer)
self.addSubview(self.expandedGridItemContainer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else {
return nil
}
if component.expandedVideoState != nil {
if let result = self.expandedGridItemContainer.hitTest(self.convert(point, to: self.expandedGridItemContainer), with: event) {
return result
} else {
return self
}
} else {
if let result = self.scollViewClippingContainer.hitTest(self.convert(point, to: self.scollViewClippingContainer), with: event) {
return result
} else {
return nil
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
@ -312,51 +441,158 @@ final class VideoChatParticipantsComponent: Component {
return
}
var validGridItemIds: [VideoParticipant.Key] = []
let gridWasEmpty = self.appliedGridIsEmpty
let gridIsEmpty = self.gridParticipants.isEmpty
self.appliedGridIsEmpty = gridIsEmpty
var expandedGridItemContainerFrame: CGRect
if component.expandedVideoState != nil {
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
} else {
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
}
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - component.collapsedContainerInsets.bottom {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - component.collapsedContainerInsets.bottom)
}
if expandedGridItemContainerFrame.size.height < 0.0 {
expandedGridItemContainerFrame.size.height = 0.0
}
}
let commonGridItemTransition: ComponentTransition = (gridIsEmpty == gridWasEmpty) ? transition : .immediate
var validGridItemIds: [VideoParticipantKey] = []
var validGridItemIndices: [Int] = []
let visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
for i in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
let videoParticipant = self.gridParticipants[i]
validGridItemIds.append(videoParticipant.key)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.gridItemViews[videoParticipant.key] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ComponentView()
self.gridItemViews[videoParticipant.key] = itemView
}
let itemFrame = itemLayout.gridItemFrame(at: i)
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(VideoChatParticipantVideoComponent(
call: component.call,
participant: videoParticipant.participant,
isPresentation: videoParticipant.isPresentation
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
let videoParticipant = self.gridParticipants[index]
let videoParticipantKey = videoParticipant.key
validGridItemIds.append(videoParticipantKey)
validGridItemIndices.append(index)
}
}
if let expandedVideoState = component.expandedVideoState {
if !validGridItemIds.contains(expandedVideoState.mainParticipant), let index = self.gridParticipants.firstIndex(where: { $0.key == expandedVideoState.mainParticipant }) {
validGridItemIds.append(expandedVideoState.mainParticipant)
validGridItemIndices.append(index)
}
}
for index in validGridItemIndices {
let videoParticipant = self.gridParticipants[index]
let videoParticipantKey = videoParticipant.key
validGridItemIds.append(videoParticipantKey)
var itemTransition = commonGridItemTransition
let itemView: GridItem
if let current = self.gridItemViews[videoParticipantKey] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = GridItem(key: videoParticipant.key)
self.gridItemViews[videoParticipantKey] = itemView
}
var expandedItemState: VideoChatParticipantVideoComponent.ExpandedState?
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
expandedItemState = VideoChatParticipantVideoComponent.ExpandedState(isPinned: expandedVideoState.isMainParticipantPinned)
}
let itemFrame: CGRect
if expandedItemState != nil {
itemFrame = CGRect(origin: CGPoint(), size: itemLayout.expandedGrid.itemContainerFrame().size)
} else {
itemFrame = itemLayout.gridItemFrame(at: index)
}
let _ = itemView.view.update(
transition: itemTransition,
component: AnyComponent(VideoChatParticipantVideoComponent(
call: component.call,
participant: videoParticipant.participant,
isPresentation: videoParticipant.isPresentation,
expandedState: expandedItemState,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if component.expandedVideoState?.mainParticipant == videoParticipantKey {
component.updateMainParticipant(nil)
} else {
component.updateMainParticipant(videoParticipantKey)
}
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil {
if expandedItemState != nil {
self.expandedGridItemContainer.addSubview(itemComponentView)
} else {
self.gridItemViewContainer.addSubview(itemComponentView)
}
itemComponentView.frame = itemFrame
if !commonGridItemTransition.animation.isImmediate {
commonGridItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
}
if !transition.animation.isImmediate {
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
} else if expandedItemState != nil && itemComponentView.superview != self.expandedGridItemContainer {
let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer)
itemComponentView.center = fromFrame.center
self.expandedGridItemContainer.addSubview(itemComponentView)
} else if expandedItemState == nil && itemComponentView.superview != self.gridItemViewContainer {
if !itemView.isCollapsing {
itemView.isCollapsing = true
let targetLocalItemFrame = itemLayout.gridItemFrame(at: index)
var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self)
targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY
targetItemFrame.origin.x -= expandedGridItemContainerFrame.minX
commonGridItemTransition.setPosition(view: itemComponentView, position: targetItemFrame.center)
commonGridItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: targetItemFrame.size), completion: { [weak self, weak itemView, weak itemComponentView] _ in
guard let self, let itemView, let itemComponentView else {
return
}
itemView.isCollapsing = false
self.gridItemViewContainer.addSubview(itemComponentView)
itemComponentView.center = targetLocalItemFrame.center
itemComponentView.bounds = CGRect(origin: CGPoint(), size: targetLocalItemFrame.size)
})
}
}
if !itemView.isCollapsing {
commonGridItemTransition.setPosition(view: itemComponentView, position: itemFrame.center)
commonGridItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
}
}
}
var removedGridItemIds: [VideoParticipant.Key] = []
var removedGridItemIds: [VideoParticipantKey] = []
for (itemId, itemView) in self.gridItemViews {
if !validGridItemIds.contains(itemId) {
removedGridItemIds.append(itemId)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
if let itemComponentView = itemView.view.view {
if !transition.animation.isImmediate {
if commonGridItemTransition.animation.isImmediate == transition.animation.isImmediate {
transition.setScale(view: itemComponentView, scale: 0.001)
}
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
} else {
itemComponentView.removeFromSuperview()
}
}
}
}
@ -418,9 +654,16 @@ final class VideoChatParticipantsComponent: Component {
)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
itemComponentView.clipsToBounds = true
self.listItemViewContainer.addSubview(itemComponentView)
if !transition.animation.isImmediate {
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
itemComponentView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: 0.0))
}
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
transition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
}
@ -431,7 +674,13 @@ final class VideoChatParticipantsComponent: Component {
removedListItemIds.append(itemId)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
if !transition.animation.isImmediate {
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
} else {
itemComponentView.removeFromSuperview()
}
}
}
}
@ -448,11 +697,18 @@ final class VideoChatParticipantsComponent: Component {
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
itemTransition = itemTransition.withAnimation(.none)
self.scrollView.addSubview(itemComponentView)
self.listItemViewContainer.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
transition.setBounds(view: self.gridItemViewContainer, bounds: CGRect(origin: CGPoint(), size: itemLayout.gridItemContainerFrame().size))
transition.setFrame(view: self.listItemViewContainer, frame: itemLayout.listItemContainerFrame())
transition.setFrame(view: self.expandedGridItemContainer, frame: expandedGridItemContainerFrame)
}
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -533,6 +789,8 @@ final class VideoChatParticipantsComponent: Component {
let itemLayout = ItemLayout(
containerSize: availableSize,
sideInset: component.sideInset,
collapsedContainerInsets: component.collapsedContainerInsets,
expandedContainerInsets: component.expandedContainerInsets,
gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count,
listItemHeight: measureListItemSize.height,
@ -549,10 +807,10 @@ final class VideoChatParticipantsComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: itemLayout.list.contentHeight())
)
let listItemsBackroundFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: itemLayout.listOffsetY), size: listItemsBackroundSize)
let listItemsBackroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackroundSize)
if let listItemsBackroundView = self.listItemsBackround.view {
if listItemsBackroundView.superview == nil {
self.scrollView.addSubview(listItemsBackroundView)
self.listItemViewContainer.addSubview(listItemsBackroundView)
}
transition.setFrame(view: listItemsBackroundView, frame: listItemsBackroundFrame)
}
@ -560,18 +818,38 @@ final class VideoChatParticipantsComponent: Component {
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
if let members = component.members {
for participant in members.participants {
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
requestedVideo.append(videoChannel)
var maxVideoQuality: PresentationGroupCallRequestedVideo.Quality = .medium
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant.id == participant.peer.id, !expandedVideoState.mainParticipant.isPresentation {
maxVideoQuality = .full
}
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
requestedVideo.append(videoChannel)
var maxPresentationQuality: PresentationGroupCallRequestedVideo.Quality = .medium
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant.id == participant.peer.id, expandedVideoState.mainParticipant.isPresentation {
maxPresentationQuality = .full
}
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
}
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: component.collapsedContainerInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.collapsedContainerInsets.top - component.collapsedContainerInsets.bottom))
transition.setPosition(view: self.scollViewClippingContainer, position: scrollClippingFrame.center)
transition.setBounds(view: self.scollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
if self.scrollView.bounds.size != availableSize {
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight())
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize

View File

@ -73,6 +73,11 @@ private final class VideoChatScreenComponent: Component {
private var members: PresentationGroupCallMembers?
private var membersDisposable: Disposable?
private let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
private var applicationStateDisposable: Disposable?
private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState?
override init(frame: CGRect) {
self.containerView = UIView()
self.containerView.clipsToBounds = true
@ -96,6 +101,7 @@ private final class VideoChatScreenComponent: Component {
deinit {
self.stateDisposable?.dispose()
self.membersDisposable?.dispose()
self.applicationStateDisposable?.dispose()
}
func animateIn() {
@ -272,7 +278,7 @@ private final class VideoChatScreenComponent: Component {
self.members = members
if !self.isUpdating {
self.state?.updated(transition: .immediate)
self.state?.updated(transition: .spring(duration: 0.4))
}
}
})
@ -290,8 +296,22 @@ private final class VideoChatScreenComponent: Component {
}
}
})
self.applicationStateDisposable = (combineLatest(queue: .mainQueue(),
component.call.accountContext.sharedContext.applicationBindings.applicationIsActive,
self.isPresentedValue.get()
)
|> deliverOnMainQueue).startStrict(next: { [weak self] applicationIsActive, isPresented in
guard let self, let component = self.component else {
return
}
let suspendVideoChannelRequests = !applicationIsActive || !isPresented
component.call.setSuspendVideoChannelRequests(suspendVideoChannelRequests)
})
}
self.isPresentedValue.set(environment.isVisible)
self.component = component
self.environment = environment
self.state = state
@ -419,7 +439,7 @@ private final class VideoChatScreenComponent: Component {
}
let actionButtonDiameter: CGFloat = 56.0
let microphoneButtonDiameter: CGFloat = 116.0
let microphoneButtonDiameter: CGFloat = self.expandedParticipantsVideoState == nil ? 116.0 : actionButtonDiameter
let maxActionMicrophoneButtonSpacing: CGFloat = 38.0
let buttonsSideInset: CGFloat = 42.0
@ -428,23 +448,60 @@ private final class VideoChatScreenComponent: Component {
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
let microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - microphoneButtonDiameter), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
let microphoneButtonFrame: CGRect
if self.expandedParticipantsVideoState == nil {
microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - microphoneButtonDiameter), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
} else {
microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - microphoneButtonDiameter - 12.0), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
}
let participantsClippingY: CGFloat
if self.expandedParticipantsVideoState == nil {
participantsClippingY = microphoneButtonFrame.minY
} else {
participantsClippingY = microphoneButtonFrame.minY - 24.0
}
let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
let participantsSize = self.participants.update(
let participantsSize = availableSize
let participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - participantsClippingY, right: environment.safeInsets.right)
let participantsExpandedInsets = UIEdgeInsets(top: environment.statusBarHeight, left: environment.safeInsets.left, bottom: availableSize.height - participantsClippingY, right: environment.safeInsets.right)
let _ = self.participants.update(
transition: transition,
component: AnyComponent(VideoChatParticipantsComponent(
call: component.call,
members: self.members,
expandedVideoState: self.expandedParticipantsVideoState,
theme: environment.theme,
strings: environment.strings,
sideInset: sideInset
collapsedContainerInsets: participantsCollapsedInsets,
expandedContainerInsets: participantsExpandedInsets,
sideInset: sideInset,
updateMainParticipant: { [weak self] key in
guard let self else {
return
}
if let key {
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key {
return
}
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false)
self.state?.updated(transition: .spring(duration: 0.4))
} else if self.expandedParticipantsVideoState != nil {
self.expandedParticipantsVideoState = nil
self.state?.updated(transition: .spring(duration: 0.4))
}
},
updateIsMainParticipantPinned: { isPinned in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: microphoneButtonFrame.minY - navigationHeight)
containerSize: participantsSize
)
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: participantsSize)
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: participantsSize)
if let participantsView = self.participants.view {
if participantsView.superview == nil {
self.containerView.addSubview(participantsView)
@ -477,7 +534,8 @@ private final class VideoChatScreenComponent: Component {
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatMicButtonComponent(
content: micButtonContent
content: micButtonContent,
isCollapsed: self.expandedParticipantsVideoState != nil
)),
effectAlignment: .center,
action: { [weak self] in
@ -514,7 +572,8 @@ private final class VideoChatScreenComponent: Component {
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
content: .video(isActive: false),
microphoneState: actionButtonMicrophoneState
microphoneState: actionButtonMicrophoneState,
isCollapsed: self.expandedParticipantsVideoState != nil
)),
effectAlignment: .center,
action: { [weak self] in
@ -541,7 +600,8 @@ private final class VideoChatScreenComponent: Component {
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
content: .leave,
microphoneState: actionButtonMicrophoneState
microphoneState: actionButtonMicrophoneState,
isCollapsed: self.expandedParticipantsVideoState != nil
)),
effectAlignment: .center,
action: { [weak self] in
@ -665,7 +725,9 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
self.idleTimerExtensionDisposable = self.call.accountContext.sharedContext.applicationBindings.pushIdleTimerExtension()
}
self.onViewDidAppear?()
DispatchQueue.main.async {
self.onViewDidAppear?()
}
}
override public func viewDidDisappear(_ animated: Bool) {