Merge branch 'nt/live-camera'
This commit is contained in:
commit
7aadee0062
@ -5,7 +5,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol AttachmentFormatPickerDelegate: class {
|
||||
func didTapCamera()
|
||||
func didTapCamera(withPhotoCapture: PhotoCapture?)
|
||||
func didTapGif()
|
||||
func didTapFile()
|
||||
func didTapContact()
|
||||
@ -20,6 +20,7 @@ class AttachmentFormatPickerView: UICollectionView {
|
||||
}
|
||||
|
||||
private let collectionViewFlowLayout = UICollectionViewFlowLayout()
|
||||
private var photoCapture: PhotoCapture?
|
||||
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
@ -47,6 +48,30 @@ class AttachmentFormatPickerView: UICollectionView {
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopCameraPreview()
|
||||
}
|
||||
|
||||
func startCameraPreview() {
|
||||
guard photoCapture == nil else { return }
|
||||
|
||||
let photoCapture = PhotoCapture()
|
||||
self.photoCapture = photoCapture
|
||||
|
||||
photoCapture.startVideoCapture().done { [weak self] in
|
||||
self?.reloadData()
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
func stopCameraPreview() {
|
||||
photoCapture?.stopCapture().retainUntilComplete()
|
||||
photoCapture = nil
|
||||
}
|
||||
|
||||
func orientationDidChange() {
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
private func updateLayout() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
@ -77,7 +102,11 @@ extension AttachmentFormatPickerView: UICollectionViewDelegate {
|
||||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
switch AttachmentType.allCases[indexPath.row] {
|
||||
case .camera:
|
||||
attachmentFormatPickerDelegate?.didTapCamera()
|
||||
// Since we're trying to pass on our prepared capture session to
|
||||
// the camera view, nil it out so we don't try and stop it here.
|
||||
let photoCapture = self.photoCapture
|
||||
self.photoCapture = nil
|
||||
attachmentFormatPickerDelegate?.didTapCamera(withPhotoCapture: photoCapture)
|
||||
case .contact:
|
||||
attachmentFormatPickerDelegate?.didTapContact()
|
||||
case .file:
|
||||
@ -108,7 +137,7 @@ extension AttachmentFormatPickerView: UICollectionViewDataSource {
|
||||
}
|
||||
|
||||
let type = AttachmentType.allCases[indexPath.item]
|
||||
cell.configure(type: type)
|
||||
cell.configure(type: type, cameraPreview: photoCapture?.previewView)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
@ -121,6 +150,11 @@ class AttachmentFormatCell: UICollectionViewCell {
|
||||
let label = UILabel()
|
||||
|
||||
var attachmentType: AttachmentType?
|
||||
weak var cameraPreview: CapturePreviewView?
|
||||
|
||||
private var hasCameraAccess: Bool {
|
||||
return AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
@ -164,8 +198,9 @@ class AttachmentFormatCell: UICollectionViewCell {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public func configure(type: AttachmentType) {
|
||||
public func configure(type: AttachmentType, cameraPreview: CapturePreviewView?) {
|
||||
self.attachmentType = type
|
||||
self.cameraPreview = cameraPreview
|
||||
|
||||
let imageName: String
|
||||
let text: String
|
||||
@ -196,6 +231,25 @@ class AttachmentFormatCell: UICollectionViewCell {
|
||||
}
|
||||
|
||||
label.text = text
|
||||
|
||||
showLiveCameraIfAvailable()
|
||||
}
|
||||
|
||||
func showLiveCameraIfAvailable() {
|
||||
guard case .camera? = attachmentType, hasCameraAccess else { return }
|
||||
|
||||
// If we have access to the camera, we'll want to show it eventually.
|
||||
// Style this in prepration for that.
|
||||
|
||||
imageView.setTemplateImageName("camera-outline-32", tintColor: .white)
|
||||
label.textColor = .white
|
||||
backgroundColor = UIColor.black.withAlphaComponent(0.4)
|
||||
|
||||
guard let cameraPreview = cameraPreview else { return }
|
||||
|
||||
contentView.insertSubview(cameraPreview, belowSubview: imageView)
|
||||
cameraPreview.autoPinEdgesToSuperviewEdges()
|
||||
cameraPreview.contentMode = .scaleAspectFill
|
||||
}
|
||||
|
||||
override public func prepareForReuse() {
|
||||
@ -203,5 +257,12 @@ class AttachmentFormatCell: UICollectionViewCell {
|
||||
|
||||
attachmentType = nil
|
||||
imageView.image = nil
|
||||
|
||||
label.textColor = Theme.attachmentKeyboardItemImageColor
|
||||
backgroundColor = Theme.attachmentKeyboardItemBackgroundColor
|
||||
|
||||
if let cameraPreview = cameraPreview, cameraPreview.superview == contentView {
|
||||
cameraPreview.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import PromiseKit
|
||||
protocol AttachmentKeyboardDelegate {
|
||||
func didSelectRecentPhoto(_ attachment: SignalAttachment)
|
||||
func didTapGalleryButton()
|
||||
func didTapCamera()
|
||||
func didTapCamera(withPhotoCapture: PhotoCapture?)
|
||||
func didTapGif()
|
||||
func didTapFile()
|
||||
func didTapContact()
|
||||
@ -147,6 +147,19 @@ class AttachmentKeyboard: CustomKeyboard {
|
||||
checkPermissionsAndLayout()
|
||||
}
|
||||
|
||||
override func wasDismissed() {
|
||||
super.wasDismissed()
|
||||
|
||||
attachmentFormatPickerView.stopCameraPreview()
|
||||
}
|
||||
|
||||
override func orientationDidChange() {
|
||||
super.orientationDidChange()
|
||||
|
||||
recentPhotosCollectionView.orientationDidChange()
|
||||
attachmentFormatPickerView.orientationDidChange()
|
||||
}
|
||||
|
||||
func checkPermissionsAndLayout() {
|
||||
switch mediaLibraryAuthorizationStatus {
|
||||
case .authorized:
|
||||
@ -154,9 +167,9 @@ class AttachmentKeyboard: CustomKeyboard {
|
||||
case .denied, .restricted:
|
||||
showRecentPhotosError()
|
||||
case .notDetermined:
|
||||
PHPhotoLibrary.requestAuthorization { _ in
|
||||
PHPhotoLibrary.requestAuthorization { status in
|
||||
DispatchQueue.main.async {
|
||||
self.recentPhotosCollectionView.permissionChanged()
|
||||
if status == .authorized { self.recentPhotosCollectionView.permissionChanged() }
|
||||
self.checkPermissionsAndLayout()
|
||||
}
|
||||
}
|
||||
@ -164,6 +177,21 @@ class AttachmentKeyboard: CustomKeyboard {
|
||||
showRecentPhotosError()
|
||||
break
|
||||
}
|
||||
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
attachmentFormatPickerView.startCameraPreview()
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||
if granted {
|
||||
DispatchQueue.main.async { self.attachmentFormatPickerView.startCameraPreview() }
|
||||
}
|
||||
}
|
||||
case .denied, .restricted:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,8 +206,8 @@ extension AttachmentKeyboard: RecentPhotosDelegate {
|
||||
}
|
||||
|
||||
extension AttachmentKeyboard: AttachmentFormatPickerDelegate {
|
||||
func didTapCamera() {
|
||||
delegate?.didTapCamera()
|
||||
func didTapCamera(withPhotoCapture photoCapture: PhotoCapture?) {
|
||||
delegate?.didTapCamera(withPhotoCapture: photoCapture)
|
||||
}
|
||||
|
||||
func didTapGif() {
|
||||
|
||||
@ -88,6 +88,10 @@ class RecentPhotosCollectionView: UICollectionView {
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
func orientationDidChange() {
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
private func updateLayout() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@class ConversationStyle;
|
||||
@class OWSLinkPreviewDraft;
|
||||
@class OWSQuotedReplyModel;
|
||||
@class PhotoCapture;
|
||||
@class SignalAttachment;
|
||||
@class StickerInfo;
|
||||
|
||||
@ -34,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)cameraButtonPressed;
|
||||
|
||||
- (void)cameraButtonPressedWithPhotoCapture:(nullable PhotoCapture *)photoCapture;
|
||||
|
||||
- (void)galleryButtonPressed;
|
||||
|
||||
- (void)gifButtonPressed;
|
||||
|
||||
@ -504,11 +504,6 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
{
|
||||
if (!self.desiredFirstResponder.isFirstResponder) {
|
||||
[self.desiredFirstResponder becomeFirstResponder];
|
||||
|
||||
if ([self.desiredFirstResponder isKindOfClass:[OWSCustomKeyboard class]]) {
|
||||
OWSCustomKeyboard *customKeyboard = (OWSCustomKeyboard *)self.desiredFirstResponder;
|
||||
[customKeyboard wasPresented];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1532,9 +1527,9 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
[self.inputToolbarDelegate galleryButtonPressed];
|
||||
}
|
||||
|
||||
- (void)didTapCamera
|
||||
- (void)didTapCameraWithPhotoCapture:(nullable PhotoCapture *)photoCapture
|
||||
{
|
||||
[self.inputToolbarDelegate cameraButtonPressed];
|
||||
[self.inputToolbarDelegate cameraButtonPressedWithPhotoCapture:photoCapture];
|
||||
}
|
||||
|
||||
- (void)didTapGif
|
||||
|
||||
@ -2867,7 +2867,7 @@ typedef enum : NSUInteger {
|
||||
|
||||
#pragma mark - Media Libary
|
||||
|
||||
- (void)takePictureOrVideo
|
||||
- (void)takePictureOrVideoWithPhotoCapture:(nullable PhotoCapture *)photoCapture
|
||||
{
|
||||
[BenchManager startEventWithTitle:@"Show-Camera" eventId:@"Show-Camera"];
|
||||
[self ows_askForCameraPermissions:^(BOOL cameraGranted) {
|
||||
@ -2882,7 +2882,8 @@ typedef enum : NSUInteger {
|
||||
// be silent.
|
||||
}
|
||||
|
||||
SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingCameraFirst];
|
||||
SendMediaNavigationController *pickerModal =
|
||||
[SendMediaNavigationController showingCameraFirstWithPhotoCapture:photoCapture];
|
||||
pickerModal.sendMediaNavDelegate = self;
|
||||
|
||||
[self dismissKeyBoard];
|
||||
@ -3215,7 +3216,14 @@ typedef enum : NSUInteger {
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self takePictureOrVideo];
|
||||
[self takePictureOrVideoWithPhotoCapture:nil];
|
||||
}
|
||||
|
||||
- (void)cameraButtonPressedWithPhotoCapture:(nullable PhotoCapture *)photoCapture
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self takePictureOrVideoWithPhotoCapture:photoCapture];
|
||||
}
|
||||
|
||||
- (void)galleryButtonPressed
|
||||
|
||||
@ -30,6 +30,7 @@ protocol PhotoCaptureDelegate: AnyObject {
|
||||
func endCaptureButtonAnimation(_ duration: TimeInterval)
|
||||
}
|
||||
|
||||
@objc
|
||||
class PhotoCapture: NSObject {
|
||||
|
||||
weak var delegate: PhotoCaptureDelegate?
|
||||
@ -38,6 +39,9 @@ class PhotoCapture: NSObject {
|
||||
}
|
||||
let session: AVCaptureSession
|
||||
|
||||
// There can only ever be one `CapturePreviewView` per AVCaptureSession
|
||||
lazy private(set) var previewView = CapturePreviewView(session: session)
|
||||
|
||||
let sessionQueue = DispatchQueue(label: "PhotoCapture.sessionQueue")
|
||||
|
||||
private var currentCaptureInput: AVCaptureDeviceInput?
|
||||
@ -101,6 +105,9 @@ class PhotoCapture: NSObject {
|
||||
}
|
||||
|
||||
func startVideoCapture() -> Promise<Void> {
|
||||
// If the session is already running, no need to do anything.
|
||||
guard !self.session.isRunning else { return Promise.value(()) }
|
||||
|
||||
return sessionQueue.async(.promise) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
@ -410,11 +417,11 @@ extension PhotoCapture: VolumeButtonObserver {
|
||||
func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleLongPressBegin()
|
||||
}
|
||||
|
||||
|
||||
func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleLongPressComplete()
|
||||
}
|
||||
|
||||
|
||||
func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleLongPressCancel()
|
||||
}
|
||||
|
||||
@ -36,15 +36,13 @@ class PhotoCaptureViewController: OWSViewController {
|
||||
|
||||
weak var delegate: PhotoCaptureViewControllerDelegate?
|
||||
|
||||
private var photoCapture: PhotoCapture!
|
||||
@objc public lazy var photoCapture = PhotoCapture()
|
||||
|
||||
deinit {
|
||||
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
||||
if let photoCapture = photoCapture {
|
||||
photoCapture.stopCapture().done {
|
||||
Logger.debug("stopCapture completed")
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
photoCapture.stopCapture().done {
|
||||
Logger.debug("stopCapture completed")
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
// MARK: - Overrides
|
||||
@ -136,7 +134,9 @@ class PhotoCaptureViewController: OWSViewController {
|
||||
// MARK: - Views
|
||||
|
||||
let captureButton = CaptureButton()
|
||||
var previewView: CapturePreviewView!
|
||||
var previewView: CapturePreviewView {
|
||||
return photoCapture.previewView
|
||||
}
|
||||
|
||||
class PhotoControl {
|
||||
let button: OWSButton
|
||||
@ -315,21 +315,26 @@ class PhotoCaptureViewController: OWSViewController {
|
||||
|
||||
var hasCaptureStarted = false
|
||||
private func setupPhotoCapture() {
|
||||
photoCapture = PhotoCapture()
|
||||
photoCapture.delegate = self
|
||||
captureButton.delegate = photoCapture
|
||||
previewView = CapturePreviewView(session: photoCapture.session)
|
||||
previewView.contentMode = .scaleAspectFit
|
||||
|
||||
photoCapture.startVideoCapture().done { [weak self] in
|
||||
let captureReady = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.hasCaptureStarted = true
|
||||
self.showCaptureUI()
|
||||
BenchEventComplete(eventId: "Show-Camera")
|
||||
}.catch { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
}
|
||||
|
||||
self.showFailureUI(error: error)
|
||||
}.retainUntilComplete()
|
||||
// If the session is already running, we're good to go.
|
||||
guard !photoCapture.session.isRunning else { return captureReady() }
|
||||
|
||||
photoCapture.startVideoCapture()
|
||||
.done(captureReady)
|
||||
.catch { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
self.showFailureUI(error: error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
private func showCaptureUI() {
|
||||
@ -646,10 +651,38 @@ class CapturePreviewView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
override var contentMode: UIView.ContentMode {
|
||||
set {
|
||||
switch newValue {
|
||||
case .scaleAspectFill:
|
||||
previewLayer.videoGravity = .resizeAspectFill
|
||||
case .scaleAspectFit:
|
||||
previewLayer.videoGravity = .resizeAspect
|
||||
case .scaleToFill:
|
||||
previewLayer.videoGravity = .resize
|
||||
default:
|
||||
owsFailDebug("Unexpected contentMode")
|
||||
}
|
||||
}
|
||||
get {
|
||||
switch previewLayer.videoGravity {
|
||||
case .resizeAspectFill:
|
||||
return .scaleAspectFill
|
||||
case .resizeAspect:
|
||||
return .scaleAspectFit
|
||||
case .resize:
|
||||
return .scaleToFill
|
||||
default:
|
||||
owsFailDebug("Unexpected contentMode")
|
||||
return .scaleToFill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(session: AVCaptureSession) {
|
||||
previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
super.init(frame: .zero)
|
||||
self.contentMode = .scaleAspectFill
|
||||
self.contentMode = .scaleAspectFit
|
||||
previewLayer.frame = bounds
|
||||
layer.addSublayer(previewLayer)
|
||||
}
|
||||
|
||||
@ -106,8 +106,11 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
public weak var sendMediaNavDelegate: SendMediaNavDelegate?
|
||||
|
||||
@objc
|
||||
public class func showingCameraFirst() -> SendMediaNavigationController {
|
||||
public class func showingCameraFirst(photoCapture: PhotoCapture?) -> SendMediaNavigationController {
|
||||
let navController = SendMediaNavigationController()
|
||||
if let photoCapture = photoCapture {
|
||||
navController.captureViewController.photoCapture = photoCapture
|
||||
}
|
||||
navController.setViewControllers([navController.captureViewController], animated: false)
|
||||
return navController
|
||||
}
|
||||
|
||||
@ -31,7 +31,8 @@ open class CustomKeyboard: UIInputView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc open func wasPresented() {}
|
||||
open func wasPresented() {}
|
||||
open func wasDismissed() {}
|
||||
|
||||
@objc public func registerWithView(_ view: UIView) {
|
||||
view.addSubview(responder)
|
||||
@ -48,6 +49,14 @@ open class CustomKeyboard: UIInputView {
|
||||
return responder.resignFirstResponder()
|
||||
}
|
||||
|
||||
open override func didMoveToSuperview() {
|
||||
if superview == nil {
|
||||
wasDismissed()
|
||||
} else {
|
||||
wasPresented()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Height Management
|
||||
|
||||
private lazy var heightConstraint = autoSetDimension(.height, toSize: 0)
|
||||
@ -107,7 +116,7 @@ open class CustomKeyboard: UIInputView {
|
||||
}
|
||||
|
||||
private class CustomKeyboardResponder: UIView {
|
||||
@objc public let customKeyboard: CustomKeyboard
|
||||
@objc public weak var customKeyboard: CustomKeyboard?
|
||||
|
||||
init(customKeyboard: CustomKeyboard) {
|
||||
self.customKeyboard = customKeyboard
|
||||
|
||||
Loading…
Reference in New Issue
Block a user