Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve responsiveness of camera #173

Merged
merged 3 commits into from
Jul 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
C3C0CC601BFE496A0052747C /* ReusableXibView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC161BFE496A0052747C /* ReusableXibView.swift */; };
C3C0CC611BFE496A0052747C /* Text.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3C0CC181BFE496A0052747C /* Text.xcassets */; };
C3C0CC621BFE496A0052747C /* TextChatInputItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC191BFE496A0052747C /* TextChatInputItem.swift */; };
C3C0CC631BFE496A0052747C /* KeyedOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC1A1BFE496A0052747C /* KeyedOperationQueue.swift */; };
C3C0CC641BFE496A0052747C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC1B1BFE496A0052747C /* Observable.swift */; };
C3C0CC661BFE496A0052747C /* CircleIconView.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C0CC1F1BFE496A0052747C /* CircleIconView.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3C0CC671BFE496A0052747C /* CircleIconView.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC201BFE496A0052747C /* CircleIconView.m */; };
Expand Down Expand Up @@ -151,7 +150,6 @@
C3C0CC161BFE496A0052747C /* ReusableXibView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableXibView.swift; sourceTree = "<group>"; };
C3C0CC181BFE496A0052747C /* Text.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Text.xcassets; sourceTree = "<group>"; };
C3C0CC191BFE496A0052747C /* TextChatInputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextChatInputItem.swift; sourceTree = "<group>"; };
C3C0CC1A1BFE496A0052747C /* KeyedOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyedOperationQueue.swift; sourceTree = "<group>"; };
C3C0CC1B1BFE496A0052747C /* Observable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
C3C0CC1F1BFE496A0052747C /* CircleIconView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleIconView.h; sourceTree = "<group>"; };
C3C0CC201BFE496A0052747C /* CircleIconView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleIconView.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -241,7 +239,6 @@
C3C0CC021BFE496A0052747C /* Info.plist */,
C38658B11BFE55620012F181 /* AnimationUtils.swift */,
C3A646FE1BFE8D40001BC98B /* Utils.swift */,
C3C0CC1A1BFE496A0052747C /* KeyedOperationQueue.swift */,
C3C0CC1B1BFE496A0052747C /* Observable.swift */,
C3C0CBD51BFE496A0052747C /* Chat Items */,
C3C0CC031BFE496A0052747C /* Input */,
Expand Down Expand Up @@ -601,7 +598,6 @@
C3C0CC351BFE496A0052747C /* PhotoMessagePresenter.swift in Sources */,
C3C0CC341BFE496A0052747C /* PhotoMessageModel.swift in Sources */,
C3C0CC5E1BFE496A0052747C /* PhotosInputView.swift in Sources */,
C3C0CC631BFE496A0052747C /* KeyedOperationQueue.swift in Sources */,
C3C0CC431BFE496A0052747C /* TextMessageCollectionViewCellDefaultStyle.swift in Sources */,
C3C0CC521BFE496A0052747C /* ChatInputBarPresenter.swift in Sources */,
C3C0CC401BFE496A0052747C /* TextBubbleView.swift in Sources */,
Expand Down
71 changes: 43 additions & 28 deletions ChattoAdditions/Source/Input/Photos/LiveCameraCaptureSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
import Foundation
import Photos

class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
protocol LiveCameraCaptureSessionProtocol {
var captureLayer: AVCaptureVideoPreviewLayer? { get }
var isInitialized: Bool { get }
var isCapturing: Bool { get }
func startCapturing(completion: () -> Void)
func stopCapturing(completion: () -> Void)
}

