417 lines
15 KiB
Swift
417 lines
15 KiB
Swift
//
|
|
// StartViewController.swift
|
|
// vcam
|
|
//
|
|
// Created by Brian Smith on 11/10/20.
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import CocoaAsyncSocket
|
|
import Kronos
|
|
import GameController
|
|
|
|
class StartViewController : BaseViewController {
|
|
@IBOutlet weak var headerView : HeaderView!
|
|
@IBOutlet weak var versionLabel : UILabel!
|
|
|
|
@IBOutlet weak var restartView : UIView!
|
|
|
|
@IBOutlet weak var entryView : UIView!
|
|
@IBOutlet weak var entryViewYConstraint : NSLayoutConstraint!
|
|
|
|
@IBOutlet weak var ipAddress : UITextField!
|
|
@IBOutlet weak var connect : UIButton!
|
|
|
|
@IBOutlet weak var connectingView : UIVisualEffectView!
|
|
|
|
private var tapGesture : UITapGestureRecognizer!
|
|
|
|
private var streamingConnection : StreamingConnection?
|
|
private var gameController : GCController?
|
|
private var _liveLinkTimer : Timer?
|
|
|
|
|
|
@objc dynamic let appSettings = AppSettings.shared
|
|
private var observers = [NSKeyValueObservation]()
|
|
|
|
var pickerData: [String] = [String]()
|
|
var selectedStreamer: String = "";
|
|
|
|
var ipAddressIsDemoMode : Bool {
|
|
self.ipAddress.text == "demo.mode"
|
|
}
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
return .lightContent
|
|
}
|
|
|
|
// Constructor (before view is even loaded)
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setupObservers()
|
|
}
|
|
|
|
deinit {
|
|
Log.info("StartViewController destructed")
|
|
}
|
|
|
|
func setupObservers() {
|
|
// Observers for keyboard show/hide
|
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
|
|
// Observers for game controller connect/disconnect
|
|
NotificationCenter.default.addObserver(self, selector: #selector(gameControllerDidConnectNotification), name: .GCControllerDidConnect, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(gameControllerDidDisconnectNotification), name: .GCControllerDidDisconnect, object: nil)
|
|
|
|
// Observers for app settings
|
|
observers.append(observe(\.appSettings.timecodeSource, options: [.initial,.new,.old], changeHandler: { [weak self] object, change in
|
|
|
|
if let oldValue = TimecodeSource(rawValue:change.oldValue ?? 0) {
|
|
switch oldValue {
|
|
case .ntp:
|
|
Clock.reset()
|
|
case .tentacleSync:
|
|
Tentacle.shared = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
switch self?.appSettings.timecodeSourceEnum() {
|
|
case .ntp:
|
|
let pool = AppSettings.shared.ntpPool.isEmpty ? "time.apple.com" : AppSettings.shared.ntpPool
|
|
Log.info("Started NTP : \(pool)")
|
|
|
|
// IMPORTANT
|
|
// We reset the NTP clock first --
|
|
// otherwise we don't know if the NTP address that is used for the pool is valid or not because it
|
|
// will be using a stale last valid time
|
|
Clock.reset()
|
|
Clock.sync(from: pool)
|
|
case .tentacleSync:
|
|
Tentacle.shared = Tentacle()
|
|
default:
|
|
break
|
|
}
|
|
|
|
}))
|
|
|
|
// any change to the subject name will remove & re-add the camera subject.
|
|
observers.append(observe(\.appSettings.liveLinkSubjectName, options: [.old,.new], changeHandler: { [weak self] object, change in
|
|
if let sc = self?.streamingConnection {
|
|
sc.subjectName = self?.appSettings.liveLinkSubjectName
|
|
}
|
|
}))
|
|
|
|
// initial & value changes for the connection type instantiates a new StreamingConnection object
|
|
observers.append(observe(\.appSettings.connectionType, options: [.initial, .old,.new], changeHandler: { [weak self] object, change in
|
|
|
|
if let validSelf = self {
|
|
validSelf.streamingConnection?.shutdown()
|
|
validSelf.streamingConnection = nil
|
|
|
|
let connectionType = validSelf.appSettings.connectionType
|
|
if let connectionClass = Bundle.main.classNamed("VCAM.\(connectionType)StreamingConnection") as? StreamingConnection.Type {
|
|
validSelf.streamingConnection = connectionClass.init(subjectName: validSelf.appSettings.liveLinkSubjectName)
|
|
validSelf.streamingConnection?.delegate = validSelf
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad();
|
|
|
|
self.restartView.isHidden = true
|
|
|
|
if let infoDict = Bundle.main.infoDictionary {
|
|
self.versionLabel.text = String(format: "v%@ (%@)", infoDict["CFBundleShortVersionString"] as! String, infoDict["CFBundleVersion"] as! String)
|
|
} else {
|
|
self.versionLabel.text = "";
|
|
}
|
|
|
|
NetUtility.triggerLocalNetworkPrivacyAlert()
|
|
|
|
self.ipAddress.text = AppSettings.shared.lastConnectionAddress
|
|
self.ipAddress.inputAssistantItem.leadingBarButtonGroups.removeAll()
|
|
self.ipAddress.inputAssistantItem.trailingBarButtonGroups.removeAll()
|
|
textFieldChanged(self.ipAddress)
|
|
|
|
self.rebuildRecentAddressesBarButtons()
|
|
|
|
// Add gesture recognizer
|
|
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
|
self.tapGesture.cancelsTouchesInView = false
|
|
self.tapGesture.delegate = self
|
|
self.view.addGestureRecognizer(tapGesture)
|
|
|
|
// Attempt to get an attached game controller
|
|
self.gameController = GCController.controllers().first
|
|
if let gc = self.gameController {
|
|
if gc.isAttachedToDevice {
|
|
gc.playerIndex = .index1
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override func viewWillAppear(_ animated : Bool) {
|
|
self.connectingView.isHidden = true
|
|
self.headerView.start()
|
|
|
|
self.streamingConnection?.delegate = self
|
|
|
|
_liveLinkTimer = Timer.scheduledTimer(withTimeInterval: 1.0/10.0, repeats: true, block: { [weak self] timer in
|
|
self?.streamingConnection?.sendTransform(simd_float4x4(), atTime: Timecode.create().toTimeInterval())
|
|
})
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
self.headerView.start()
|
|
self.streamingConnection?.disconnect()
|
|
}
|
|
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
self.headerView.stop()
|
|
|
|
// Remove gesture recognizer
|
|
self.view.removeGestureRecognizer(self.tapGesture)
|
|
}
|
|
|
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
|
|
|
// hide the keyboard if it was being shown
|
|
self.view.endEditing(true)
|
|
|
|
if segue.identifier == "showVideoView" {
|
|
|
|
// connection was successful, we save the last address in our recents list
|
|
AppSettings.shared.addRecentConnectionAddress(AppSettings.shared.lastConnectionAddress)
|
|
self.rebuildRecentAddressesBarButtons()
|
|
|
|
if let vc = segue.destination as? VideoViewController {
|
|
|
|
// stop the timer locally which is sending LL identity xform
|
|
_liveLinkTimer?.invalidate()
|
|
_liveLinkTimer = nil
|
|
|
|
// This a little awkward but what happens here is the streamingConnection and gameController gets
|
|
// are passed (weakly) from StartViewController to VideoViewController as it needs them
|
|
// They are not nil'd in this view because this view still exists because it is the ancestor of the segue
|
|
vc.streamingConnection = self.streamingConnection
|
|
vc.streamingConnection?.delegate = vc
|
|
vc.gameController = self.gameController
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func keyboardWillShow(notification: NSNotification) {
|
|
|
|
guard let userInfo = notification.userInfo else {return}
|
|
|
|
// if the keyboard will overlap the connect button, then we move the view up so that nothing
|
|
// is obscured. In cases where there is a keyboard connected, the toolbar is shown only and
|
|
// nothing will move.
|
|
|
|
let keyboardFrame = self.view.convert((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue, from: self.view.window)
|
|
let connectButtonFrame = self.view.convert(connect.frame, from: connect.superview)
|
|
|
|
if keyboardFrame.minY < connectButtonFrame.maxY {
|
|
self.entryViewYConstraint.constant = -(keyboardFrame.size.height - self.headerView.frame.height) / 2.0
|
|
} else {
|
|
self.entryViewYConstraint.constant = 0
|
|
}
|
|
|
|
UIView.animate(withDuration: 0.2) { [weak self] in
|
|
self?.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
@objc func keyboardWillHide(notification: NSNotification) {
|
|
if self.entryViewYConstraint.constant != 0 {
|
|
self.entryViewYConstraint.constant = 0
|
|
|
|
UIView.animate(withDuration: 0.2) { [weak self] in
|
|
self?.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
|
|
func rebuildRecentAddressesBarButtons() {
|
|
|
|
var view : UIView?
|
|
|
|
if let addresses = AppSettings.shared.recentConnectionAddresses {
|
|
for address in addresses {
|
|
|
|
if view == nil {
|
|
view = UIView()
|
|
}
|
|
|
|
let item = UIButton(configuration: UIButton.Configuration.gray())
|
|
item.setTitle(address, for: .normal)
|
|
item.setTitleColor(UIColor.white, for: .normal)
|
|
item.addTarget(self, action: #selector(handleRecentAddressSelection), for: .touchUpInside)
|
|
|
|
view!.addSubview(item)
|
|
item.layoutToSuperview(.centerY)
|
|
|
|
if view!.subviews.count == 1 {
|
|
item.layoutToSuperview(.left)
|
|
} else {
|
|
item.layout(.left, to: .right, of: view!.subviews[view!.subviews.count - 2], offset: 20)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if let v = view {
|
|
v.subviews.last?.layoutToSuperview(.right)
|
|
let inputView = UIInputView(frame: CGRect(x: 0, y: 0, width: 100, height: 50), inputViewStyle: .keyboard)
|
|
inputView.addSubview(v)
|
|
v.layoutToSuperview(.top, offset: 4)
|
|
v.layoutToSuperview(.centerX, .bottom)
|
|
|
|
self.ipAddress.inputAccessoryView = inputView // inputAssistantItem.leadingBarButtonGroups.append(group)
|
|
}
|
|
|
|
}
|
|
|
|
@objc func handleRecentAddressSelection(_ sender : Any?) {
|
|
if let btn = sender as? UIButton {
|
|
if let addr = btn.title(for: .normal) {
|
|
self.ipAddress.text = addr
|
|
self.ipAddress.resignFirstResponder()
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func handleTap(_ recognizer: UITapGestureRecognizer) {
|
|
self.view.endEditing(true)
|
|
}
|
|
|
|
@IBAction func connect(_ sender : Any?) {
|
|
|
|
AppSettings.shared.lastConnectionAddress = self.ipAddress.text!
|
|
|
|
if self.ipAddressIsDemoMode {
|
|
|
|
self.performSegue(withIdentifier: "showVideoViewDemoMode", sender: self)
|
|
|
|
} else {
|
|
|
|
// show the connection view
|
|
self.connectingView.isHidden = false
|
|
self.connectingView.alpha = 0.0
|
|
UIView.animate(withDuration: 0.2) { [weak self] in
|
|
self?.connectingView.alpha = 1.0
|
|
}
|
|
|
|
showConnectingAlertView(mode: .connecting, { [weak self] in
|
|
self?.hideConnectingView() {}
|
|
})
|
|
|
|
do {
|
|
self.streamingConnection?.destination = self.ipAddress.text!.trimmed()
|
|
try self.streamingConnection?.connect()
|
|
|
|
} catch StreamingConnectionError.runtimeError(let errorMessage) {
|
|
|
|
showConnectionErrorAlert(errorMessage)
|
|
|
|
} catch {
|
|
|
|
showConnectionErrorAlert("\(Localized.messageCouldntConnect()) : \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func showConnectionErrorAlert(_ message : String) {
|
|
hideConnectingAlertView() {
|
|
|
|
let errorAlert = UIAlertController(title: Localized.titleError(), message: "\(Localized.messageCouldntConnect()) : \(message)", preferredStyle: .alert)
|
|
errorAlert.addAction(UIAlertAction(title: Localized.buttonOK(), style: .default, handler: { [weak self] _ in
|
|
self?.hideConnectingView { }
|
|
}))
|
|
self.present(errorAlert, animated: true)
|
|
}
|
|
}
|
|
|
|
func hideConnectingView( _ completion : @escaping () -> Void) {
|
|
UIView.animate(withDuration: 0.2, animations: { [weak self] in
|
|
self?.connectingView.alpha = 0.0
|
|
}, completion: { [weak self] b in
|
|
self?.connectingView.isHidden = true
|
|
})
|
|
hideConnectingAlertView(completion)
|
|
}
|
|
|
|
@IBAction func textFieldChanged(_ sender : Any?) {
|
|
self.connect.isEnabled = !self.ipAddress.text!.isEmpty
|
|
}
|
|
|
|
@objc func gameControllerDidConnectNotification(_ notification: NSNotification) {
|
|
|
|
self.gameController = notification.object as? GCController
|
|
self.gameController?.playerIndex = .index1
|
|
|
|
if let videoViewController = self.presentedViewController as? VideoViewController {
|
|
videoViewController.gameController = self.gameController
|
|
}
|
|
}
|
|
|
|
@objc func gameControllerDidDisconnectNotification(_ notification: NSNotification) {
|
|
if let gc = notification.object as? GCController {
|
|
Log.info("gameControllerDidDisconnectNotification \(gc.vendorName ?? "Unknown controller")")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension StartViewController : UIGestureRecognizerDelegate {
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
|
|
return touch.view != self.connect && touch.view != self.ipAddress
|
|
}
|
|
}
|
|
|
|
|
|
extension StartViewController : UITextFieldDelegate {
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
textField.resignFirstResponder()
|
|
|
|
connect(textField)
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
extension StartViewController : UIPickerViewDelegate, UIPickerViewDataSource {
|
|
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
|
// Number of columns
|
|
return 1;
|
|
}
|
|
|
|
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
|
// Number of rows
|
|
return pickerData.count
|
|
}
|
|
|
|
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
|
return pickerData[row]
|
|
}
|
|
|
|
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
|
// This method is triggered whenever the user makes a change to the picker selection.
|
|
// The parameter named row and component represents what was selected.
|
|
selectedStreamer = pickerData[row]
|
|
}
|
|
}
|