Skip to content

Commit

Permalink
BBMetalCamera can take photo
Browse files Browse the repository at this point in the history
  • Loading branch information
Silence-GitHub committed May 14, 2019
1 parent cbbd517 commit b19fd90
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 0 deletions.
128 changes: 128 additions & 0 deletions BBMetalImage/BBMetalImage/BBMetalCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@

import AVFoundation

/// Camera photo delegate defines handling taking photo result behaviors
public protocol BBMetalCameraPhotoDelegate: AnyObject {
/// Called when camera did take a photo and get Metal texture
///
/// - Parameters:
/// - camera: camera to use
/// - texture: Metal texture of the photo
func camera(_ camera: BBMetalCamera, didOutput texture: MTLTexture)

/// Called when camera fail taking a photo
///
/// - Parameters:
/// - camera: camera to use
/// - error: error for taking the photo
func camera(_ camera: BBMetalCamera, didFail error: Error)
}

/// Camera capturing image and providing Metal texture
public class BBMetalCamera: NSObject {
/// Image consumers
Expand Down Expand Up @@ -104,12 +121,54 @@ public class BBMetalCamera: NSObject {
}
private var _audioConsumer: BBMetalAudioConsumer?

private var photoOutput: AVCapturePhotoOutput!

/// Whether can take photo or not.
/// Set this property to true before calling `takePhoto(with:)` method.
public var canTakePhoto: Bool {
get {
lock.wait()
let c = _canTakePhoto
lock.signal()
return c
}
set {
lock.wait()
_canTakePhoto = newValue
if newValue {
if !addPhotoOutput() { _canTakePhoto = false }
} else {
removePhotoOutput()
}
lock.signal()
}
}
private var _canTakePhoto: Bool

/// Camera photo delegate handling taking photo result.
/// To take photo, this property should not be nil.
public weak var photoDelegate: BBMetalCameraPhotoDelegate? {
get {
lock.wait()
let p = _photoDelegate
lock.signal()
return p
}
set {
lock.wait()
_photoDelegate = newValue
lock.signal()
}
}
private weak var _photoDelegate: BBMetalCameraPhotoDelegate?

#if !targetEnvironment(simulator)
private var textureCache: CVMetalTextureCache!
#endif

public init?(sessionPreset: AVCaptureSession.Preset = .high, position: AVCaptureDevice.Position = .back) {
_consumers = []
_canTakePhoto = false
_benchmark = false
capturedFrameCount = 0
totalCaptureFrameTime = 0
Expand Down Expand Up @@ -213,6 +272,44 @@ public class BBMetalCamera: NSObject {
}
}

@discardableResult
private func addPhotoOutput() -> Bool {
if photoOutput != nil { return true }

session.beginConfiguration()
defer { session.commitConfiguration() }

let output = AVCapturePhotoOutput()
if !session.canAddOutput(output) {
print("Can not add photo output")
return false
}
session.addOutput(output)
photoOutput = output

return true
}

private func removePhotoOutput() {
session.beginConfiguration()
if let output = photoOutput { session.removeOutput(output) }
session.commitConfiguration()
}

/// Takes a photo.
/// Before calling this method, set `canTakePhoto` property to true and `photoDelegate` property to nonnull.
///
/// - Parameter settings: a specification of the features and settings to use for a single photo capture request
public func takePhoto(with settings: AVCapturePhotoSettings? = nil) {
lock.wait()
if let output = photoOutput,
_photoDelegate != nil {
let currentSettings = settings ?? AVCapturePhotoSettings(format: [kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA])
output.capturePhoto(with: currentSettings, delegate: self)
}
lock.signal()
}

/// Switches camera position (back to front, or front to back)
///
/// - Returns: true if succeed, or false if fail
Expand Down Expand Up @@ -369,3 +466,34 @@ extension BBMetalCamera: AVCaptureVideoDataOutputSampleBufferDelegate, AVCapture
return nil
}
}

extension BBMetalCamera: AVCapturePhotoCaptureDelegate {
public func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?,
previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?,
resolvedSettings: AVCaptureResolvedPhotoSettings,
bracketSettings: AVCaptureBracketedStillImageSettings?,
error: Error?) {

guard let delegate = photoDelegate else { return }

if let error = error { delegate.camera(self, didFail: error) }

if let sampleBuffer = photoSampleBuffer,
let texture = texture(with: sampleBuffer),
let rotatedTexture = rotatedTexture(with: texture, angle: 90) {
// Setting `videoOrientation` of `AVCaptureConnection` dose not work. So rotate texture here.
delegate.camera(self, didOutput: rotatedTexture)
} else {
delegate.camera(self, didFail: NSError(domain: "BBMetalCamera.Photo", code: 0, userInfo: [NSLocalizedDescriptionKey : "Can not get Metal texture"]))
}
}

