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 ...@@ -286,6 +286,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
completionHandler() 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 { extension AppDelegate: PKPushRegistryDelegate {
......
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints> <constraints>
<constraint firstItem="fac-lR-4on" firstAttribute="leading" secondItem="T4r-6x-bEH" secondAttribute="leading" id="5S2-6O-mBj"/> <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="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 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"/> <constraint firstAttribute="bottom" secondItem="fac-lR-4on" secondAttribute="bottom" constant="10" id="px3-II-ycz"/>
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</view> </view>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="KFB-xs-EVT"> <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> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth"> <userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="0.0"/> <real key="value" value="0.0"/>
...@@ -165,7 +165,7 @@ ...@@ -165,7 +165,7 @@
<constraint firstItem="DMu-Or-dd7" firstAttribute="height" secondItem="ZVy-nB-bKJ" secondAttribute="height" id="Xd0-GB-gSC"/> <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 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 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"/> <constraint firstItem="DMu-Or-dd7" firstAttribute="centerY" secondItem="ZVy-nB-bKJ" secondAttribute="centerY" id="yFh-zq-lXh"/>
</constraints> </constraints>
</view> </view>
...@@ -183,15 +183,17 @@ ...@@ -183,15 +183,17 @@
<constraint firstItem="zMN-6z-uXT" firstAttribute="centerX" secondItem="QpJ-Sx-9dG" secondAttribute="centerX" id="8Mt-nX-xlY"/> <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="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="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 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 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 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 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="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="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="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="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="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="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"/> <constraint firstItem="fnt-PQ-Q6P" firstAttribute="centerX" secondItem="QpJ-Sx-9dG" secondAttribute="centerX" id="b3O-Sw-To4"/>
...@@ -214,6 +216,8 @@ ...@@ -214,6 +216,8 @@
<connections> <connections>
<outlet property="audioOnlyImage" destination="D1t-vZ-Pww" id="mE2-hu-x6s"/> <outlet property="audioOnlyImage" destination="D1t-vZ-Pww" id="mE2-hu-x6s"/>
<outlet property="buttonsContainer" destination="LK6-u0-eLU" id="rb7-9y-4Eh"/> <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="callInfoTimerLabel" destination="fac-lR-4on" id="fMl-JM-d9Y"/>
<outlet property="callNameLabel" destination="cgd-Wa-clf" id="oEa-7j-Eac"/> <outlet property="callNameLabel" destination="cgd-Wa-clf" id="oEa-7j-Eac"/>
<outlet property="callProfileImage" destination="T4r-6x-bEH" id="Pi5-eB-Eve"/> <outlet property="callProfileImage" destination="T4r-6x-bEH" id="Pi5-eB-Eve"/>
...@@ -224,7 +228,8 @@ ...@@ -224,7 +228,8 @@
<outlet property="incomingVideo" destination="DMu-Or-dd7" id="ogh-ft-54u"/> <outlet property="incomingVideo" destination="DMu-Or-dd7" id="ogh-ft-54u"/>
<outlet property="infoBottomLabel" destination="SdV-jx-Mla" id="yX9-em-p4w"/> <outlet property="infoBottomLabel" destination="SdV-jx-Mla" id="yX9-em-p4w"/>
<outlet property="infoContainer" destination="3RN-4M-qR4" id="CUO-h6-mFf"/> <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="mainView" destination="QpJ-Sx-9dG" id="0y9-R4-q5W"/>
<outlet property="nameLabel" destination="73Y-N1-Yga" id="XcQ-V6-ZrF"/> <outlet property="nameLabel" destination="73Y-N1-Yga" id="XcQ-V6-ZrF"/>
<outlet property="profileImageView" destination="fnt-PQ-Q6P" id="MgB-Ev-bTc"/> <outlet property="profileImageView" destination="fnt-PQ-Q6P" id="MgB-Ev-bTc"/>
......
...@@ -44,7 +44,10 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { ...@@ -44,7 +44,10 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
@IBOutlet private weak var audioOnlyImage: UIImageView! @IBOutlet private weak var audioOnlyImage: UIImageView!
@IBOutlet private weak var callNameLabel: UILabel! @IBOutlet private weak var callNameLabel: UILabel!
@IBOutlet private weak var callInfoTimerLabel: 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 callPulse: UIView!
@IBOutlet private weak var buttonsContainer: ButtonsContainerView! @IBOutlet private weak var buttonsContainer: ButtonsContainerView!
...@@ -57,6 +60,8 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { ...@@ -57,6 +60,8 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
private var task: DispatchWorkItem? private var task: DispatchWorkItem?
private var shouldRotateScreen = false
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(screenTapped)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(screenTapped))
...@@ -156,6 +161,12 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { ...@@ -156,6 +161,12 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
.bind(to: self.buttonsContainer.pauseCallButton.rx.image()) .bind(to: self.buttonsContainer.pauseCallButton.rx.image())
.disposed(by: self.disposeBag) .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 // disable switch camera button for audio only calls
self.buttonsContainer.switchCameraButton.isEnabled = !(self.viewModel.isAudioOnly) self.buttonsContainer.switchCameraButton.isEnabled = !(self.viewModel.isAudioOnly)
} }
...@@ -285,24 +296,40 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { ...@@ -285,24 +296,40 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
self.view.layoutIfNeeded() 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() { func showContactInfo() {
if !self.infoContainer.isHidden { if !self.infoContainer.isHidden {
task?.cancel() task?.cancel()
self.hideContactInfo() self.hideContactInfo()
return return
} }
self.infoLabelTopConstraint.constant = -200.00
self.infoLabelConstraint.constant = -200.00 self.callButtonsRightConstraint.constant = self.view.bounds.width
self.callButtonsLeftConstraint.constant = -self.view.bounds.width
self.buttonsContainer.isHidden = false self.buttonsContainer.isHidden = false
self.infoContainer.isHidden = false self.infoContainer.isHidden = false
self.view.layoutIfNeeded() self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.2, delay: 0.0, UIView.animate(withDuration: 0.2, delay: 0.0,
options: .curveEaseOut, options: .curveEaseOut,
animations: { [weak self] in animations: { [unowned self] in
self?.infoLabelConstraint.constant = 0.00 self.infoLabelTopConstraint.constant = 0.00
self?.view.layoutIfNeeded() self.callButtonsRightConstraint.constant = 0.00
}, completion: nil) self.callButtonsLeftConstraint.constant = 0.00
self.view.layoutIfNeeded()
}, completion: nil)
task = DispatchWorkItem { self.hideContactInfo() } task = DispatchWorkItem { self.hideContactInfo() }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task!) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task!)
...@@ -311,18 +338,33 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { ...@@ -311,18 +338,33 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
func hideContactInfo() { func hideContactInfo() {
UIView.animate(withDuration: 0.2, delay: 0.00, UIView.animate(withDuration: 0.2, delay: 0.00,
options: .curveEaseOut, options: .curveEaseOut,
animations: { [weak self] in animations: { [unowned self] in
self?.infoLabelConstraint.constant = -200.00 self.infoLabelTopConstraint.constant = -200.00
self?.view.layoutIfNeeded() self.callButtonsRightConstraint.constant = self.view.bounds.width
}, completion: { [weak self] _ in self.callButtonsLeftConstraint.constant = -self.view.bounds.width
self?.infoContainer.isHidden = true self.view.layoutIfNeeded()
self?.buttonsContainer.isHidden = true }, completion: { [weak self] _ in
self?.infoContainer.isHidden = true
self?.buttonsContainer.isHidden = true
}) })
} }
func showAllInfo() { func showAllInfo() {
self.buttonsContainer.isHidden = false self.buttonsContainer.isHidden = false
self.infoContainer.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 { ...@@ -146,19 +146,19 @@ class CallViewModel: Stateable, ViewModel {
}) })
}() }()
lazy var shouldRespondOnTap: Observable<Bool> = { lazy var isActiveVideoCall: Observable<Bool> = {
return self.callService.currentCall return self.callService.currentCall
.filter({ [weak self] call in .filter({ [weak self] call in
return call.callId == self?.call?.callId return call.callId == self?.call?.callId
}).map({ call in }).map({ call in
return call.state == .current return call.state == .current && !self.isAudioOnly
}) })
}() }()
lazy var showCallOptions: Observable<Bool> = { lazy var showCallOptions: Observable<Bool> = {
return Observable.combineLatest(self.screenTapped.asObservable(), return Observable.combineLatest(self.screenTapped.asObservable(),
shouldRespondOnTap) { [unowned self] (tapped, shouldRespond) in isActiveVideoCall) { [unowned self] (tapped, shouldRespond) in
if tapped && shouldRespond && !self.isAudioOnly { if tapped && shouldRespond {
return true return true
} }
return false return false
...@@ -378,4 +378,8 @@ class CallViewModel: Stateable, ViewModel { ...@@ -378,4 +378,8 @@ class CallViewModel: Stateable, ViewModel {
func switchSpeaker() { func switchSpeaker() {
self.audioService.switchSpeaker() self.audioService.switchSpeaker()
} }
func setCameraOrientation(orientation: UIDeviceOrientation) {
videoService.setCameraOrientation(orientation: orientation)
}
} }
...@@ -93,6 +93,18 @@ extension UIImage { ...@@ -93,6 +93,18 @@ extension UIImage {
return imageData 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? { func resizeIntoRectangle(of size: CGSize) -> UIImage? {
if self.size.width < size.width && self.size.height < size.height { if self.size.width < size.width && self.size.height < size.height {
return self return self
......
...@@ -44,9 +44,14 @@ protocol FrameExtractorDelegate: class { ...@@ -44,9 +44,14 @@ protocol FrameExtractorDelegate: class {
class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
let nameLandscape = "frontCameraLanscape"
let namePortrait = "frontCameraPortrait"
let nameCamera = "camera://"
private let log = SwiftyBeaver.self private let log = SwiftyBeaver.self
private let quality = AVCaptureSession.Preset.medium private let quality = AVCaptureSession.Preset.medium
private var orientation = AVCaptureVideoOrientation.portrait
var permissionGranted = Variable<Bool>(false) var permissionGranted = Variable<Bool>(false)
...@@ -64,7 +69,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { ...@@ -64,7 +69,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
super.init() 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 { guard self.permissionGranted.value else {
throw VideoError.needPermission throw VideoError.needPermission
} }
...@@ -82,11 +87,19 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { ...@@ -82,11 +87,19 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
bestRate = frameRates.maxFrameRate bestRate = frameRates.maxFrameRate
} }
} }
let devInfo: DeviceInfo = ["format": "BGRA", if orientation == .portrait || orientation == .portraitUpsideDown {
"width": String(dimensions.height), let devInfo: DeviceInfo = ["format": "BGRA",
"height": String(dimensions.width), "width": String(dimensions.height),
"rate": String(bestRate)] "height": String(dimensions.width),
return devInfo "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() { func startCapturing() {
...@@ -120,7 +133,8 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { ...@@ -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() captureSession.beginConfiguration()
guard self.permissionGranted.value else { guard self.permissionGranted.value else {
throw VideoError.needPermission throw VideoError.needPermission
...@@ -149,7 +163,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { ...@@ -149,7 +163,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
guard connection.isVideoMirroringSupported else { guard connection.isVideoMirroringSupported else {
throw VideoError.unsupportedParameter throw VideoError.unsupportedParameter
} }
connection.videoOrientation = .portrait connection.videoOrientation = orientation
connection.isVideoMirrored = position == .front connection.isVideoMirrored = position == .front
captureSession.commitConfiguration() captureSession.commitConfiguration()
} }
...@@ -203,7 +217,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { ...@@ -203,7 +217,7 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
completable(.error(VideoError.switchCameraFailed)) completable(.error(VideoError.switchCameraFailed))
return Disposables.create {} return Disposables.create {}
} }
connection.videoOrientation = .portrait connection.videoOrientation = self.orientation
self.captureSession.commitConfiguration() self.captureSession.commitConfiguration()
completable(.completed) completable(.completed)
} else { } else {
...@@ -213,6 +227,30 @@ class FrameExtractor: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { ...@@ -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 // MARK: Sample buffer to UIImage conversion
private func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage? { private func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage? {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
...@@ -239,6 +277,7 @@ class VideoService: FrameExtractorDelegate { ...@@ -239,6 +277,7 @@ class VideoService: FrameExtractorDelegate {
let capturedVideoFrame = PublishSubject<UIImage?>() let capturedVideoFrame = PublishSubject<UIImage?>()
private let log = SwiftyBeaver.self private let log = SwiftyBeaver.self
private var blockOutgoingFrame = true
fileprivate let disposeBag = DisposeBag() fileprivate let disposeBag = DisposeBag()
...@@ -262,11 +301,13 @@ class VideoService: FrameExtractorDelegate { ...@@ -262,11 +301,13 @@ class VideoService: FrameExtractorDelegate {
private func enumerateVideoInputDevices() { private func enumerateVideoInputDevices() {
do { do {
try camera.configureSession(withPosition: AVCaptureDevice.Position.front) try camera.configureSession(withPosition: AVCaptureDevice.Position.front, withOrientation: AVCaptureVideoOrientation.portrait)
self.log.debug("Camera successfully configured") self.log.debug("Camera successfully configured")
let frontCameraDevInfo: [String: String] = try camera.getDeviceInfo(forPosition: AVCaptureDevice.Position.front) let frontLandscapeCameraDevInfo: [String: String] = try camera.getDeviceInfo(forPosition: AVCaptureDevice.Position.front, orientation: .landscapeLeft)
self.log.debug("Camera device info: \(frontCameraDevInfo as AnyObject)") let frontPortraitCameraDevInfo: [String: String] = try camera.getDeviceInfo(forPosition: AVCaptureDevice.Position.front, orientation: .portrait)
videoAdapter.addVideoDevice(withName: "frontCamera", withDevInfo: frontCameraDevInfo) videoAdapter.addVideoDevice(withName: camera.nameLandscape, withDevInfo: frontLandscapeCameraDevInfo)
videoAdapter.addVideoDevice(withName: camera.namePortrait, withDevInfo: frontPortraitCameraDevInfo)
} catch let e as VideoError { } catch let e as VideoError {
self.log.error("Error during capture device enumeration: \(e)") self.log.error("Error during capture device enumeration: \(e)")
} catch { } catch {
...@@ -282,6 +323,33 @@ class VideoService: FrameExtractorDelegate { ...@@ -282,6 +323,33 @@ class VideoService: FrameExtractorDelegate {
print(error) print(error)
}.disposed(by: self.disposeBag) }.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 { extension VideoService: VideoAdapterDelegate {
...@@ -306,6 +374,7 @@ extension VideoService: VideoAdapterDelegate { ...@@ -306,6 +374,7 @@ extension VideoService: VideoAdapterDelegate {
func startCapture(withDevice device: String) { func startCapture(withDevice device: String) {
self.log.debug("Capture started...") self.log.debug("Capture started...")
self.camera.startCapturing() self.camera.startCapturing()
self.blockOutgoingFrame = false
} }
func stopCapture() { func stopCapture() {
...@@ -318,8 +387,10 @@ extension VideoService: VideoAdapterDelegate { ...@@ -318,8 +387,10 @@ extension VideoService: VideoAdapterDelegate {
} }
func captured(image: UIImage) { func captured(image: UIImage) {
videoAdapter.writeOutgoingFrame(with: image)
self.capturedVideoFrame.onNext(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