private enum OperationType: String {
case start
case stop
}
class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {

var isInitialized: Bool = false

Expand All @@ -40,6 +43,7 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {

deinit {
var layer = self.captureLayer
layer?.removeFromSuperlayer()
var session: AVCaptureSession? = self.isInitialized ? self.captureSession : nil
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// Analogously to AVCaptureSession creation, dealloc can take very long, so let's do it out of the main thread
Expand All @@ -51,33 +55,31 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
func startCapturing(completion: () -> Void) {
let operation = NSBlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
guard let strongSelf = self, strongOperation = operation else { return }
if !strongOperation.cancelled && !strongSelf.captureSession.running {
strongSelf.captureSession.startRunning()
dispatch_async(dispatch_get_main_queue(), completion)
}
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
sSelf.addInputDevicesIfNeeded()
sSelf.captureSession.startRunning()
dispatch_async(dispatch_get_main_queue(), completion)
}
self.queue.cancelOperation(forKey: OperationType.stop.rawValue)
self.queue.addOperation(operation, forKey: OperationType.start.rawValue)
self.queue.cancelAllOperations()
self.queue.addOperation(operation)
}

func stopCapturing(completion: () -> Void) {
let operation = NSBlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
guard let strongSelf = self, strongOperation = operation else { return }
if !strongOperation.cancelled && strongSelf.captureSession.running {
strongSelf.captureSession.stopRunning()
dispatch_async(dispatch_get_main_queue(), completion)
}
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
sSelf.captureSession.stopRunning()
sSelf.removeInputDevices()
dispatch_async(dispatch_get_main_queue(), completion)
}
self.queue.cancelOperation(forKey: OperationType.start.rawValue)
self.queue.addOperation(operation, forKey: OperationType.stop.rawValue)
self.queue.cancelAllOperations()
self.queue.addOperation(operation)
}

private (set) var captureLayer: AVCaptureVideoPreviewLayer?

private lazy var queue: KeyedOperationQueue = {
let queue = KeyedOperationQueue()
private lazy var queue: NSOperationQueue = {
let queue = NSOperationQueue()
queue.qualityOfService = .UserInitiated
queue.maxConcurrentOperationCount = 1
return queue
Expand All @@ -87,16 +89,29 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")

let session = AVCaptureSession()
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
do {
let input = try AVCaptureDeviceInput(device: device)
session.addInput(input)
} catch {

}
self.captureLayer = AVCaptureVideoPreviewLayer(session: session)
self.captureLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
self.isInitialized = true
return session
}()

private func addInputDevicesIfNeeded() {
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
if self.captureSession.inputs?.count == 0 {
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
do {
let input = try AVCaptureDeviceInput(device: device)
self.captureSession.addInput(input)
} catch {

}
}
}

private func removeInputDevices() {
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
self.captureSession.inputs?.forEach { (input) in
self.captureSession.removeInput(input as! AVCaptureInput)
}
}
}
21 changes: 5 additions & 16 deletions ChattoAdditions/Source/Input/Photos/LiveCameraCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,6 @@ import Foundation
import UIKit
import Chatto

protocol LiveCameraCaptureSessionProtocol {
var captureLayer: AVCaptureVideoPreviewLayer? { get }
var isInitialized: Bool { get }
var isCapturing: Bool { get }
func startCapturing(completion: () -> Void)
func stopCapturing(completion: () -> Void)
}

class LiveCameraCell: UICollectionViewCell {

private struct Constants {
Expand Down Expand Up @@ -71,22 +63,19 @@ class LiveCameraCell: UICollectionViewCell {
captureLayer.removeAnimationForKey(animationKey)
captureLayer.addAnimation(animation, forKey: animationKey)
}
self.setNeedsLayout()
}
}
}

typealias CellCallback = (cell: LiveCameraCell) -> Void

var onWillBeAddedToWindow: CellCallback?
override func willMoveToWindow(newWindow: UIWindow?) {
if newWindow != nil {
self.onWillBeAddedToWindow?(cell: self)
}
}