private func rotatedTexture(with inTexture: MTLTexture, angle: Float) -> MTLTexture? {
let source = BBMetalStaticImageSource(texture: inTexture)
let filter = BBMetalRotateFilter(angle: angle, fitSize: true)
source.add(consumer: filter).runSynchronously = true
source.transmitTexture()
return filter.outputTexture
}
}
4 changes: 4 additions & 0 deletions BBMetalImageDemo/BBMetalImageDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
4A00949F225C53C50089941C /* CameraFilterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A00949E225C53C50089941C /* CameraFilterVC.swift */; };
4A084D8D225D9EE30019FB57 /* CameraFilterMenuVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A084D8C225D9EE30019FB57 /* CameraFilterMenuVC.swift */; };
4A3CAE3F228A060900252B70 /* CameraPhotoFilterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3CAE3E228A060900252B70 /* CameraPhotoFilterVC.swift */; };
4A4CADC2225317C20058BEB6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CADC1225317C20058BEB6 /* AppDelegate.swift */; };
4A4CADC4225317C20058BEB6 /* MainMenuVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CADC3225317C20058BEB6 /* MainMenuVC.swift */; };
4A4CADC7225317C20058BEB6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CADC5225317C20058BEB6 /* Main.storyboard */; };
Expand Down Expand Up @@ -46,6 +47,7 @@
/* Begin PBXFileReference section */
4A00949E225C53C50089941C /* CameraFilterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraFilterVC.swift; sourceTree = "<group>"; };
4A084D8C225D9EE30019FB57 /* CameraFilterMenuVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraFilterMenuVC.swift; sourceTree = "<group>"; };
4A3CAE3E228A060900252B70 /* CameraPhotoFilterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPhotoFilterVC.swift; sourceTree = "<group>"; };
4A4CADBE225317C20058BEB6 /* BBMetalImageDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BBMetalImageDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
4A4CADC1225317C20058BEB6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
4A4CADC3225317C20058BEB6 /* MainMenuVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuVC.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -106,6 +108,7 @@
4AFC464E2253CDDC009D37A1 /* StaticImageFilterVC.swift */,
4A084D8C225D9EE30019FB57 /* CameraFilterMenuVC.swift */,
4A00949E225C53C50089941C /* CameraFilterVC.swift */,
4A3CAE3E228A060900252B70 /* CameraPhotoFilterVC.swift */,
4A665CFC22721CAA00DFDF4C /* VideoFilterVC.swift */,
4A633D672281F33C00138B2A /* VideoFilterVC2.swift */,
4A9BCB65227A03E000E02C6F /* VideoPlayerVC.swift */,
Expand Down Expand Up @@ -208,6 +211,7 @@
4AFC464F2253CDDC009D37A1 /* StaticImageFilterVC.swift in Sources */,
4A4CADC2225317C20058BEB6 /* AppDelegate.swift in Sources */,
4A00949F225C53C50089941C /* CameraFilterVC.swift in Sources */,
4A3CAE3F228A060900252B70 /* CameraPhotoFilterVC.swift in Sources */,
4A9BCB66227A03E000E02C6F /* VideoPlayerVC.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
75 changes: 75 additions & 0 deletions BBMetalImageDemo/BBMetalImageDemo/CameraPhotoFilterVC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// CameraPhotoFilterVC.swift
// BBMetalImageDemo
//
// Created by Kaibo Lu on 5/13/19.
// Copyright © 2019 Kaibo Lu. All rights reserved.
//

import UIKit
import AVFoundation
import BBMetalImage

class CameraPhotoFilterVC: UIViewController {
private var camera: BBMetalCamera!
private var metalView: BBMetalView!

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .gray

let x: CGFloat = 10
let width: CGFloat = view.bounds.width - 20
metalView = BBMetalView(frame: CGRect(x: x, y: 100, width: width, height: view.bounds.height - 200),
device: BBMetalDevice.sharedDevice)
view.addSubview(metalView)

let photoButton = UIButton(frame: CGRect(x: x, y: metalView.frame.maxY + 10, width: width, height: 30))
photoButton.backgroundColor = .blue
photoButton.setTitle("Take photo", for: .normal)
photoButton.addTarget(self, action: #selector(clickPhotoButton(_:)), for: .touchUpInside)
view.addSubview(photoButton)

camera = BBMetalCamera(sessionPreset: .hd1920x1080)
camera.canTakePhoto = true
camera.photoDelegate = self
camera.add(consumer: BBMetalLookupFilter(lookupTable: UIImage(named: "test_lookup")!.bb_metalTexture!))
.add(consumer: metalView)
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
camera.start()
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
camera.stop()
}

@objc private func clickPhotoButton(_ button: UIButton) {
camera.takePhoto()
}
}

extension CameraPhotoFilterVC: BBMetalCameraPhotoDelegate {
func camera(_ camera: BBMetalCamera, didOutput texture: MTLTexture) {
// In main thread
let filter = BBMetalLookupFilter(lookupTable: UIImage(named: "test_lookup")!.bb_metalTexture!)
let imageView = UIImageView(frame: metalView.frame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.image = filter.filteredImage(with: texture.bb_image!)
view.addSubview(imageView)

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
imageView.removeFromSuperview()
}
}

func camera(_ camera: BBMetalCamera, didFail error: Error) {
// In main thread
print("Fail taking photo. Error: \(error)")
}
}
4 changes: 4 additions & 0 deletions BBMetalImageDemo/BBMetalImageDemo/MainMenuVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class MainMenuVC: UIViewController {
let cameraFilter = { [weak self] in
if let self = self { self.navigationController?.pushViewController(CameraFilterMenuVC(), animated: true) }
}
let cameraPhotoFilter = { [weak self] in
if let self = self { self.navigationController?.pushViewController(CameraPhotoFilterVC(), animated: true) }
}
let videoFilter = { [weak self] in
if let self = self { self.navigationController?.pushViewController(VideoFilterVC(), animated: true) }
}
Expand All @@ -33,6 +36,7 @@ class MainMenuVC: UIViewController {
}
list = [("Static image filter", staticImageFilter),
("Camera filter", cameraFilter),
("Camera photo filter", cameraPhotoFilter),
("Video filter", videoFilter),
("Video filter 2", videoFilter2)]

Expand Down

0 comments on commit b19fd90

Please sign in to comment.