Video chat improvements

This commit is contained in:
Isaac 2024-10-01 22:10:11 +08:00
parent 776caeb8ae
commit 0e1998df1e
3 changed files with 116 additions and 28 deletions

View File

@ -11,6 +11,7 @@ import SwiftSignalKit
import MetalEngine
import CallScreen
import AvatarNode
import ContextUI
final class VideoChatParticipantThumbnailComponent: Component {
let call: PresentationGroupCall
@ -21,6 +22,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let isSpeaking: Bool
let interfaceOrientation: UIInterfaceOrientation
let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
call: PresentationGroupCall,
@ -30,7 +32,8 @@ final class VideoChatParticipantThumbnailComponent: Component {
isSelected: Bool,
isSpeaking: Bool,
interfaceOrientation: UIInterfaceOrientation,
action: (() -> Void)?
action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
) {
self.call = call
self.theme = theme
@ -40,6 +43,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isSpeaking = isSpeaking
self.interfaceOrientation = interfaceOrientation
self.action = action
self.contextAction = contextAction
}
static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool {
@ -64,6 +68,12 @@ final class VideoChatParticipantThumbnailComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
@ -79,7 +89,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
}
}
final class View: HighlightTrackingButton {
final class View: ContextControllerSourceView {
private static let selectedBorderImage: UIImage? = {
return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate)
}()
@ -88,6 +98,10 @@ final class VideoChatParticipantThumbnailComponent: Component {
private weak var componentState: EmptyComponentState?
private var isUpdating: Bool = false
private let extractedContainerView: ContextExtractedContentContainingView
private let backgroundLayer: SimpleLayer
private var avatarNode: AvatarNode?
private let title = ComponentView<Empty>()
private let muteStatus = ComponentView<Empty>()
@ -101,13 +115,30 @@ final class VideoChatParticipantThumbnailComponent: Component {
private var videoSpec: VideoSpec?
override init(frame: CGRect) {
self.extractedContainerView = ContextExtractedContentContainingView()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor(rgb: 0x1C1C1E).cgColor
super.init(frame: frame)
//TODO:release optimize
self.clipsToBounds = true
self.layer.cornerRadius = 10.0
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.extractedContainerView.contentView.layer.addSublayer(self.backgroundLayer)
self.extractedContainerView.contentView.clipsToBounds = true
self.extractedContainerView.contentView.layer.cornerRadius = 10.0
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
@ -118,11 +149,13 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.videoDisposable?.dispose()
}
@objc private func pressed() {
guard let component = self.component, let action = component.action else {
return
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let component = self.component, let action = component.action else {
return
}
action()
}
action()
}
func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -131,10 +164,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isUpdating = false
}
if self.component == nil {
self.backgroundColor = UIColor(rgb: 0x1C1C1E)
}
let previousComponent = self.component
let wasSpeaking = previousComponent?.isSpeaking ?? false
@ -156,6 +185,14 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.component = component
self.componentState = state
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
@ -163,7 +200,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
avatarNode.isUserInteractionEnabled = false
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
self.extractedContainerView.contentView.addSubview(avatarNode.view)
}
let avatarSize = CGSize(width: 50.0, height: 50.0)
@ -188,7 +225,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize)
if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View {
if muteStatusView.superview == nil {
self.addSubview(muteStatusView)
self.extractedContainerView.contentView.addSubview(muteStatusView)
}
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
@ -208,7 +245,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
self.extractedContainerView.contentView.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
@ -222,7 +259,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
videoBackgroundLayer = SimpleLayer()
videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor
self.videoBackgroundLayer = videoBackgroundLayer
self.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer)
self.extractedContainerView.contentView.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer)
videoBackgroundLayer.isHidden = true
}
@ -232,8 +269,8 @@ final class VideoChatParticipantThumbnailComponent: Component {
} else {
videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer
self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
videoLayer.blurredLayer.opacity = 0.25
@ -346,7 +383,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
selectedBorderView = UIImageView()
self.selectedBorderView = selectedBorderView
selectedBorderView.alpha = 0.0
self.addSubview(selectedBorderView)
self.extractedContainerView.contentView.addSubview(selectedBorderView)
selectedBorderView.image = View.selectedBorderImage
selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize)
@ -438,6 +475,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
let speakingParticipants: Set<EnginePeer.Id>
let interfaceOrientation: UIInterfaceOrientation
let updateSelectedParticipant: (Participant.Key) -> Void
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
call: PresentationGroupCall,
@ -446,7 +484,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
selectedParticipant: Participant.Key?,
speakingParticipants: Set<EnginePeer.Id>,
interfaceOrientation: UIInterfaceOrientation,
updateSelectedParticipant: @escaping (Participant.Key) -> Void
updateSelectedParticipant: @escaping (Participant.Key) -> Void,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
) {
self.call = call
self.theme = theme
@ -455,6 +494,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
self.speakingParticipants = speakingParticipants
self.interfaceOrientation = interfaceOrientation
self.updateSelectedParticipant = updateSelectedParticipant
self.contextAction = contextAction
}
static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool {
@ -476,6 +516,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
@ -617,7 +660,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
return
}
component.updateSelectedParticipant(participantKey)
}
},
contextAction: component.contextAction
)),
environment: {},
containerSize: itemFrame.size

View File

@ -205,7 +205,7 @@ final class VideoChatParticipantVideoComponent: Component {
super.init(frame: frame)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view)
self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView)
@ -276,6 +276,8 @@ final class VideoChatParticipantVideoComponent: Component {
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))

View File

@ -648,6 +648,8 @@ final class VideoChatParticipantsComponent: Component {
private var isPinchToZoomActive: Bool = false
private var stopRequestingNonCentralVideo: Bool = false
private var stopRequestingNonCentralVideoTimer: Foundation.Timer?
private var currentLoadMoreToken: String?
private var mainScrollViewEventCycleState: EventCycleState?
@ -722,6 +724,10 @@ final class VideoChatParticipantsComponent: Component {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stopRequestingNonCentralVideoTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else {
return nil
@ -1381,6 +1387,12 @@ final class VideoChatParticipantsComponent: Component {
return
}
component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil)
},
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
}
)),
environment: {},
@ -1599,9 +1611,33 @@ final class VideoChatParticipantsComponent: Component {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
self.state = state
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden {
if self.stopRequestingNonCentralVideoTimer == nil || previousComponent?.expandedVideoState != expandedVideoState {
self.stopRequestingNonCentralVideoTimer?.invalidate()
self.stopRequestingNonCentralVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.stopRequestingNonCentralVideo = true
self.stopRequestingNonCentralVideoTimer = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
})
}
} else {
self.stopRequestingNonCentralVideo = false
if let stopRequestingNonCentralVideoTimer = self.stopRequestingNonCentralVideoTimer {
self.stopRequestingNonCentralVideoTimer = nil
stopRequestingNonCentralVideoTimer.invalidate()
}
}
let measureListItemSize = self.measureListItemView.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
@ -1749,13 +1785,19 @@ final class VideoChatParticipantsComponent: Component {
}
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxVideoQuality != .full {
} else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxPresentationQuality != .full {
} else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
}