Skip to content

Commit 059152a

Browse files
committed
Initial Commit
1 parent 4cab5ab commit 059152a

File tree

4 files changed

+340
-15
lines changed

4 files changed

+340
-15
lines changed

Package.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@ import PackageDescription
55

66
let package = Package(
77
name: "MSCaptureView",
8+
platforms: [
9+
.macOS(.v10_14),
10+
],
811
products: [
9-
// Products define the executables and libraries produced by a package, and make them visible to other packages.
1012
.library(
1113
name: "MSCaptureView",
1214
targets: ["MSCaptureView"]),
1315
],
1416
dependencies: [
15-
// Dependencies declare other packages that this package depends on.
16-
// .package(url: /* package url */, from: "1.0.0"),
1717
],
1818
targets: [
19-
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
20-
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
2119
.target(
2220
name: "MSCaptureView",
2321
dependencies: []),

README.md

+53-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,54 @@
1-
# MSCaptureView
1+
# MSCaptureView - NSView subclass to capture video/audio from a Mac internal camera & microphone.
22

3-
A description of this package.
3+
AVFoundation provides all the tools a programmer needs to capture video & audio generated by the Camera and Microphone of a Mac or IOS program. AVCaptureView, part of AVKit, provides a View for displaying the video input and capturing the output to a file. The developer still has to write considerable code to connect the two parts. On the iOS side, Apple provides the often used UIImagePickerController class to combine all the components required to capture video and audio. Surprisingly, there is no such View or Controller on the Mac platform.
4+
5+
Now there is.
6+
7+
MSCaptureView can be used to give simple movie capture ability to any Mac program. Only a few calls are needed to set up the capture. All the functionality is encapsulated with the View, with simple calls to turn the capture on or off.
8+
9+
## Installation
10+
11+
Since MSCaptureView is a Swift Package, the IDE or Make file of a project must reference MSCaptureView's repository:
12+
13+
https://github.com/magesteve/MSCaptureView
14+
15+
To clone MSCaptureView, the following Terminal command should be used:
16+
17+
% git clone https://github.com/magesteve/MSCaptureView.git
18+
19+
## Platform Specific Usage
20+
21+
MSCaptureView is a NSVIew class; thus it must be added to a programs NSVIewController or NSWindowController. Use Interface Builder to add a generic NSView to the Interface of the App (XIB or Storyboard files). Then change the Views type to MSCaptureView. Create an IBOutlet reference between the view and its controller.
22+
23+
When the program starts, invoke the requestCaptureAuthorization() function so that app asks the user for permission to use the Camera & Microphone. When the view appears, call the showPreview() function to start the video preview. At some point, use the use(url) function to set the output location for the movie to be created. The startCapture() function starts recording to the movie file, while the stopCapture() function halts it. Swift closures can be used to inform the app of state changes by the view (ex: Authorization succeeded, start Movie recording, stop Movie recording).
24+
25+
## Documentation
26+
27+
All public classes, protocols, properties & functions have inline documentation (DOxygen style). Further explanation of the Framework, refer to the MSCaptureView-Demo repository or any example projects.
28+
29+
https://github.com/magesteve/MSCaptureView-Demo
30+
31+
## Requirements
32+
33+
MSCaptureView requires specific changes to the MacOS program it is running within.
34+
35+
1. Add NSCameraUsageDescription & NSMicrophoneUsageDescription to Info.plist. (ex: "This app requires the Camera for video capture.")
36+
2. Add AVFoundation Framework.
37+
3. Add Sandbox access to Hardware Camera and Audio Input,
38+
4. Add Sandbox access File Access for the Movie Folder set to Read/Write.
39+
40+
## Versions
41+
42+
1.0.0 Initial Release
43+
44+
## Future
45+
46+
I would like to add an optional HUD so that start and stop can be done directly within the preview.
47+
48+
### Steve Sheets, [email protected]
49+
50+
Originally from Silicon Valley, Steve has been embedded in the software industry for over 35 years. As an expert in user interface and design, he started developer desktop applications for companies like Apple and AOL, moved into mobile development, and is now working in the virtual reality and Augment Reality space. He has taught Objective-C & Swift development classes (MoDev, Learning Tree), as well as given talk on variety of developer topics (DC Mac Dev group, Capital One Swift Conference). He is an avid game player, swordsman and an occasional game designer.
51+
52+
## License
53+
54+
MSCaptureView is available under the MIT license. The intent of the project is to be always Open Source and freely available. Please keep me informed of any interesting uses!
+279-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,280 @@
1-
struct MSCaptureView {
2-
var text = "Hello, World!"
1+
//
2+
// MSCaptureView.swift
3+
// MSCaptureView
4+
//
5+
// Created by Steve Sheets on 8/13/20.
6+
// Copyright © 2020 Steve Sheets. All rights reserved.
7+
//
8+
9+
import Cocoa
10+
import AVFoundation
11+
12+
// MARK: MSCaptureView Class
13+
14+
/// NSView subclass to provied video capture and preview from Macintosh Cameria/Microphone
15+
public class MSCaptureView: NSView, AVCaptureFileOutputRecordingDelegate {
16+
17+
// MARK: Static Properties
18+
19+
/// Version information (major, minor, patch)
20+
public static let version = (1, 0, 0)
21+
22+
// MARK: Type Alias
23+
24+
/// Closure type that is passed the current Capture View and returns nothing
25+
public typealias StatusChangedEventClosure = (MSCaptureView) -> Void
26+
27+
/// Closure type that is passed nothing View and returns nothing
28+
public typealias AuthorizedEventClosure = () -> Void
29+
30+
// MARK: Private Property
31+
32+
private var captureSession: AVCaptureSession?
33+
private var capturePreviewLayer: AVCaptureVideoPreviewLayer?
34+
private var captureDeviceInputCamera: AVCaptureDeviceInput?
35+
private var captureDeviceInputMicrophone: AVCaptureDeviceInput?
36+
private var captureMovieFileOutput: AVCaptureMovieFileOutput?
37+
private var captureURL: URL?
38+
39+
// MARK: Public Properties
40+
41+
/// Closure evoked when the recording starts
42+
public var captureRecordingStartedEvent: StatusChangedEventClosure?
43+
44+
/// Closure evoked when the recording stops (either by call or by error).
45+
public var captureRecordingStoppedEvent: StatusChangedEventClosure?
46+
47+
// MARK: Public Read-Only Properties
48+
49+
/// Calculated property showing if the app has been authorized to use camera and microphone
50+
public var hasCaptureAuthorization: Bool {
51+
get {
52+
let videoStatus = AVCaptureDevice.authorizationStatus(for: .video)
53+
let microphoneStatus = AVCaptureDevice.authorizationStatus(for: .audio)
54+
55+
if case .authorized = videoStatus, case .authorized = microphoneStatus {
56+
return true
57+
}
58+
59+
return false
60+
}
61+
}
62+
63+
/// Calculated property showing if the app has preview turned on.
64+
public var hasPreview: Bool {
65+
get {
66+
guard let session = captureSession else { return false }
67+
68+
return session.isRunning
69+
}
70+
}
71+
72+
/// Calculated property showing if the app has output url set.
73+
public var hasURL: Bool {
74+
get {
75+
return captureURL != nil
76+
}
77+
}
78+
79+
/// Calculated property showing if the app is currently capturing
80+
public var isCapturing: Bool {
81+
get {
82+
guard let output = captureMovieFileOutput else { return false }
83+
84+
return output.isRecording
85+
}
86+
}
87+
88+
// MARK: Private Static Functions
89+
90+
private static func requestAudioAuthorization(success: @escaping AuthorizedEventClosure) {
91+
switch AVCaptureDevice.authorizationStatus(for: .audio) {
92+
case .authorized:
93+
success()
94+
return
95+
96+
case .notDetermined:
97+
AVCaptureDevice.requestAccess(for: .audio) {granted in
98+
guard granted else { return }
99+
100+
success()
101+
}
102+
103+
case .denied,
104+
.restricted:
105+
return
106+
107+
@unknown default:
108+
return
109+
}
110+
}
111+
112+
// MARK: Public Static Functions
113+
114+
/// Check the audio & video authorization. If not determined, makes requrest for them (displaying UI). If users has authorized both video and audio, then closure is invoked.
115+
/// - Parameter success: AuthorizedEventClosure to invoke if both video and audio is authorized.
116+
public static func requestCaptureAuthorization(success: @escaping AuthorizedEventClosure) {
117+
switch AVCaptureDevice.authorizationStatus(for: .video) {
118+
case .authorized:
119+
requestAudioAuthorization(success: success)
120+
return
121+
122+
case .notDetermined:
123+
AVCaptureDevice.requestAccess(for: .video) { granted in
124+
guard granted else { return }
125+
126+
MSCaptureView.requestAudioAuthorization(success: success)
127+
}
128+
129+
case .denied,
130+
.restricted:
131+
return
132+
133+
@unknown default:
134+
return
135+
}
136+
}
137+
138+
// MARK: Public Functions
139+
140+
/// Set URL to output captured video to.
141+
///
142+
/// If file already exists at this location, the file will be deleted as start of capture, not at setting the Capture URL.
143+
/// - Parameter url: URL to output.
144+
public func use(url: URL) {
145+
captureURL = url
146+
}
147+
148+
/// Clears the capture URL (the URL used to save the captured file to).
149+
public func clearURL() {
150+
captureURL = nil
151+
}
152+
153+
/// Attempts to turn on Preview layer on view. Return success if this occurs.
154+
///
155+
/// The actual video may take a second or two before it appears. This call creates the majority of the internal settings.
156+
/// If the app is not authorized, or if the Preview is already turned on, this call does nothing.
157+
/// - Returns: BOOL if video is turned on, returns True.
158+
@discardableResult public func showPreview() -> Bool {
159+
guard hasCaptureAuthorization, captureSession==nil else { return true }
160+
161+
self.wantsLayer = true
162+
163+
let session = AVCaptureSession()
164+
session.sessionPreset = .high
165+
166+
let layer = AVCaptureVideoPreviewLayer(session: session)
167+
168+
guard let camera = AVCaptureDevice.default(for: AVMediaType.video), let cameraInput = try? AVCaptureDeviceInput(device: camera) else { return false }
169+
170+
if session.canAddInput(cameraInput) {
171+
session.addInput(cameraInput)
172+
}
173+
174+
guard let microphone = AVCaptureDevice.default(for: AVMediaType.audio), let microphoneInput = try? AVCaptureDeviceInput(device: microphone) else { return false }
175+
176+
if session.canAddInput(microphoneInput) {
177+
session.addInput(microphoneInput)
178+
}
179+
180+
let movieOutput = AVCaptureMovieFileOutput()
181+
if session.canAddOutput(movieOutput) {
182+
session.addOutput(movieOutput)
183+
}
184+
185+
layer.frame = self.bounds
186+
layer.videoGravity = .resizeAspectFill
187+
self.layer = layer
188+
self.layerContentsPlacement = .scaleAxesIndependently
189+
190+
captureSession = session
191+
capturePreviewLayer = layer
192+
captureDeviceInputCamera = cameraInput
193+
captureDeviceInputMicrophone = microphoneInput
194+
captureMovieFileOutput = movieOutput
195+
196+
session.startRunning()
197+
198+
return true
199+
}
200+
201+
/// Turns off the Preview layer on view.
202+
///
203+
/// If the app is not authorized, or if the Preview is not turned on, this call does nothing.
204+
public func hidePreview() {
205+
guard hasCaptureAuthorization, let session = captureSession, let layer = capturePreviewLayer, let cameraInput = captureDeviceInputCamera, let microphoneInput = captureDeviceInputMicrophone, let movieOutput = captureMovieFileOutput else { return }
206+
207+
session.stopRunning()
208+
209+
self.layer = CALayer()
210+
self.wantsLayer = true
211+
212+
layer.session = nil
213+
214+
if session.canAddInput(cameraInput) {
215+
session.removeInput(cameraInput)
216+
}
217+
218+
if session.canAddInput(microphoneInput) {
219+
session.removeInput(microphoneInput)
220+
}
221+
222+
if session.canAddOutput(movieOutput) {
223+
session.removeOutput(movieOutput)
224+
}
225+
226+
captureSession = nil
227+
capturePreviewLayer = nil
228+
captureDeviceInputCamera = nil
229+
captureDeviceInputMicrophone = nil
230+
captureMovieFileOutput = nil
231+
}
232+
233+
/// Starts capturing the video to the output URL.
234+
///
235+
/// If the app is not authroized, or if the Capture URL is not set, or if the capturing is already started, this call does nothing.
236+
/// If successful, the captureRecordingStartedEvent will be invoked.
237+
/// If the file has permission issues, the captureRecordingStoppedEvent will be invoked after captureRecordingStartedEvent.
238+
/// If the Capture URL points to a file that exists, this call will delete it before starting recording.
239+
public func startCapture() {
240+
guard hasCaptureAuthorization, let url = captureURL, let output = captureMovieFileOutput else { return }
241+
242+
do {
243+
try FileManager.default.removeItem(at: url)
244+
}
245+
catch {
246+
}
247+
248+
output.startRecording(to: url, recordingDelegate: self)
249+
}
250+
251+
/// Stops capturing the video to the output URL.
252+
///
253+
/// If the app is not authroized, or if the Capture URL is not set, or if the capturing is already started, this call does nothing.
254+
/// If succesfull, the captureRecordingStoppedEvent closure will be invoked.
255+
public func stopCapture() {
256+
guard hasCaptureAuthorization, let output = captureMovieFileOutput else { return }
257+
258+
output.stopRecording()
259+
}
260+
261+
// MARK: Delegate Functions
262+
263+
public func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo: URL, from: [AVCaptureConnection]) {
264+
if let block = captureRecordingStartedEvent {
265+
block(self)
266+
}
267+
}
268+
269+
public func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
270+
if let block = captureRecordingStoppedEvent {
271+
block(self)
272+
}
273+
274+
if let error = error {
275+
print("AV File Capture Error: \(error.localizedDescription)")
276+
}
277+
}
278+
3279
}
280+

Tests/MSCaptureViewTests/MSCaptureViewTests.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import XCTest
22
@testable import MSCaptureView
33

44
final class MSCaptureViewTests: XCTestCase {
5-
func testExample() {
6-
// This is an example of a functional test case.
7-
// Use XCTAssert and related functions to verify your tests produce the correct
8-
// results.
9-
XCTAssertEqual(MSCaptureView().text, "Hello, World!")
5+
func testVesion() {
6+
let (major, minor, _) = MSCaptureView.version
7+
XCTAssertEqual(major, 1)
8+
XCTAssertEqual(minor, 0)
109
}
1110

1211
static var allTests = [
13-
("testExample", testExample),
12+
("testVesion", testVesion),
1413
]
1514
}

0 commit comments

Comments
 (0)