Files
iOS/Sources/Extensions/NotificationContent/CameraStreamMJPEGViewController.swift
Zac West 4d9a530637 Reorganize files in repo, pull out build settings from pbxproj (#1140)
This is somewhat in prep of being able to make the project file generated, but also just organizes things into more concrete directory structures.

This pulls out _all_ of the build settings from the root level, and most from the target level, into xcconfigs.

The new directory structure looks like:

- Sources
  - App
    - (everything from HomeAssistant/)
  - WatchApp
  - Shared
  - MacBridge
  - Extensions
    - Intents
    - NotificationContent
    - NotificationService
    - Share
    - Today
    - Watch
    - Widgets
- Tests
  - App
  - UI
  - Shared

Somewhat intentionally, the file structure under these is not yet standardized/organized.

The project targets are now:

- App
- WatchApp
- Shared-iOS
- Shared-watchOS
- MacBridge
- Tests-App
- Tests-UI
- Tests-Shared
- Extension-Intents
- Extension-NotificationContent
- Extension-NotificationService
- Extension-Share
- Extension-Today
- Extension-Widget
- WatchExtension-Watch

This does not yet clean up resources vs. sources, nor does it handle some of the "it's in Sources/App but it's part of Shared" crossover directory issues.
2020-10-03 00:15:04 -07:00

134 lines
4.2 KiB
Swift

import Foundation
import Shared
import PromiseKit
import Alamofire
import UIKit
class CameraStreamMJPEGViewController: UIViewController, CameraStreamHandler {
let api: HomeAssistantAPI
let connectionInfo: ConnectionInfo
let response: StreamCameraResponse
let imageView: UIImageView
let streamer: MJPEGStreamer
let promise: Promise<Void>
var didUpdateState: (CameraStreamHandlerState) -> Void = { _ in }
private let seal: Resolver<Void>
enum MJPEGError: LocalizedError {
case noPath
case noStreamer
case networkError(path: String, error: Error?)
var errorDescription: String? {
switch self {
case .noPath:
return L10n.Extensions.NotificationContent.Error.Request.authFailed
case .noStreamer:
return L10n.Extensions.NotificationContent.Error.Request.authFailed
case .networkError(let path, let error):
if let error = error as? AFError, let responseCode = error.responseCode {
switch responseCode {
case 401:
return L10n.Extensions.NotificationContent.Error.Request.authFailed
case 404:
return L10n.Extensions.NotificationContent.Error.Request.entityNotFound(path)
default:
return L10n.Extensions.NotificationContent.Error.Request.other(responseCode)
}
}
return error?.localizedDescription ?? L10n.Extensions.NotificationContent.Error.Request.other(-1)
}
}
}
required init(api: HomeAssistantAPI, response: StreamCameraResponse) throws {
guard response.mjpegPath != nil else {
throw MJPEGError.noPath
}
guard let streamer = api.VideoStreamer() else {
throw MJPEGError.noStreamer
}
self.api = api
self.connectionInfo = try api.connectionInfo()
self.response = response
self.streamer = streamer
self.imageView = UIImageView()
(self.promise, self.seal) = Promise<Void>.pending()
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.topAnchor),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
setupStreamer()
}
func pause() {
streamer.cancel()
didUpdateState(.paused)
}
func play() {
if !streamer.isActive {
setupStreamer()
}
}
private var aspectRatioConstraint: NSLayoutConstraint? {
willSet {
aspectRatioConstraint?.isActive = false
}
didSet {
aspectRatioConstraint?.isActive = true
}
}
private var lastSize: CGSize? {
didSet {
if oldValue != lastSize, let size = lastSize {
aspectRatioConstraint = Self.aspectRatioConstraint(on: imageView, size: size)
}
}
}
private func setupStreamer() {
guard let path = response.mjpegPath else {
fatalError("we checked for a non-nil path on init, this should not be possible")
}
let url = connectionInfo.activeURL.appendingPathComponent(path)
// assume 16:9
lastSize = CGSize(width: 16, height: 9)
streamer.streamImages(fromURL: url) { [weak self, imageView] image, error in
guard let image = image else {
self?.seal.reject(MJPEGError.networkError(path: path, error: error))
return
}
imageView.image = image
self?.seal.fulfill(())
self?.lastSize = image.size
self?.didUpdateState(.playing)
}
}
}