AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/Cell/ZoomableVideoRenderView.swift (159 lines of code) (raw):
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
import Foundation
import SwiftUI
struct ZoomableVideoRenderView: UIViewRepresentable {
private enum SmallScreenConstants {
static let maxScale: CGFloat = 2.0
static let minScale: CGFloat = 0.5
static let maxLength: CGFloat = 850
}
private enum GeneralScreenConstants {
static let maxScale: CGFloat = 4.0
static let minScale: CGFloat = 1.0
static let defaultAspectRatio: CGFloat = 1.6 // 16: 10 aspect ratio
static let tapCount: Int = 2
}
private enum InitialZoomScaleConstants {
static let isSmallScreen: Bool = UIScreen.isScreenSmall(SmallScreenConstants.maxLength)
static let minScale: CGFloat = isSmallScreen ? SmallScreenConstants.minScale : GeneralScreenConstants.minScale
static let maxScale: CGFloat = isSmallScreen ? SmallScreenConstants.maxScale : GeneralScreenConstants.maxScale
}
let videoRendererViewInfo: ParticipantRendererViewInfo!
weak var rendererViewManager: RendererViewManager?
init(videoRendererViewInfo: ParticipantRendererViewInfo,
rendererViewManager: RendererViewManager?) {
self.videoRendererViewInfo = videoRendererViewInfo
self.rendererViewManager = rendererViewManager
}
func makeUIView(context: Context) -> UIScrollView {
// Setup scrollview and render view
let scrollView = UIScrollView()
// Setup scrollview and render view
setupScrollView(scrollView, context: context)
// Add double tap action
addDoubleTapGestureRecognizer(for: scrollView, coordinator: context.coordinator)
return scrollView
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
// check when updateUIView is called after makeUIView
if let currentContentView = scrollView.subviews.first,
currentContentView.subviews.first === videoRendererViewInfo.rendererView {
return
}
}
static func dismantleUIView(_ uiView: UIScrollView, coordinator: Coordinator) {
uiView.delegate = nil
for view in uiView.subviews {
view.removeFromSuperview()
}
}
private func addDoubleTapGestureRecognizer(for scrollView: UIScrollView,
coordinator: Coordinator) {
let doubleTapGesture = UITapGestureRecognizer(target: coordinator,
action: #selector(Coordinator.doubleTapped))
doubleTapGesture.numberOfTapsRequired = GeneralScreenConstants.tapCount
doubleTapGesture.delegate = coordinator
rendererViewManager?.didRenderFirstFrame = coordinator.videoStreamRenderer(didRenderFirstFrameWithSize:)
scrollView.addGestureRecognizer(doubleTapGesture)
}
private func setupScrollView(_ scrollView: UIScrollView,
context: Context) {
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = InitialZoomScaleConstants.maxScale
scrollView.minimumZoomScale = InitialZoomScaleConstants.minScale
scrollView.bouncesZoom = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.decelerationRate = .fast
// Creates a content view for scrollview, that contains the rendererView
let contentView = UIView()
scrollView.addSubview(contentView)
scrollView.contentSize = scrollView.bounds.size
// ZoomScale should be set before contentView.frame in order to resize contentView correctly
scrollView.zoomScale = InitialZoomScaleConstants.minScale
contentView.translatesAutoresizingMaskIntoConstraints = true
contentView.frame = scrollView.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
videoRendererViewInfo.rendererView.translatesAutoresizingMaskIntoConstraints = true
videoRendererViewInfo.rendererView.frame = scrollView.bounds
videoRendererViewInfo.rendererView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.addSubview(videoRendererViewInfo.rendererView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private var streamSize: CGSize = .zero
private var zoomableRenderView: ZoomableVideoRenderView
init(_ rendererView: ZoomableVideoRenderView) {
self.zoomableRenderView = rendererView
super.init()
streamSize = rendererView.videoRendererViewInfo.streamSize
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return scrollView.subviews.first
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let initialMinZoomScale = InitialZoomScaleConstants.minScale
let boundedZoomScale = min(scrollView.zoomScale, scrollView.maximumZoomScale) * (1 / initialMinZoomScale)
if boundedZoomScale > 1 {
let aspectRatioVideoStream = self.streamSize != .zero ?
self.streamSize.width / self.streamSize.height : GeneralScreenConstants.defaultAspectRatio
let spectRatioScrollView = scrollView.bounds.width / scrollView.bounds.height
let scrollViewHasNarrowAspectRatio = spectRatioScrollView < aspectRatioVideoStream
var videoContentWidth = scrollView.bounds.width
var videoContentHeight = videoContentWidth / aspectRatioVideoStream
if !scrollViewHasNarrowAspectRatio {
videoContentHeight = scrollView.bounds.height
videoContentWidth = videoContentHeight * aspectRatioVideoStream
}
let ratioW = scrollView.frame.width / videoContentWidth
let ratioH = scrollView.frame.height / videoContentHeight
let ratio = ratioW < ratioH ? ratioW : ratioH
let newWidth = videoContentWidth * ratio
let newHeight = videoContentHeight * ratio
let left = 0.5 * (newWidth * boundedZoomScale > scrollView.frame.width ?
(newWidth - scrollView.frame.width)
: (scrollView.frame.width - scrollView.contentSize.width))
let top = 0.5 * (newHeight * boundedZoomScale > scrollView.frame.height ?
(!scrollViewHasNarrowAspectRatio ?
(newHeight - scrollView.frame.height) :
(scrollView.frame.height - scrollView.contentSize.height
+ (newHeight * boundedZoomScale - scrollView.frame.height)))
: (scrollView.frame.height - scrollView.contentSize.height))
scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
} else {
scrollView.contentInset = .zero
}
}
@objc func doubleTapped(gesture: UITapGestureRecognizer) {
guard let scrollView = gesture.view as? UIScrollView else {
return
}
let point = gesture.location(in: gesture.view)
let currentScale = scrollView.zoomScale
let minScale = InitialZoomScaleConstants.minScale
let maxScale = InitialZoomScaleConstants.maxScale
let finalScale = (currentScale == minScale) ? maxScale : minScale
zoom(scrollView, basedOn: point, scale: finalScale)
}
private func zoom(_ scrollView: UIScrollView, basedOn point: CGPoint, scale: CGFloat) {
// Normalize current content size back to content scale of Constants.minScale
var contentSize = CGSize()
contentSize.width = (scrollView.frame.width / scrollView.zoomScale)
contentSize.height = (scrollView.frame.height / scrollView.zoomScale)
// translate the zoom point to relative to the content rect
var zoomPoint = CGPoint()
zoomPoint.x = (point.x / scrollView.bounds.size.width) * contentSize.width
zoomPoint.y = (point.y / scrollView.bounds.size.height) * contentSize.height
// derive the size of the region to zoom to
var zoomSize = CGSize()
zoomSize.width = scrollView.bounds.size.width / scale
zoomSize.height = scrollView.bounds.size.height / scale
// offset the zoom rect so the actual zoom point is in the middle of the rectangle
var zoomRect = CGRect()
zoomRect.origin.x = zoomPoint.x - zoomSize.width / 2.0
zoomRect.origin.y = zoomPoint.y - zoomSize.height / 2.0
zoomRect.size.width = zoomSize.width
zoomRect.size.height = zoomSize.height
if scale == scrollView.maximumZoomScale {
scrollView.zoom(to: zoomRect, animated: true)
} else {
scrollView.setZoomScale(InitialZoomScaleConstants.minScale,
animated: true)
}
}
func videoStreamRenderer(didRenderFirstFrameWithSize size: CGSize) {
streamSize = size
}
}
}