Skip to content

Commit 05cb9e4

Browse files
committed
Capturing video.
1 parent 1a92841 commit 05cb9e4

File tree

6 files changed

+458
-19
lines changed

6 files changed

+458
-19
lines changed

FanOfAscii.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,7 @@
10181018
CODE_SIGN_IDENTITY = "Apple Development";
10191019
CODE_SIGN_STYLE = Automatic;
10201020
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
1021-
DEVELOPMENT_TEAM = "";
1021+
DEVELOPMENT_TEAM = 2Z7DE68A4G;
10221022
GCC_DYNAMIC_NO_PIC = NO;
10231023
INFOPLIST_FILE = LiveViewTestApp/Info.plist;
10241024
LD_RUNPATH_SEARCH_PATHS = (
@@ -1048,7 +1048,7 @@
10481048
CODE_SIGN_IDENTITY = "Apple Development";
10491049
CODE_SIGN_STYLE = Automatic;
10501050
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
1051-
DEVELOPMENT_TEAM = "";
1051+
DEVELOPMENT_TEAM = 2Z7DE68A4G;
10521052
INFOPLIST_FILE = LiveViewTestApp/Info.plist;
10531053
LD_RUNPATH_SEARCH_PATHS = (
10541054
"$(inherited)",

FanOfAscii/Modules/BookCore.playgroundmodule/Sources/UserInterface/Chapters/01-FanOfAscii/01-Introduction/IntroductionLiveViewController.swift

Lines changed: 334 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,340 @@
33
// Created on 2020/2/10.
44
//
55

6-
import Foundation
6+
import UIKit
7+
import AVFoundation
8+
import Accelerate
79

810
class IntroductionLiveViewController: BaseViewController {
9-
11+
12+
@IBOutlet private weak var sessionSetupResultLabel: UILabel!
13+
14+
private enum CameraOrientation {
15+
case front, back
16+
}
17+
18+
private enum SessionSetupResult {
19+
case success
20+
case notAuthorized
21+
case configurationFailed
22+
}
23+
24+
private var setupResult: SessionSetupResult = .success
25+
26+
private let session = AVCaptureSession()
27+
private let sessionQueue = DispatchQueue(label: "SessionQueue", attributes: [], autoreleaseFrequency: .workItem)
28+
29+
private let dataOutputQueue = DispatchQueue(label: "VideoDataQueue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
30+
31+
private let backCameras = AVCaptureDevice.DiscoverySession(
32+
deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
33+
mediaType: AVMediaType.video,
34+
position: AVCaptureDevice.Position.back).devices
35+
private let frontCameras = AVCaptureDevice.DiscoverySession(
36+
deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
37+
mediaType: AVMediaType.video,
38+
position: AVCaptureDevice.Position.front).devices
39+
40+
private var cameraOrientation: CameraOrientation = .back
41+
42+
private lazy var info420vToARGB: vImage_YpCbCrToARGB? = {
43+
var info = vImage_YpCbCrToARGB()
44+
var pixelRange = vImage_YpCbCrPixelRange(
45+
Yp_bias: 16,
46+
CbCr_bias: 128,
47+
YpRangeMax: 235,
48+
CbCrRangeMax: 240,
49+
YpMax: 235,
50+
YpMin: 16,
51+
CbCrMax: 240,
52+
CbCrMin: 16)
53+
54+
if vImageConvert_YpCbCrToARGB_GenerateConversion(
55+
kvImage_YpCbCrToARGBMatrix_ITU_R_601_4,
56+
&pixelRange,
57+
&info,
58+
kvImage420Yp8_CbCr8,
59+
kvImageARGB8888,
60+
vImage_Flags(kvImageNoFlags)) == kvImageNoError {
61+
return info
62+
}
63+
return nil
64+
}()
65+
66+
private var destinationBuffer = vImage_Buffer()
67+
68+
private var cgImageFormat = vImage_CGImageFormat(bitsPerComponent: 8,
69+
bitsPerPixel: 32,
70+
colorSpace: nil,
71+
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue),
72+
version: 0,
73+
decode: nil,
74+
renderingIntent: .defaultIntent)
75+
76+
private var shouldRecreateDestinationBuffer = true
77+
78+
deinit {
79+
if destinationBuffer.data != nil {
80+
free(destinationBuffer.data)
81+
}
82+
}
83+
84+
override func viewDidLoad() {
85+
super.viewDidLoad()
86+
87+
sessionSetupResultLabel.isHidden = true
88+
89+
requestAuthorization()
90+
sessionQueue.async {
91+
self.reconfigureSession()
92+
}
93+
}
94+
95+
override func viewWillDisappear(_ animated: Bool) {
96+
sessionQueue.async {
97+
if self.setupResult == .success {
98+
self.session.stopRunning()
99+
}
100+
}
101+
102+
super.viewWillDisappear(animated)
103+
}
104+
105+
private func requestAuthorization() {
106+
switch AVCaptureDevice.authorizationStatus(for: .video) {
107+
case .authorized:
108+
break
109+
case .notDetermined:
110+
sessionQueue.suspend()
111+
AVCaptureDevice.requestAccess(for: .video) { granted in
112+
if !granted {
113+
self.setupResult = .notAuthorized
114+
}
115+
self.sessionQueue.resume()
116+
}
117+
default:
118+
setupResult = .notAuthorized
119+
}
120+
}
121+
122+
private func reconfigureSession() {
123+
guard setupResult == .success else {
124+
return
125+
}
126+
127+
session.beginConfiguration()
128+
defer {
129+
if setupResult != .success {
130+
session.commitConfiguration()
131+
}
132+
}
133+
134+
session.inputs.forEach { input in
135+
session.removeInput(input)
136+
}
137+
138+
session.sessionPreset = AVCaptureSession.Preset.photo
139+
140+
let cameraOptional: AVCaptureDevice?
141+
switch cameraOrientation {
142+
case .front:
143+
cameraOptional = frontCameras.first
144+
case .back:
145+
cameraOptional = backCameras.first
146+
}
147+
guard let cameraDevice = cameraOptional else {
148+
print("No camera found")
149+
setupResult = .configurationFailed
150+
return
151+
}
152+
153+
do {
154+
let videoInput = try AVCaptureDeviceInput(device: cameraDevice)
155+
guard session.canAddInput(videoInput) else {
156+
print("Could not add video device input to the session")
157+
setupResult = .configurationFailed
158+
return
159+
}
160+
session.addInput(videoInput)
161+
} catch {
162+
print("Could not create video device input: \(error)")
163+
setupResult = .configurationFailed
164+
return
165+
}
166+
167+
if session.outputs.isEmpty {
168+
let videoDataOutput = AVCaptureVideoDataOutput()
169+
if session.canAddOutput(videoDataOutput) {
170+
session.addOutput(videoDataOutput)
171+
let format_420v = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
172+
if videoDataOutput.availableVideoPixelFormatTypes.contains(format_420v) {
173+
videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(format_420v)]
174+
} else {
175+
print("Pixel format used (420v) is not supported.")
176+
setupResult = .configurationFailed
177+
return
178+
}
179+
videoDataOutput.setSampleBufferDelegate(self, queue: dataOutputQueue)
180+
} else {
181+
print("Could not add video data output to the session")
182+
setupResult = .configurationFailed
183+
return
184+
}
185+
}
186+
187+
session.commitConfiguration()
188+
}
189+
190+
override func viewWillAppear(_ animated: Bool) {
191+
super.viewWillAppear(animated)
192+
193+
sessionQueue.async {
194+
switch self.setupResult {
195+
case .success:
196+
self.adjustVideoOrientation()
197+
198+
self.session.startRunning()
199+
200+
DispatchQueue.main.async {
201+
self.sessionSetupResultLabel.isHidden = true
202+
}
203+
case .notAuthorized:
204+
DispatchQueue.main.async {
205+
let message = "AVCamFilter doesn't have permission to use the camera, please change privacy settings"
206+
self.sessionSetupResultLabel.isHidden = false
207+
self.sessionSetupResultLabel.text = message
208+
}
209+
case .configurationFailed:
210+
DispatchQueue.main.async {
211+
let message = "Unable to capture media"
212+
self.sessionSetupResultLabel.isHidden = false
213+
self.sessionSetupResultLabel.text = message
214+
}
215+
}
216+
}
217+
}
218+
219+
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
220+
super.viewWillTransition(to: size, with: coordinator)
221+
222+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
223+
self.adjustVideoOrientation()
224+
}
225+
}
226+
227+
func adjustVideoOrientation() {
228+
let interfaceOrientation = self.interfaceOrientation
229+
sessionQueue.async {
230+
if let interfaceOrientation = AVCaptureVideoOrientation(interfaceOrientation: interfaceOrientation),
231+
let videoCaptureConnection = self.session.outputs.first?.connection(with: .video) {
232+
videoCaptureConnection.videoOrientation = interfaceOrientation
233+
self.dataOutputQueue.async {
234+
self.shouldRecreateDestinationBuffer = true
235+
}
236+
}
237+
}
238+
}
239+
240+
override func didSelectImage(image: UIImage, pickerController: ImagePickerViewController) {
241+
242+
}
243+
244+
override func toolBarButtonTapped(buttonView: ToolBarButtonView) {
245+
246+
}
247+
248+
}
249+
250+
extension IntroductionLiveViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
251+
252+
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
253+
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
254+
return
255+
}
256+
257+
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
258+
processYpCbCrToRGB(pixelBuffer: pixelBuffer)
259+
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
260+
}
261+
262+
func processYpCbCrToRGB(pixelBuffer: CVPixelBuffer) {
263+
let lumaBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)
264+
let lumaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0)
265+
let lumaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0)
266+
let lumaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
267+
268+
var sourceLumaBuffer = vImage_Buffer(data: lumaBaseAddress,
269+
height: vImagePixelCount(lumaHeight),
270+
width: vImagePixelCount(lumaWidth),
271+
rowBytes: lumaRowBytes)
272+
273+
let chromaBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)
274+
let chromaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1)
275+
let chromaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1)
276+
let chromaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
277+
278+
var sourceChromaBuffer = vImage_Buffer(data: chromaBaseAddress,
279+
height: vImagePixelCount(chromaHeight),
280+
width: vImagePixelCount(chromaWidth * 2),
281+
rowBytes: chromaRowBytes)
282+
283+
if destinationBuffer.data == nil || shouldRecreateDestinationBuffer {
284+
if destinationBuffer.data != nil {
285+
free(destinationBuffer.data)
286+
}
287+
guard vImageBuffer_Init(&destinationBuffer,
288+
sourceLumaBuffer.height,
289+
sourceLumaBuffer.width,
290+
cgImageFormat.bitsPerPixel,
291+
vImage_Flags(kvImageNoFlags)) == kvImageNoError else {
292+
return
293+
}
294+
shouldRecreateDestinationBuffer = false
295+
}
296+
297+
guard vImageConvert_420Yp8_CbCr8ToARGB8888(&sourceLumaBuffer,
298+
&sourceChromaBuffer,
299+
&destinationBuffer,
300+
&info420vToARGB!,
301+
nil,
302+
255,
303+
vImage_Flags(kvImagePrintDiagnosticsToConsole)) == kvImageNoError else {
304+
return
305+
}
306+
307+
var error = kvImageNoError
308+
309+
let cgImage = vImageCreateCGImageFromBuffer(&destinationBuffer,
310+
&cgImageFormat,
311+
nil,
312+
nil,
313+
vImage_Flags(kvImageNoFlags),
314+
&error)
315+
316+
if let cgImage = cgImage, error == kvImageNoError {
317+
DispatchQueue.main.async {
318+
self.updateShowcaseImage(image: UIImage(cgImage: cgImage.takeRetainedValue()))
319+
}
320+
}
321+
}
322+
323+
}
324+
325+
extension AVCaptureVideoOrientation {
326+
327+
init?(interfaceOrientation: UIInterfaceOrientation) {
328+
switch interfaceOrientation {
329+
case .portrait:
330+
self = .portrait
331+
case .portraitUpsideDown:
332+
self = .portraitUpsideDown
333+
case .landscapeLeft:
334+
self = .landscapeLeft
335+
case .landscapeRight:
336+
self = .landscapeRight
337+
default:
338+
return nil
339+
}
340+
}
341+
10342
}

FanOfAscii/Modules/BookCore.playgroundmodule/Sources/UserInterface/ImagePicker/ImagePickerViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ extension ImagePickerViewController: UICollectionViewDelegate, UICollectionViewD
6060
switch indexPath.section {
6161
case 0:
6262
cell.setImage(named: "image-picker/button-camera")
63+
cell.state = .selected
6364
case 1:
6465
if indexPath == selectedCellIndexPath {
6566
cell.state = .selected

0 commit comments

Comments
 (0)