var onWasAddedToWindow: CellCallback?
var onWasRemovedFromWindow: CellCallback?
override func didMoveToWindow() {
if self.window == nil {
if let _ = self.window {
self.onWasAddedToWindow?(cell: self)
} else {
self.onWasRemovedFromWindow?(cell: self)
}
}
Expand Down
30 changes: 21 additions & 9 deletions ChattoAdditions/Source/Input/Photos/LiveCameraCellPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class LiveCameraCellPresenter {
func cellWillBeShown(cell: LiveCameraCell) {
self.cell = cell
self.configureCell()
self.startCapturing()
}

func cellWasHidden(cell: LiveCameraCell) {
Expand All @@ -51,23 +52,23 @@ final class LiveCameraCellPresenter {

cameraCell.updateWithAuthorizationStatus(self.cameraAuthorizationStatus)

self.startCapturing()

if self.captureSession.isCapturing {
cameraCell.captureLayer = self.captureSession.captureLayer
} else {
cameraCell.captureLayer = nil
}

cameraCell.onWillBeAddedToWindow = { [weak self] (cell) in
if self?.cell === cell {
self?.configureCell()
cameraCell.onWasAddedToWindow = { [weak self] (cell) in
guard let sSelf = self where sSelf.cell === cell else { return }
if !sSelf.cameraPickerIsVisible {
sSelf.startCapturing()
}
}

cameraCell.onWasRemovedFromWindow = { [weak self] (cell) in
if self?.cell === cell {
self?.stopCapturing()
guard let sSelf = self where sSelf.cell === cell else { return }
if !sSelf.cameraPickerIsVisible {
sSelf.stopCapturing()
}
}
}
Expand Down Expand Up @@ -102,11 +103,22 @@ final class LiveCameraCellPresenter {
}
}

var cameraPickerIsVisible = false
func cameraPickerWillAppear() {
self.cameraPickerIsVisible = true
self.stopCapturing()
}

func cameraPickerDidDisappear() {
self.cameraPickerIsVisible = false
self.startCapturing()
}

func startCapturing() {
guard self.isCaptureAvailable else { return }
guard self.isCaptureAvailable, let _ = self.cell else { return }

self.captureSession.startCapturing() { [weak self] in
self?.configureCell()
self?.cell?.captureLayer = self?.captureSession.captureLayer
}
}

Expand Down
19 changes: 12 additions & 7 deletions ChattoAdditions/Source/Input/Photos/PhotosInputCameraPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,33 @@ class PhotosInputCameraPicker: NSObject {
self.presentingController = presentingController
}

private var requestImageCompletion: ((UIImage?) -> Void)?
func requestImage(completion: (UIImage?) -> Void) {
private var completionBlocks: (onImageTaken: ((UIImage?) -> Void)?, onCameraPickerDismissed: (() -> Void)?)?

func presentCameraPicker(onImageTaken onImageTaken: (UIImage?) -> Void, onCameraPickerDismissed: () -> Void) {
guard UIImagePickerController.isSourceTypeAvailable(.Camera) else {
completion(nil)
onImageTaken(nil)
onCameraPickerDismissed()
return
}

guard let presentingController = self.presentingController else {
completion(nil)
onImageTaken(nil)
onCameraPickerDismissed()

return
}

self.requestImageCompletion = completion
self.completionBlocks = (onImageTaken: onImageTaken, onCameraPickerDismissed: onCameraPickerDismissed)
let controller = UIImagePickerController()
controller.delegate = self
controller.sourceType = .Camera
presentingController.presentViewController(controller, animated: true, completion:nil)
}

private func finishPickingImage(image: UIImage?, fromPicker picker: UIImagePickerController) {
picker.dismissViewControllerAnimated(true, completion: nil)
self.requestImageCompletion?(image)
let (onImageTaken, onCameraPickerDismissed) = self.completionBlocks ?? (nil, nil)
picker.dismissViewControllerAnimated(true, completion: onCameraPickerDismissed)
onImageTaken?(image)
}
}

Expand Down
11 changes: 8 additions & 3 deletions ChattoAdditions/Source/Input/Photos/PhotosInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,16 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
if self.cameraAuthorizationStatus != .Authorized {
self.delegate?.inputViewDidRequestCameraPermission(self)
} else {
self.cameraPicker.requestImage { image in
self.liveCameraPresenter.cameraPickerWillAppear()
self.cameraPicker.presentCameraPicker(onImageTaken: { [weak self] (image) in
guard let sSelf = self else { return }

if let image = image {
self.delegate?.inputView(self, didSelectImage: image)
sSelf.delegate?.inputView(sSelf, didSelectImage: image)
}
}
}, onCameraPickerDismissed: { [weak self] in
self?.liveCameraPresenter.cameraPickerDidDisappear()
})
}
} else {
if self.photoLibraryAuthorizationStatus != .Authorized {
Expand Down
46 changes: 0 additions & 46 deletions ChattoAdditions/Source/KeyedOperationQueue.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class LiveCameraCellPresenterTests: XCTestCase {
self.presenter.captureSession = mockCaptureSession

self.presenter.cameraAuthorizationStatus = .Authorized
self.presenter.cellWillBeShown(self.cell)

self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil)

Expand Down Expand Up @@ -169,7 +171,9 @@ class LiveCameraCellPresenterTests: XCTestCase {
self.presenter.cellWillBeShown(self.cell)
self.cell.didMoveToWindow()

self.cell.willMoveToWindow(UIWindow())
let window = UIWindow()
window.addSubview(self.cell)

XCTAssertTrue(mockCaptureSession.isCapturing)
}

Expand Down
Loading