This repository has been archived by the owner on Aug 25, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(QualityIssues) Add ConferenceView and ConferenceParticipantView - ne…
…w layout for in-call video stream appearance
- Loading branch information
1 parent
7241e01
commit 09d6b7f
Showing
6 changed files
with
337 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"info" : { | ||
"version" : 1, | ||
"author" : "xcode" | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
QualityIssues/QIAssets.xcassets/videoOff.imageset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. | ||
*/ | ||
|
||
import UIKit | ||
|
||
final class ConferenceParticipantView: UIView { | ||
|
||
@IBOutlet private var labelContainer: UIView! | ||
@IBOutlet private var nameLabel: UILabel! | ||
@IBOutlet private var backgroundImage: UIImageView! | ||
@IBOutlet private(set) var streamView: UIView! | ||
|
||
private let cornerRadius: CGFloat = 4 | ||
|
||
var isVideoEnabled: Bool = false { | ||
didSet { streamView.isHidden = !isVideoEnabled } | ||
} | ||
|
||
var isNameEnabled: Bool = false { | ||
didSet { | ||
if let name = name, !name.isEmpty { | ||
nameLabel.isHidden = !isNameEnabled | ||
labelContainer.isHidden = !isNameEnabled | ||
} | ||
} | ||
} | ||
|
||
var name: String? { | ||
get { nameLabel.text } | ||
set { | ||
if let newValue = newValue, !newValue.isEmpty, isNameEnabled { | ||
labelContainer.isHidden = false | ||
} else { | ||
labelContainer.isHidden = true | ||
} | ||
nameLabel.text = newValue | ||
} | ||
} | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
sharedInit() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
super.init(coder: coder) | ||
sharedInit() | ||
} | ||
|
||
private func sharedInit() { | ||
setupFromNib() | ||
isUserInteractionEnabled = true | ||
contentMode = .scaleAspectFit | ||
streamView.clipsToBounds = true | ||
backgroundImage.layer.cornerRadius = cornerRadius | ||
labelContainer.layer.cornerRadius = cornerRadius | ||
streamView.layer.cornerRadius = cornerRadius | ||
} | ||
|
||
private func setupFromNib() { | ||
if let view = UINib( | ||
nibName: String(describing: Self.self), | ||
bundle: Bundle(for: Self.self) | ||
).instantiate( | ||
withOwner: self, | ||
options: nil | ||
).first as? UIView { | ||
addSubview(view) | ||
view.frame = bounds | ||
} else { | ||
fatalError("Error loading \(self) from nib") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> | ||
<device id="retina6_1" orientation="portrait" appearance="light"/> | ||
<dependencies> | ||
<deployment identifier="iOS"/> | ||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/> | ||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||
</dependencies> | ||
<objects> | ||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ConferenceParticipantView" customModule="Quality_Issues" customModuleProvider="target"> | ||
<connections> | ||
<outlet property="backgroundImage" destination="ehu-hQ-HRC" id="5Bz-8I-u6y"/> | ||
<outlet property="labelContainer" destination="btL-ne-mNm" id="Qma-FY-Ol3"/> | ||
<outlet property="nameLabel" destination="tMh-g5-4jg" id="S5Y-BZ-GcP"/> | ||
<outlet property="streamView" destination="lUs-RH-WTQ" id="NNT-8D-fVv"/> | ||
</connections> | ||
</placeholder> | ||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> | ||
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="iN0-l3-epB"> | ||
<rect key="frame" x="0.0" y="0.0" width="414" height="235"/> | ||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||
<subviews> | ||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="videoOff" translatesAutoresizingMaskIntoConstraints="NO" id="ehu-hQ-HRC"> | ||
<rect key="frame" x="2" y="2" width="410" height="231"/> | ||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||
<color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||
</imageView> | ||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lUs-RH-WTQ"> | ||
<rect key="frame" x="2" y="2" width="410" height="231"/> | ||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||
</view> | ||
<view hidden="YES" alpha="0.59999999999999998" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="btL-ne-mNm"> | ||
<rect key="frame" x="4" y="4" width="8" height="22"/> | ||
<subviews> | ||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="200" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tMh-g5-4jg"> | ||
<rect key="frame" x="4" y="4" width="0.0" height="14"/> | ||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/> | ||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||
<nil key="highlightedColor"/> | ||
</label> | ||
</subviews> | ||
<color key="backgroundColor" red="0.13333333333333333" green="0.062745098039215685" blue="0.25098039215686274" alpha="1" colorSpace="calibratedRGB"/> | ||
<constraints> | ||
<constraint firstAttribute="height" constant="22" id="6wj-AJ-cDN"/> | ||
<constraint firstAttribute="trailing" secondItem="tMh-g5-4jg" secondAttribute="trailing" constant="4" id="VBa-uK-am4"/> | ||
<constraint firstAttribute="bottom" secondItem="tMh-g5-4jg" secondAttribute="bottom" constant="4" id="b0O-Zt-gsV"/> | ||
<constraint firstItem="tMh-g5-4jg" firstAttribute="top" secondItem="btL-ne-mNm" secondAttribute="top" constant="4" id="kDk-WD-cfD"/> | ||
<constraint firstItem="tMh-g5-4jg" firstAttribute="leading" secondItem="btL-ne-mNm" secondAttribute="leading" constant="4" id="v40-ah-4ul"/> | ||
</constraints> | ||
</view> | ||
</subviews> | ||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||
<constraints> | ||
<constraint firstAttribute="trailing" secondItem="ehu-hQ-HRC" secondAttribute="trailing" constant="2" id="2ZA-ba-DQg"/> | ||
<constraint firstItem="ehu-hQ-HRC" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="2" id="873-6z-ShQ"/> | ||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="btL-ne-mNm" secondAttribute="bottom" constant="4" id="IVu-yp-rM2"/> | ||
<constraint firstItem="lUs-RH-WTQ" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="2" id="Qcg-hu-MoQ"/> | ||
<constraint firstItem="ehu-hQ-HRC" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="2" id="Sk0-85-XOR"/> | ||
<constraint firstAttribute="bottom" secondItem="ehu-hQ-HRC" secondAttribute="bottom" constant="2" id="Zds-e7-M5O"/> | ||
<constraint firstAttribute="bottom" secondItem="lUs-RH-WTQ" secondAttribute="bottom" constant="2" id="cZh-4Y-k7f"/> | ||
<constraint firstItem="btL-ne-mNm" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="4" id="cgJ-8G-qgX"/> | ||
<constraint firstAttribute="trailing" secondItem="lUs-RH-WTQ" secondAttribute="trailing" constant="2" id="leS-Vh-xCr"/> | ||
<constraint firstItem="lUs-RH-WTQ" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="2" id="luq-Ev-sV4"/> | ||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="btL-ne-mNm" secondAttribute="trailing" constant="4" id="vaa-II-Ve1"/> | ||
<constraint firstItem="btL-ne-mNm" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="4" id="zv4-19-LCA"/> | ||
</constraints> | ||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/> | ||
<point key="canvasLocation" x="137.68115942028987" y="-68.638392857142847"/> | ||
</view> | ||
</objects> | ||
<resources> | ||
<image name="videoOff" width="32" height="32"/> | ||
</resources> | ||
</document> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} | ||
} | ||
} | ||
} |