189 lines
6.9 KiB
Swift
189 lines
6.9 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import MetalKit
|
|
import SignalRingRTC
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
import WebRTC
|
|
|
|
class RemoteVideoView: UIView {
|
|
private lazy var rtcMetalView = RTCMTLVideoView(frame: bounds)
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
addSubview(rtcMetalView)
|
|
rtcMetalView.autoPinEdgesToSuperviewEdges()
|
|
// We want the rendered video to go edge-to-edge.
|
|
rtcMetalView.layoutMargins = .zero
|
|
// HACK: Although RTCMTLVideo view is positioned to the top edge of the screen
|
|
// It's inner (private) MTKView is below the status bar.
|
|
for subview in rtcMetalView.subviews {
|
|
if subview is MTKView {
|
|
subview.autoPinEdgesToSuperviewEdges()
|
|
} else {
|
|
owsFailDebug("New subviews added to MTLVideoView. Reconsider this hack.")
|
|
}
|
|
}
|
|
|
|
applyDefaultRendererConfiguration()
|
|
|
|
if Platform.isSimulator {
|
|
backgroundColor = .blue.withAlphaComponent(0.4)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
var isScreenShare = false {
|
|
didSet {
|
|
if oldValue != isScreenShare {
|
|
applyDefaultRendererConfigurationOnNextFrame = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var isGroupCall = false {
|
|
didSet {
|
|
if oldValue != isGroupCall {
|
|
applyDefaultRendererConfigurationOnNextFrame = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var isFullScreen = false {
|
|
didSet {
|
|
if oldValue != isFullScreen {
|
|
applyDefaultRendererConfigurationOnNextFrame = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private var applyDefaultRendererConfigurationOnNextFrame = false
|
|
|
|
private func applyDefaultRendererConfiguration() {
|
|
if UIDevice.current.isIPad {
|
|
rtcMetalView.videoContentMode = .scaleAspectFit
|
|
rtcMetalView.rotationOverride = nil
|
|
} else {
|
|
rtcMetalView.videoContentMode = .scaleAspectFill
|
|
rtcMetalView.rotationOverride = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension RemoteVideoView: RTCVideoRenderer {
|
|
|
|
func setSize(_ size: CGSize) {
|
|
rtcMetalView.setSize(size)
|
|
}
|
|
|
|
func renderFrame(_ frame: RTCVideoFrame?) {
|
|
rtcMetalView.renderFrame(frame)
|
|
|
|
DispatchMainThreadSafe { [self] in
|
|
if applyDefaultRendererConfigurationOnNextFrame {
|
|
applyDefaultRendererConfigurationOnNextFrame = false
|
|
applyDefaultRendererConfiguration()
|
|
}
|
|
|
|
guard let frame else { return }
|
|
|
|
// In certain cases, rotate the video so it's always right side up in landscape.
|
|
// We only allow portrait orientation in the calling views on iPhone so we don't
|
|
// get this for free.
|
|
let shouldOverrideRotation: Bool
|
|
if UIDevice.current.isIPad || !isFullScreen {
|
|
// iPad allows all orientations so we can skip this.
|
|
// Non-full-screen views are part of a portrait-locked UI (e.g. the group call grid)
|
|
// and rotating the video without rotating the UI would look weird.
|
|
shouldOverrideRotation = false
|
|
} else if isGroupCall {
|
|
// For speaker view in a group call, keep screenshares right-side up.
|
|
shouldOverrideRotation = isScreenShare
|
|
} else {
|
|
// For a 1:1 call, always keep video right-side up.
|
|
shouldOverrideRotation = true
|
|
}
|
|
|
|
let isLandscape: Bool
|
|
if shouldOverrideRotation {
|
|
switch UIDevice.current.orientation {
|
|
case .portrait, .portraitUpsideDown:
|
|
// We don't have to do anything, the renderer will automatically
|
|
// make sure it's right-side-up.
|
|
isLandscape = false
|
|
rtcMetalView.rotationOverride = nil
|
|
|
|
case .landscapeLeft:
|
|
isLandscape = true
|
|
switch frame.rotation {
|
|
// Portrait upside-down
|
|
case ._270:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._0.rawValue)
|
|
|
|
// Portrait
|
|
case ._90:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._180.rawValue)
|
|
|
|
// Landscape right
|
|
case ._180:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._270.rawValue)
|
|
|
|
// Landscape left
|
|
case ._0:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._90.rawValue)
|
|
@unknown default:
|
|
owsFailBeta("unknown frame.rotation: \(frame.rotation)")
|
|
}
|
|
|
|
case .landscapeRight:
|
|
isLandscape = true
|
|
switch frame.rotation {
|
|
// Portrait upside-down
|
|
case ._270:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._180.rawValue)
|
|
|
|
// Portrait
|
|
case ._90:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._0.rawValue)
|
|
|
|
// Landscape right
|
|
case ._180:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._90.rawValue)
|
|
|
|
// Landscape left
|
|
case ._0:
|
|
rtcMetalView.rotationOverride = NSNumber(value: RTCVideoRotation._270.rawValue)
|
|
@unknown default:
|
|
owsFailBeta("unknown frame.rotation: \(frame.rotation)")
|
|
}
|
|
|
|
default:
|
|
// Do nothing if we're face down, up, etc.
|
|
// Assume we're already set up for the correct orientation.
|
|
isLandscape = false
|
|
}
|
|
} else {
|
|
rtcMetalView.rotationOverride = nil
|
|
isLandscape = bounds.width > bounds.height
|
|
}
|
|
|
|
let remoteIsLandscape = frame.rotation == RTCVideoRotation._180 || frame.rotation == RTCVideoRotation._0
|
|
let isSquarish = max(bounds.width, bounds.height) / min(bounds.width, bounds.height) <= 1.2
|
|
|
|
// If we're both in the same orientation, let the video fill the screen.
|
|
// Otherwise, fit the video to the screen size respecting the aspect ratio.
|
|
if (isLandscape == remoteIsLandscape || isSquarish) && !isScreenShare {
|
|
rtcMetalView.videoContentMode = .scaleAspectFill
|
|
} else {
|
|
rtcMetalView.videoContentMode = .scaleAspectFit
|
|
}
|
|
}
|
|
}
|
|
}
|