Commit 06e1c999 authored by Andreas Traczyk's avatar Andreas Traczyk

video calls: support device rotation

Change-Id: If5e224fedf0f4447eebff3753961a019118395b7
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent 19093e0b
......@@ -286,6 +286,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
completionHandler()
}
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
if (rootViewController.responds(to: Selector(("canRotate")))) {
return .all
}
}
return .portrait
}
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
if rootViewController == nil {
return nil
}
if rootViewController.isKind(of: (UITabBarController).self) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
} else if rootViewController.isKind(of: (UINavigationController).self) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
} else if rootViewController.presentedViewController != nil {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
}
extension AppDelegate: PKPushRegistryDelegate {
......
......@@ -118,7 +118,7 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="fac-lR-4on" firstAttribute="leading" secondItem="T4r-6x-bEH" secondAttribute="leading" id="5S2-6O-mBj"/>
<constraint firstItem="T4r-6x-bEH" firstAttribute="centerY" secondItem="z3c-S7-uGw" secondAttribute="centerY" id="Ece-ax-84D"/>
<constraint firstItem="T4r-6x-bEH" firstAttribute="top" secondItem="z3c-S7-uGw" secondAttribute="top" constant="60" id="diz-IR-6aJ"/>
<constraint firstItem="cgd-Wa-clf" firstAttribute="leading" secondItem="T4r-6x-bEH" secondAttribute="trailing" constant="10" id="f1b-Fu-RU9"/>
<constraint firstItem="cgd-Wa-clf" firstAttribute="centerY" secondItem="z3c-S7-uGw" secondAttribute="centerY" id="hhM-ww-O86"/>
<constraint firstAttribute="bottom" secondItem="fac-lR-4on" secondAttribute="bottom" constant="10" id="px3-II-ycz"/>
......@@ -141,7 +141,7 @@
</userDefinedRuntimeAttributes>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="KFB-xs-EVT">
<rect key="frame" x="258" y="40" width="107" height="111"/>
<rect key="frame" x="258" y="50" width="107" height="111"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="0.0"/>
......@@ -165,7 +165,7 @@
<constraint firstItem="DMu-Or-dd7" firstAttribute="height" secondItem="ZVy-nB-bKJ" secondAttribute="height" id="Xd0-GB-gSC"/>
<constraint firstItem="D1t-vZ-Pww" firstAttribute="centerY" secondItem="ZVy-nB-bKJ" secondAttribute="centerY" id="ZOF-vM-2OV"/>
<constraint firstAttribute="trailing" secondItem="KFB-xs-EVT" secondAttribute="trailing" constant="10" id="eEm-Ok-tjV"/>
<constraint firstItem="KFB-xs-EVT" firstAttribute="top" secondItem="ZVy-nB-bKJ" secondAttribute="top" constant="40" id="jZm-0x-mBh"/>
<constraint firstItem="KFB-xs-EVT" firstAttribute="top" secondItem="ZVy-nB-bKJ" secondAttribute="top" constant="50" id="jZm-0x-mBh"/>
<constraint firstItem="DMu-Or-dd7" firstAttribute="centerY" secondItem="ZVy-nB-bKJ" secondAttribute="centerY" id="yFh-zq-lXh"/>
</constraints>
</view>
......@@ -183,15 +183,17 @@
<constraint firstItem="zMN-6z-uXT" firstAttribute="centerX" secondItem="QpJ-Sx-9dG" secondAttribute="centerX" id="8Mt-nX-xlY"/>
<constraint firstItem="Dj8-CK-PQm" firstAttribute="width" secondItem="fnt-PQ-Q6P" secondAttribute="width" id="8Pl-sx-ODH"/>
<constraint firstItem="zMN-6z-uXT" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="QpJ-Sx-9dG" secondAttribute="leading" constant="8" id="Bf4-J4-K9c"/>
<constraint firstItem="fnt-PQ-Q6P" firstAttribute="top" secondItem="WrD-XI-6aI" secondAttribute="bottom" constant="130" id="C6d-Dz-lnR"/>
<constraint firstItem="fnt-PQ-Q6P" firstAttribute="top" secondItem="WrD-XI-6aI" secondAttribute="bottom" priority="750" constant="130" id="C6d-Dz-lnR"/>
<constraint firstItem="ZVy-nB-bKJ" firstAttribute="height" secondItem="QpJ-Sx-9dG" secondAttribute="height" id="DB0-aS-tKo"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="SdV-jx-Mla" secondAttribute="trailing" constant="8" id="EDd-Cg-QHP"/>
<constraint firstItem="uC8-vY-dHO" firstAttribute="leading" secondItem="QpJ-Sx-9dG" secondAttribute="leading" id="G08-ef-Ucc"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="73Y-N1-Yga" secondAttribute="trailing" constant="8" id="Gcb-08-NRr"/>
<constraint firstItem="73Y-N1-Yga" firstAttribute="top" secondItem="fnt-PQ-Q6P" secondAttribute="bottom" constant="8" id="JC6-KJ-L8L"/>
<constraint firstItem="SdV-jx-Mla" firstAttribute="centerX" secondItem="QpJ-Sx-9dG" secondAttribute="centerX" id="MXS-7j-cD5"/>
<constraint firstItem="ZVy-nB-bKJ" firstAttribute="top" secondItem="QpJ-Sx-9dG" secondAttribute="top" id="NrK-DR-BPG"/>
<constraint firstItem="uC8-vY-dHO" firstAttribute="top" secondItem="QpJ-Sx-9dG" secondAttribute="top" id="Rse-54-gPI"/>
<constraint firstItem="uC8-vY-dHO" firstAttribute="height" secondItem="QpJ-Sx-9dG" secondAttribute="height" id="UFy-Kq-FAS"/>
<constraint firstItem="LK6-u0-eLU" firstAttribute="top" relation="greaterThanOrEqual" secondItem="fnt-PQ-Q6P" secondAttribute="bottom" constant="5" id="VKn-Jp-AH3"/>
<constraint firstItem="zMN-6z-uXT" firstAttribute="top" secondItem="73Y-N1-Yga" secondAttribute="bottom" constant="16" id="YQp-tl-h73"/>
<constraint firstItem="SdV-jx-Mla" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="QpJ-Sx-9dG" secondAttribute="leading" constant="8" id="Zms-si-GOc"/>
<constraint firstItem="fnt-PQ-Q6P" firstAttribute="centerX" secondItem="QpJ-Sx-9dG" secondAttribute="centerX" id="b3O-Sw-To4"/>
......@@ -214,6 +216,8 @@
<connections>
<outlet property="audioOnlyImage" destination="D1t-vZ-Pww" id="mE2-hu-x6s"/>
<outlet property="buttonsContainer" destination="LK6-u0-eLU" id="rb7-9y-4Eh"/>
<outlet property="callButtonsLeftConstraint" destination="0oL-jv-UVj" id="Tvs-hZ-N1z"/>
<outlet property="callButtonsRightConstraint" destination="cl9-Fw-bSb" id="dRy-Dq-W2H"/>
<outlet property="callInfoTimerLabel" destination="fac-lR-4on" id="fMl-JM-d9Y"/>
<outlet property="callNameLabel" destination="cgd-Wa-clf" id="oEa-7j-Eac"/>
<outlet property="callProfileImage" destination="T4r-6x-bEH" id="Pi5-eB-Eve"/>
......@@ -224,7 +228,8 @@
<outlet property="incomingVideo" destination="DMu-Or-dd7" id="ogh-ft-54u"/>
<outlet property="infoBottomLabel" destination="SdV-jx-Mla" id="yX9-em-p4w"/>
<outlet property="infoContainer" destination="3RN-4M-qR4" id="CUO-h6-mFf"/>
<outlet property="infoLabelConstraint" destination="72y-vN-PbI" id="sGV-5n-H9t"/>
<outlet property="infoLabelHeightConstraint" destination="b8L-UJ-IKC" id="ie0-mK-KgH"/>
<outlet property="infoLabelTopConstraint" destination="72y-vN-PbI" id="OcB-3V-VcL"/>
<outlet property="mainView" destination="QpJ-Sx-9dG" id="0y9-R4-q5W"/>
<outlet property="nameLabel" destination="73Y-N1-Yga" id="XcQ-V6-ZrF"/>
<outlet property="profileImageView" destination="fnt-PQ-Q6P" id="MgB-Ev-bTc"/>
......
......@@ -44,7 +44,10 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
@IBOutlet private weak var audioOnlyImage: UIImageView!
@IBOutlet private weak var callNameLabel: UILabel!
@IBOutlet private weak var callInfoTimerLabel: UILabel!
@IBOutlet private weak var infoLabelConstraint: NSLayoutConstraint!
@IBOutlet private weak var infoLabelTopConstraint: NSLayoutConstraint!
@IBOutlet private weak var callButtonsLeftConstraint: NSLayoutConstraint!
@IBOutlet private weak var callButtonsRightConstraint: NSLayoutConstraint!
@IBOutlet private weak var infoLabelHeightConstraint: NSLayoutConstraint!
@IBOutlet private weak var callPulse: UIView!
@IBOutlet private weak var buttonsContainer: ButtonsContainerView!
......@@ -57,6 +60,8 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
private var task: DispatchWorkItem?
private var shouldRotateScreen = false
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(screenTapped))
......@@ -156,6 +161,12 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
.bind(to: self.buttonsContainer.pauseCallButton.rx.image())
.disposed(by: self.disposeBag)
self.viewModel.isActiveVideoCall
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] rotate in
self?.shouldRotateScreen = rotate
}).disposed(by: self.disposeBag)
// disable switch camera button for audio only calls
self.buttonsContainer.switchCameraButton.isEnabled = !(self.viewModel.isAudioOnly)
}
......@@ -285,24 +296,40 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
self.view.layoutIfNeeded()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let orientation = UIDevice.current.orientation
switch orientation {
case .landscapeRight, .landscapeLeft:
let height = size.height - 150
self.infoLabelHeightConstraint.constant = height
default:
self.infoLabelHeightConstraint.constant = 200
}
self.viewModel.setCameraOrientation(orientation: UIDevice.current.orientation)
super.viewWillTransition(to: size, with: coordinator)
}
func showContactInfo() {
if !self.infoContainer.isHidden {
task?.cancel()
self.hideContactInfo()
return
}
self.infoLabelConstraint.constant = -200.00
self.infoLabelTopConstraint.constant = -200.00
self.callButtonsRightConstraint.constant = self.view.bounds.width
self.callButtonsLeftConstraint.constant = -self.view.bounds.width
self.buttonsContainer.isHidden = false
self.infoContainer.isHidden = false
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.2, delay: 0.0,
options: .curveEaseOut,
animations: { [weak self] in
self?.infoLabelConstraint.constant = 0.00
self?.view.layoutIfNeeded()
}, completion: nil)
animations: { [unowned self] in
self.infoLabelTopConstraint.constant = 0.00
self.callButtonsRightConstraint.constant = 0.00
self.callButtonsLeftConstraint.constant = 0.00
self.view.layoutIfNeeded()
}, completion: nil)
task = DispatchWorkItem { self.hideContactInfo() }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task!)
......@@ -311,18 +338,33 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
func hideContactInfo() {
UIView.animate(withDuration: 0.2, delay: 0.00,
options: .curveEaseOut,
animations: { [weak self] in
self?.infoLabelConstraint.constant = -200.00
self?.view.layoutIfNeeded()
}, completion: { [weak self] _ in
self?.infoContainer.isHidden = true
self?.buttonsContainer.isHidden = true
animations: { [unowned self] in
self.infoLabelTopConstraint.constant = -200.00
self.callButtonsRightConstraint.constant = self.view.bounds.width
self.callButtonsLeftConstraint.constant = -self.view.bounds.width
self.view.layoutIfNeeded()
}, completion: { [weak self] _ in
self?.infoContainer.isHidden = true
self?.buttonsContainer.isHidden = true
})
}
func showAllInfo() {
self.buttonsContainer.isHidden = false
self.infoContainer.isHidden = false
self.infoLabelConstraint.constant = 0.00
self.infoLabelTopConstraint.constant = 0.00
}
@objc func canRotate() {
// empty function to support call screen rotation
}
override func viewWillDisappear(_ animated: Bool) {
UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
super.viewWillDisappear(animated)
}
override var shouldAutorotate: Bool {
return self.shouldRotateScreen
}
}
......@@ -146,19 +146,19 @@ class CallViewModel: Stateable, ViewModel {
})
}()
lazy var shouldRespondOnTap: Observable<Bool> = {
lazy var isActiveVideoCall: Observable<Bool> = {
return self.callService.currentCall
.filter({ [weak self] call in
return call.callId == self?.call?.callId
}).map({ call in
return call.state == .current
return call.state == .current && !self.isAudioOnly
})
}()
lazy var showCallOptions: Observable<Bool> = {
return Observable.combineLatest(self.screenTapped.asObservable(),
shouldRespondOnTap) { [unowned self] (tapped, shouldRespond) in
if tapped && shouldRespond && !self.isAudioOnly {
isActiveVideoCall) { [unowned self] (tapped, shouldRespond) in
if tapped && shouldRespond {
return true
}
return false
......@@ -378,4 +378,8 @@ class CallViewModel: Stateable, ViewModel {
func switchSpeaker() {
self.audioService.switchSpeaker()
}
func setCameraOrientation(orientation: UIDeviceOrientation) {
videoService.setCameraOrientation(orientation: orientation)
}
}
......@@ -93,6 +93,18 @@ extension UIImage {
return imageData
}
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
func resizeIntoRectangle(of size: CGSize) -> UIImage? {
if self.size.width < size.width && self.size.height < size.height {
return self
......
......@@ -44,9 +44,14 @@ protocol FrameExtractorDelegate: class {
class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
let nameLandscape = "frontCameraLanscape"
let namePortrait = "frontCameraPortrait"
let nameCamera = "camera://"
private let log = SwiftyBeaver.self
private let quality = AVCaptureSession.Preset.medium
private var orientation = AVCaptureVideoOrientation.portrait
var permissionGranted = Variable<Bool>(false)
......@@ -64,7 +69,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
super.init()
}
func getDeviceInfo(forPosition position: AVCaptureDevice.Position) throws -> DeviceInfo {
func getDeviceInfo(forPosition position: AVCaptureDevice.Position, orientation: UIDeviceOrientation) throws -> DeviceInfo {
guard self.permissionGranted.value else {
throw VideoError.needPermission
}
......@@ -82,11 +87,19 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
bestRate = frameRates.maxFrameRate
}
}
let devInfo: DeviceInfo = ["format": "BGRA",
"width": String(dimensions.height),
"height": String(dimensions.width),
"rate": String(bestRate)]
return devInfo
if orientation == .portrait || orientation == .portraitUpsideDown {
let devInfo: DeviceInfo = ["format": "BGRA",
"width": String(dimensions.height),
"height": String(dimensions.width),
"rate": String(bestRate)]
return devInfo
} else {
let devInfo: DeviceInfo = ["format": "BGRA",
"width": String(dimensions.width),
"height": String(dimensions.height),
"rate": String(bestRate)]
return devInfo
}
}
func startCapturing() {
......@@ -120,7 +133,8 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
}
}
func configureSession(withPosition position: AVCaptureDevice.Position) throws {
func configureSession(withPosition position: AVCaptureDevice.Position,
withOrientation orientation: AVCaptureVideoOrientation) throws {
captureSession.beginConfiguration()
guard self.permissionGranted.value else {
throw VideoError.needPermission
......@@ -149,7 +163,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
guard connection.isVideoMirroringSupported else {
throw VideoError.unsupportedParameter
}
connection.videoOrientation = .portrait
connection.videoOrientation = orientation
connection.isVideoMirrored = position == .front
captureSession.commitConfiguration()
}
......@@ -203,7 +217,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
completable(.error(VideoError.switchCameraFailed))
return Disposables.create {}
}
connection.videoOrientation = .portrait
connection.videoOrientation = self.orientation
self.captureSession.commitConfiguration()
completable(.completed)
} else {
......@@ -213,6 +227,30 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
}
}
func rotateCamera(orientation: AVCaptureVideoOrientation) -> Completable {
return Completable.create { [unowned self] completable in
guard self.permissionGranted.value else {
completable(.error(VideoError.needPermission))
return Disposables.create {}
}
self.captureSession.beginConfiguration()
let videoOutput = self.captureSession.outputs[0]
guard let connection = videoOutput.connection(with: AVFoundation.AVMediaType.video) else {
completable(.error(VideoError.getConnectionFailed))
return Disposables.create {}
}
guard connection.isVideoOrientationSupported else {
completable(.error(VideoError.unsupportedParameter))
return Disposables.create {}
}
self.orientation = orientation
connection.videoOrientation = orientation
self.captureSession.commitConfiguration()
completable(.completed)
return Disposables.create { }
}
}
// MARK: Sample buffer to UIImage conversion
private func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage? {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
......@@ -239,6 +277,7 @@ class VideoService: FrameExtractorDelegate {
let capturedVideoFrame = PublishSubject<UIImage?>()
private let log = SwiftyBeaver.self
private var blockOutgoingFrame = true
fileprivate let disposeBag = DisposeBag()
......@@ -262,11 +301,13 @@ class VideoService: FrameExtractorDelegate {
private func enumerateVideoInputDevices() {
do {
try camera.configureSession(withPosition: AVCaptureDevice.Position.front)
try camera.configureSession(withPosition: AVCaptureDevice.Position.front, withOrientation: AVCaptureVideoOrientation.portrait)
self.log.debug("Camera successfully configured")
let frontCameraDevInfo: [String: String] = try camera.getDeviceInfo(forPosition: AVCaptureDevice.Position.front)
self.log.debug("Camera device info: \(frontCameraDevInfo as AnyObject)")
videoAdapter.addVideoDevice(withName: "frontCamera", withDevInfo: frontCameraDevInfo)
let frontLandscapeCameraDevInfo: [String: String] = try camera.getDeviceInfo(forPosition: AVCaptureDevice.Position.front, orientation: .landscapeLeft)
let frontPortraitCameraDevInfo: [String: String] = try camera.getDeviceInfo(forPosition: AVCaptureDevice.Position.front, orientation: .portrait)
videoAdapter.addVideoDevice(withName: camera.nameLandscape, withDevInfo: frontLandscapeCameraDevInfo)
videoAdapter.addVideoDevice(withName: camera.namePortrait, withDevInfo: frontPortraitCameraDevInfo)
} catch let e as VideoError {
self.log.error("Error during capture device enumeration: \(e)")
} catch {
......@@ -282,6 +323,33 @@ class VideoService: FrameExtractorDelegate {
print(error)
}.disposed(by: self.disposeBag)
}
func setCameraOrientation(orientation: UIDeviceOrientation) {
self.blockOutgoingFrame = true
let deviceName: String =
(orientation == .landscapeLeft || orientation == .landscapeRight) ?
self.camera.nameLandscape : self.camera.namePortrait
self.switchInput(toDevice: self.camera.nameCamera + deviceName)
var newOrientation: AVCaptureVideoOrientation
switch orientation {
case .portrait:
newOrientation = AVCaptureVideoOrientation.portrait
case .portraitUpsideDown:
newOrientation = AVCaptureVideoOrientation.portraitUpsideDown
case .landscapeLeft:
newOrientation = AVCaptureVideoOrientation.landscapeRight
case .landscapeRight:
newOrientation = AVCaptureVideoOrientation.landscapeLeft
default:
newOrientation = AVCaptureVideoOrientation.portrait
}
self.camera.rotateCamera(orientation: newOrientation)
.subscribe(onCompleted: { [unowned self] in
self.log.debug("new camera orientation: \(orientation)")
}, onError: { error in
self.log.debug("camera re-orientation error: \(error)")
}).disposed(by: self.disposeBag)
}
}
extension VideoService: VideoAdapterDelegate {
......@@ -306,6 +374,7 @@ extension VideoService: VideoAdapterDelegate {
func startCapture(withDevice device: String) {
self.log.debug("Capture started...")
self.camera.startCapturing()
self.blockOutgoingFrame = false
}
func stopCapture() {
......@@ -318,8 +387,10 @@ extension VideoService: VideoAdapterDelegate {
}
func captured(image: UIImage) {
videoAdapter.writeOutgoingFrame(with: image)
self.capturedVideoFrame.onNext(image)
if self.blockOutgoingFrame {
return
}
videoAdapter.writeOutgoingFrame(with: image)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment