From 09d6b7fe813430e2a96a6f0f76cf029b5b25b4d8 Mon Sep 17 00:00:00 2001 From: VladimirBrejcha Date: Tue, 1 Jun 2021 16:28:42 +0300 Subject: [PATCH] (QualityIssues) Add ConferenceView and ConferenceParticipantView - new layout for in-call video stream appearance --- QualityIssues/QIAssets.xcassets/Contents.json | 6 +- .../videoOff.imageset/Contents.json | 24 +++ .../videoOff.imageset/videoOff.png | Bin 0 -> 1095 bytes .../UI/ConferenceParticipantView.swift | 75 ++++++++ .../UI/ConferenceParticipantView.xib | 74 ++++++++ QualityIssues/UI/ConferenceView.swift | 161 ++++++++++++++++++ 6 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 QualityIssues/QIAssets.xcassets/videoOff.imageset/Contents.json create mode 100644 QualityIssues/QIAssets.xcassets/videoOff.imageset/videoOff.png create mode 100644 QualityIssues/UI/ConferenceParticipantView.swift create mode 100644 QualityIssues/UI/ConferenceParticipantView.xib create mode 100644 QualityIssues/UI/ConferenceView.swift diff --git a/QualityIssues/QIAssets.xcassets/Contents.json b/QualityIssues/QIAssets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/QualityIssues/QIAssets.xcassets/Contents.json +++ b/QualityIssues/QIAssets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/QualityIssues/QIAssets.xcassets/videoOff.imageset/Contents.json b/QualityIssues/QIAssets.xcassets/videoOff.imageset/Contents.json new file mode 100644 index 0000000..cf3edfc --- /dev/null +++ b/QualityIssues/QIAssets.xcassets/videoOff.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "videoOff.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/QualityIssues/QIAssets.xcassets/videoOff.imageset/videoOff.png b/QualityIssues/QIAssets.xcassets/videoOff.imageset/videoOff.png new file mode 100644 index 0000000000000000000000000000000000000000..0a5ee1e86252f66b3263e0c269df9e70ba1caf65 GIT binary patch literal 1095 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9EO-XP4l)OOlRpde#$ zkh>GZx^prwfgF}}M_)$E)e-c?47?|&Px;TbZFutAL8z^lk(juR!pm$I;plPMT zlnH!JS_euTvM-^ZUVqloQu5{gAg$MuRxHcY_wl%2h_2l5)#+TRGo{EzlYF5cw$B|1=3t-Yb(#_6m3CI&NI-o9go z;7P?DcGooyubsoR!SMgoCjtU}f0v%PB^>jgwd}dmeKU>94(EUG)+uVX{PWJ9@1412 z`Xr7feSX#BHfmoQDm{1~vORqvRC2s$zDb%-j$e@Tg_yRiNR2%nHznd##LZ7cDQ=0-5>I<>*FXw0du~!j4vi|$_jvUM8jah9Q9Bf+h)}QB^`Q3UF_oKF2 zFPZge9IHF-m3`RB$2uWKhb2IhrDd;zTYJ-yXC(_~e0iZD&NtH`f7Sh`rtRzP*Tq_{ zyg#Y)*px{}{`^#!`r_=KNeOv8@VrSztl>m5R4uUZU@2 z0fGJJ!X@n`&R>%+ZhX8;O|hqEcFLcGJv#NP?sjB0Ts$jvsPl^cp_(`UzrMKTe?7d| za*zGAwe~wXpEn)jZaVwdQ2fH#g*OGxfEec&p9C{JW_uSa&iI|IH^FZHGAD!Y$72$1 z&6&k{@D9+5m!GqgSc>btufxB`qJbf7kb4pN5D#^oH(fWDJ3NC(nK zp3ndGr$+A?m!jE*CruT$Pram4`vj7jz4SC9=0r2U7BGmD`Y-Z&z2=!;qLuY8XU#vd zb<4@k+1XFimy6WbN>{#*D{5S~;OiZa*cb8gFX|V+TphLEWyS3HlfRW#Do7;Gz1jZk z`y%;UUsH1#yuW`^*>&_|@uG;(Z-ya9cjhsjfA`eyp8PY`gRfp?h&S0UG-x@bGIjZG zfot!cu6}XqWc2a+c5BO{mnu^JWNi>Md$D>>V5P0%>a5lOh5or${gZp}m1}|Pyx7m` zm)x)Kdmq1W!KL#{Pc+=BW)f_>zyiq&NIAfS!8wBgm`OSnOPo$k7HSfZ-lE8{(OHm* x-| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QualityIssues/UI/ConferenceView.swift b/QualityIssues/UI/ConferenceView.swift new file mode 100644 index 0000000..7c14bdb --- /dev/null +++ b/QualityIssues/UI/ConferenceView.swift @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +import UIKit + +@objc class ConferenceView: UIView { + private struct Participant { + let place: Int + var view: ConferenceParticipantView + } + + private struct PendingParticipant { + var view: ConferenceParticipantView + var renderer: ((_ view: UIView?) -> Void)? = nil + } + + private var enlargedParticipantId: String? + private var participants = [String: Participant]() + private var pendingParticipants = [String: PendingParticipant]() + private let maxNumberOfViews = 4 + private var defaultParticipantView: ConferenceParticipantView { + let view = ConferenceParticipantView() + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleParticipant)) + view.addGestureRecognizer(tapGesture) + view.frame = CGRect(x: self.bounds.width / 2, y: self.bounds.height / 2, width: 0, height: 0) + return view + } + + @objc var enlargeParticipantHandler: ((Bool) -> Void)? + + @objc func addParticipant(id: String, name: String? = nil) { + guard participants[id] == nil, pendingParticipants[id] == nil else { + print("addParticipant: unexpected branch") + return + } + let participantView = defaultParticipantView + participantView.name = name + if participants.count < maxNumberOfViews { + addSubview(participantView) + let participant = Participant(place: participants.count, view: participantView) + participants[id] = participant + } else { + let participant = PendingParticipant(view: participantView) + pendingParticipants[id] = participant + } + rearrange() + } + + @objc func removeParticipant(id: String) { + if let participant = participants.removeValue(forKey: id) { + participant.view.removeFromSuperview() + if (id == enlargedParticipantId) { + enlargedParticipantId = nil + } + if participants.count < maxNumberOfViews, + let (participantId, pendingParticipant) = pendingParticipants.first { + pendingParticipants[participantId] = nil + participants[participantId] = Participant( + place: participants.count, + view: pendingParticipant.view + ) + pendingParticipant.renderer?(pendingParticipant.view.streamView) + addSubview(pendingParticipant.view) + } + rearrange() + } else { + pendingParticipants[id] = nil + } + } + + @objc func requestRendering(participantId id: String, render: @escaping (_ view: UIView?) -> Void) { + if let participant = participants[id] { + participant.view.isVideoEnabled = true + render(participant.view.streamView) + } else if pendingParticipants[id] != nil { + pendingParticipants[id]?.renderer = render + } else { + print("requestRendering: unexpected branch") + } + } + + @objc func stopRendering(participantId id: String) { + if let participant = participants[id] { + participant.view.isVideoEnabled = false + } else if let pendingParticipant = pendingParticipants[id] { + pendingParticipant.view.isVideoEnabled = false + pendingParticipants[id]?.renderer = nil + } + } + + @objc func updateParticipantName(id: String, name: String) { + if let participantView = participants[id]?.view { + participantView.name = name + } else if let pendingParticipantView = pendingParticipants[id]?.view { + pendingParticipantView.name = name + } + } + + @objc private func toggleParticipant(_ sender: UIGestureRecognizer!) { + if let participantView = sender.view as? ConferenceParticipantView, + let id = participants.first(where: { _, value in value.view == participantView })?.key { + if id == enlargedParticipantId { + enlargedParticipantId = nil + } else { + enlargedParticipantId = id + } + } + rearrange() + } + + override func layoutSubviews() { + rearrange() + super.layoutSubviews() + } + + private func rearrange() { + DispatchQueue.main.async { () -> Void in + self.enlargeParticipantHandler?(self.enlargedParticipantId != nil) + + let surface = self.bounds.size + + if let rootParticipant = self.enlargedParticipantId { + guard let rootParticipantView = self.participants[rootParticipant]?.view + else { return } + self.participants.values.forEach { $0.view.alpha = 0 } + rootParticipantView.isNameEnabled = true + rootParticipantView.frame = CGRect(x: 0, y: 0, width: surface.width, height: surface.height) + rootParticipantView.alpha = 1 + + } else { + var w, h: CGFloat + + switch self.participants.count { + case 0..<2: + w = 1; h = 1 + case 2: + w = 2; h = 1 + case 3..<5: + w = 2; h = 2 + default: + return + } + + let size = CGSize(width: surface.width / w, height: surface.height / h) + self.participants.values.forEach { participant in + let view = participant.view + view.alpha = 1 + view.isNameEnabled = false + let x = participant.place % Int(w) + let y = participant.place / Int(w) + view.frame = CGRect( + origin: CGPoint(x: CGFloat(x) * size.width, y: CGFloat(y) * size.height), + size: size + ) + view.layoutSubviews() + } + } + } + } +}