Ver3.5 ๐ธ iOS ์นด๋ฉ๋ผ์ AVFoundation์ ํ์ฉํ ์ฌ์งยท๋น๋์ค ์ฒ๋ฆฌ ์์ ์ ๋ณต

๐ธ iOS ์นด๋ฉ๋ผ์ AVFoundation์ ํ์ฉํ ์ฌ์งยท๋น๋์ค ์ฒ๋ฆฌ ์์ ์ ๋ณต
์นด๋ฉ๋ผ ์ฑ ๊ฐ๋ฐ์ด ์ด๋ ต๋ค๊ณ ? ๊ฑฑ์ ใดใด ๐ AVFoundation ๊ธฐ์ด๋ถํฐ ์ค์ ์ฝ๋๊น์ง ์น ๋ค ์๋ ค์ค๊ฒ!
์ผ ์์งํ ๋งํด์, iOS ์นด๋ฉ๋ผ ๊ฐ๋ฐ ์ฒ์ ์ ํ๋ฉด ์ง์ง ๋ฉ๋ถ ์ค์์ ๐ต
AVFoundation์ด ๋ญ์ง, AVCaptureSession์ ๋ ๋ญ์ง, ์ฉ์ด๋ถํฐ ๋ฏ์ค์ด์ ํฌ๊ธฐํ๊ณ ์ถ์ด์ง๋ ๊ทธ ๋๋ ์์ง?
๊ทผ๋ฐ ํ๋ฒ ์ ๋๋ก ์ดํดํ๋ฉด ์ง์ง ๋ณ๊ฑฐ ์์ดใ
ใ
์ค๋ ์ด ๊ธ ๋ค ์ฝ๊ณ ๋๋ฉด "์ ์ด๊ฑฐ์์ด?" ํ๊ฒ ๋ ๊ฑฐ์ ๐
iOS์์ ์นด๋ฉ๋ผ์ ๋ฏธ๋์ด๋ฅผ ๋ค๋ฃจ๋ ํต์ฌ ํ๋ ์์ํฌ๋ ๋ฐ๋ก AVFoundation์ด์ผ.
Apple์ด iOS 4.0๋ถํฐ ์ ๊ณตํด์จ ๊ฐ๋ ฅํ ๋ฏธ๋์ด ์ฒ๋ฆฌ ํ๋ ์์ํฌ๋ก, ์ฌ์ง ์ดฌ์๋ถํฐ ๋น๋์ค ๋
นํ, ์ค์๊ฐ ํํฐ ์ ์ฉ, ์ค๋์ค ์ฒ๋ฆฌ๊น์ง ๊ฑฐ์ ๋ชจ๋ ๊ฑธ ์ปค๋ฒํด.
์, ๊ทธ๋ผ ์ง์ง ์์ํด๋ณด์๊ณ ! ๐
AVFoundation์ Apple์ด ์ ๊ณตํ๋ ๋ฏธ๋์ด ์ฒ๋ฆฌ ํ๋ ์์ํฌ์ผ.
iOS, macOS, tvOS, watchOS ์ ๋ถ ์ง์ํ๊ณ , ์ค๋์คยท๋น๋์ค์ ์บก์ฒ, ์ฌ์, ํธ์ง, ๋ด๋ณด๋ด๊ธฐ๋ฅผ ๋ชจ๋ ๋ด๋นํด.
์นด๋ฉ๋ผ ์ฑ ๋ง๋ค ๋ ์ด ํ๋ ์์ํฌ ์์ด๋ ์ง์ง ์๋ฌด๊ฒ๋ ๋ชป ํ๋ค๊ณ ๋ด๋ ๋จ ๐
AVFoundation์ ํต์ฌ ์ปดํฌ๋ํธ๋ค์ ๋จผ์ ์์์ผ ์ ์ฒด ๊ทธ๋ฆผ์ด ๊ทธ๋ ค์ง๊ฑฐ๋ .
๋ง์น ๋ ๊ณ ๋ธ๋ก์ฒ๋ผ ๊ฐ ์ปดํฌ๋ํธ๋ฅผ ์กฐ๋ฆฝํด์ ์นด๋ฉ๋ผ ๊ธฐ๋ฅ์ ๋ง๋๋ ๊ฑฐ์ผ!
| ํด๋์ค๋ช | ์ญํ |
|---|---|
AVCaptureSession |
์ ์ฒด ์บก์ฒ ํ์ดํ๋ผ์ธ์ ์ค์ฌ. ์ ๋ ฅ๊ณผ ์ถ๋ ฅ์ ์ฐ๊ฒฐํ๋ ํ๋ธ |
AVCaptureDevice |
์ค์ ์นด๋ฉ๋ผ/๋ง์ดํฌ ํ๋์จ์ด๋ฅผ ๋ํ๋ด๋ ๊ฐ์ฒด |
AVCaptureDeviceInput |
๋๋ฐ์ด์ค๋ฅผ ์ธ์ ์ ์ฐ๊ฒฐํ๋ ์ ๋ ฅ ๊ฐ์ฒด |
AVCapturePhotoOutput |
์ฌ์ง ์ดฌ์ ์ถ๋ ฅ ์ฒ๋ฆฌ |
AVCaptureMovieFileOutput |
๋น๋์ค ํ์ผ ๋ นํ ์ถ๋ ฅ ์ฒ๋ฆฌ |
AVCaptureVideoDataOutput |
์ค์๊ฐ ๋น๋์ค ํ๋ ์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ |
AVCaptureVideoPreviewLayer |
์นด๋ฉ๋ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ๋ฉด์ ํ์ํ๋ ๋ ์ด์ด |
ใดใด ์ธ์ธ ํ์ ์์ดใ
ใ
๊ทธ๋ฅ "์ ์ด๋ฐ ๊ฒ ์๊ตฌ๋" ํ๊ณ ํ๋ฆ๋ง ํ์
ํ๋ฉด ๋ผ.
์ฝ๋ฉํ๋ค ๋ณด๋ฉด ์์ฐ์ค๋ฝ๊ฒ ์ตํ์ง๊ฑฐ๋ . ์ผ๋จ ์ ์ฒด ํ์ดํ๋ผ์ธ ๊ตฌ์กฐ๋ฅผ ์ดํดํ๋ ๊ฒ ํต์ฌ์ด์ผ! ๐ช
์นด๋ฉ๋ผ ๊ธฐ๋ฅ ๊ตฌํ ์ ์ ๋ฐ๋์ ํด์ผ ํ๋ ๊ฒ ์์ด.
๋ฐ๋ก Info.plist ๊ถํ ์ค์ ์ด์ผ.
์ด๊ฑฐ ๋น ๋จ๋ฆฌ๋ฉด ์ฑ์ด ํฌ๋์ ๋๊ฑฐ๋ ์นด๋ฉ๋ผ๊ฐ ์์ ์ ์ด๋ ค์ ๋ฉ๋ถ ์ด ๐ค
NSMicrophoneUsageDescription - ๋ง์ดํฌ ์ ๊ทผ ๊ถํ ์ค๋ช (๋น๋์ค ๋ นํ ์)
NSPhotoLibraryUsageDescription - ์ฌ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ทผ ๊ถํ ์ค๋ช
NSPhotoLibraryAddUsageDescription - ์ฌ์ง ์ ์ฅ ๊ถํ ์ค๋ช
๊ฐ ํค์ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค ์ค๋ช ๋ฌธ์์ด์ ๋ฃ์ด์ค์ผ ํด.
์: "์ฌ์ง ์ดฌ์์ ์ํด ์นด๋ฉ๋ผ ์ ๊ทผ์ด ํ์ํฉ๋๋ค."
Xcode 14 ์ดํ๋ถํฐ๋ Info.plist ํ์ผ์ ์ง์ ํธ์งํ๊ฑฐ๋
ํ๋ก์ ํธ ์ค์ โ Info ํญ์์ ์ถ๊ฐํ ์ ์์ด.
๊ถํ ์์ฒญ ์ฝ๋๋ ์์ฑํด์ผ ํ๋๋ฐ, ์ด๋ ๊ฒ ํด:
import AVFoundation
// ์นด๋ฉ๋ผ ๊ถํ ์์ฒญ
func requestCameraPermission() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
// ์ด๋ฏธ ๊ถํ ์์ - ๋ฐ๋ก ์ธ์
์ค์ ์์
setupCaptureSession()
case .notDetermined:
// ์์ง ์์ฒญ ์ ํจ - ๊ถํ ์์ฒญ
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
DispatchQueue.main.async {
self.setupCaptureSession()
}
}
}
case .denied, .restricted:
// ๊ฑฐ๋ถ๋จ - ์ค์ ์ฑ์ผ๋ก ์๋ด
showPermissionAlert()
@unknown default:
break
}
}
UI ์ ๋ฐ์ดํธ๋ ๋ฐ๋์
DispatchQueue.main.async๋ก ๊ฐ์ธ์ค์ผ ํด!์ด๊ฑฐ ๋น ๋จ๋ฆฌ๋ฉด UI๊ฐ ์ด์ํ๊ฒ ๋์ํ๋ ๋ฒ๊ทธ ์๊น ๐
์, ์ด์ ์ง์ง ํต์ฌ์ธ AVCaptureSession ์ค์ ์ด์ผ.
์ด๊ฒ ์นด๋ฉ๋ผ ๊ธฐ๋ฅ์ ์ฌ์ฅ ๊ฐ์ ์กด์ฌ๊ฑฐ๋ .
์
๋ ฅ(์นด๋ฉ๋ผ)๊ณผ ์ถ๋ ฅ(์ฌ์ง/๋น๋์ค)์ ์ฐ๊ฒฐํด์ฃผ๋ ํ์ดํ๋ผ์ธ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ! ๐
import UIKit
import AVFoundation
class CameraViewController: UIViewController {
// ํต์ฌ ํ๋กํผํฐ๋ค
private let captureSession = AVCaptureSession()
private var videoDeviceInput: AVCaptureDeviceInput?
private let photoOutput = AVCapturePhotoOutput()
private var previewLayer: AVCaptureVideoPreviewLayer?
// ์ธ์
์ค์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์์ ์คํ (์ค์!)
private let sessionQueue = DispatchQueue(label: "session.queue")
override func viewDidLoad() {
super.viewDidLoad()
requestCameraPermission()
}
func setupCaptureSession() {
// ๋ฐ๋์ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์์ ์คํ
sessionQueue.async {
self.configureCaptureSession()
}
}
private func configureCaptureSession() {
// ์ธ์
์ค์ ์์
captureSession.beginConfiguration()
// ์ธ์
ํ๋ฆฌ์
์ค์ (ํ์ง ๊ฒฐ์ )
captureSession.sessionPreset = .photo
// 1. ์นด๋ฉ๋ผ ๋๋ฐ์ด์ค ๊ฐ์ ธ์ค๊ธฐ
guard let videoDevice = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back
) else {
print("์นด๋ฉ๋ผ ๋๋ฐ์ด์ค๋ฅผ ์ฐพ์ ์ ์์ ๐ฑ")
captureSession.commitConfiguration()
return
}
// 2. ๋๋ฐ์ด์ค ์
๋ ฅ ์์ฑ
do {
let videoInput = try AVCaptureDeviceInput(device: videoDevice)
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
self.videoDeviceInput = videoInput
}
} catch {
print("์
๋ ฅ ์์ฑ ์คํจ: \(error)")
captureSession.commitConfiguration()
return
}
// 3. ์ฌ์ง ์ถ๋ ฅ ์ถ๊ฐ
if captureSession.canAddOutput(photoOutput) {
captureSession.addOutput(photoOutput)
// HEIF ํฌ๋งท ์ง์ ์ฌ๋ถ ํ์ธ
photoOutput.isHighResolutionCaptureEnabled = true
}
// ์ธ์
์ค์ ์๋ฃ
captureSession.commitConfiguration()
// ์ธ์
์์
captureSession.startRunning()
// ๋ฉ์ธ ์ค๋ ๋์์ ํ๋ฆฌ๋ทฐ ๋ ์ด์ด ์ค์
DispatchQueue.main.async {
self.setupPreviewLayer()
}
}
}
๊ทธ๋์ ๋ณ๋์ ์ง๋ ฌ ํ(serial queue)๋ฅผ ๋ง๋ค์ด์ ๊ฑฐ๊ธฐ์ ์ฒ๋ฆฌํ๋ ๊ฒ Apple ๊ถ์ฅ ๋ฐฉ์์ด์ผ.
๋ฉ์ธ ์ค๋ ๋๋ UI ์ ๋ฐ์ดํธ๋ง ๋ด๋นํ๊ฒ ํ๋ ๊ฑฐ์ง! ๐ฏ
์นด๋ฉ๋ผ ํ๋ฉด์ ์ค์ ๋ก ๋ณด์ฌ์ฃผ๋ ค๋ฉด AVCaptureVideoPreviewLayer๋ฅผ ๋ทฐ์ ์ถ๊ฐํด์ผ ํด.
์ด๊ฒ ์์ผ๋ฉด ์นด๋ฉ๋ผ๋ ๋์๊ฐ๋๋ฐ ํ๋ฉด์ ์๋ฌด๊ฒ๋ ์ ๋ณด์ด๋ ์ํฉ์ด ๋จใ
ใ
private func setupPreviewLayer() {
// ํ๋ฆฌ๋ทฐ ๋ ์ด์ด ์์ฑ
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds
// ๋ทฐ์ ๋ ์ด์ด์ ์ถ๊ฐ
view.layer.insertSublayer(previewLayer, at: 0)
self.previewLayer = previewLayer
}
// ํ๋ฉด ํ์ ์ ํ๋ฆฌ๋ทฐ ๋ ์ด์ด ํฌ๊ธฐ ์
๋ฐ์ดํธ
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
previewLayer?.frame = view.bounds
}
// ๋ทฐ๊ฐ ์ฌ๋ผ์ง ๋ ์ธ์
์ค์ง (๋ฐฐํฐ๋ฆฌ ์ ์ฝ!)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sessionQueue.async {
self.captureSession.stopRunning()
}
}
// ๋ทฐ๊ฐ ๋ค์ ๋ํ๋ ๋ ์ธ์
์ฌ์์
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
sessionQueue.async {
if !self.captureSession.isRunning {
self.captureSession.startRunning()
}
}
}
| ์ต์ | ์ค๋ช |
|---|---|
.resizeAspectFill |
ํ๋ฉด์ ๊ฝ ์ฑ์ (์๋ฆด ์ ์์). ์ธ์คํ๊ทธ๋จ ์นด๋ฉ๋ผ ์คํ์ผ |
.resizeAspect |
๋น์จ ์ ์งํ๋ฉฐ ์ ์ฒด ํ์ (๋ ํฐ๋ฐ์ค ์๊ธธ ์ ์์) |
.resize |
๋น์จ ๋ฌด์ํ๊ณ ํ๋ฉด์ ๋ง์ถค (์๊ณก ๋ฐ์) |
๋๋์ด ์ค์ ๋ก ์ฌ์ง์ ์ฐ๋ ์ฝ๋์ผ!
AVCapturePhotoOutput์ ์ฌ์ฉํ๋ฉด ๊ณ ํด์๋ ์ฌ์ง, HDR, Live Photo, RAW ํฌ๋งท๊น์ง ๋ค ์ง์ํด.
iOS 10๋ถํฐ ๊ธฐ์กด์ AVCaptureStillImageOutput์ด deprecated๋๊ณ ์ด๊ฑธ๋ก ๋์ฒด๋์ด. ๐ธ
// ์ฌ์ง ์ดฌ์ ๋ฒํผ ์ก์
@IBAction func capturePhoto(_ sender: UIButton) {
// ์ดฌ์ ์ค์ ๊ตฌ์ฑ
let photoSettings = AVCapturePhotoSettings()
// ํ๋์ ์ค์
if videoDeviceInput?.device.isFlashAvailable == true {
photoSettings.flashMode = .auto
}
// ๊ณ ํด์๋ ์ดฌ์ ํ์ฑํ
photoSettings.isHighResolutionPhotoEnabled = true
// HEIF ํฌ๋งท ์ฌ์ฉ (iOS 11+, ์ฉ๋ ์ ์ฝ)
if photoOutput.availablePhotoCodecTypes.contains(.hevc) {
let settings = AVCapturePhotoSettings(
format: [AVVideoCodecKey: AVVideoCodecType.hevc]
)
photoOutput.capturePhoto(with: settings, delegate: self)
} else {
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
}
// AVCapturePhotoCaptureDelegate ๊ตฌํ
extension CameraViewController: AVCapturePhotoCaptureDelegate {
// ์ดฌ์ ์๋ฃ ์ฝ๋ฐฑ
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
if let error = error {
print("์ดฌ์ ์คํจ: \(error) ๐ญ")
return
}
// ์ด๋ฏธ์ง ๋ฐ์ดํฐ ์ถ์ถ
guard let imageData = photo.fileDataRepresentation() else {
print("์ด๋ฏธ์ง ๋ฐ์ดํฐ ๋ณํ ์คํจ")
return
}
// UIImage๋ก ๋ณํ
guard let image = UIImage(data: imageData) else { return }
// ์ฌ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ฅ
saveToPhotoLibrary(image: image)
// ๋๋ ์ด๋ฏธ์ง๋ทฐ์ ํ์
DispatchQueue.main.async {
self.previewImageView.image = image
}
}
}
import Photos
func saveToPhotoLibrary(image: UIImage) {
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else {
print("์ฌ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ทผ ๊ถํ ์์ ๐ค")
return
}
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}) { success, error in
if success {
print("์ฌ์ง ์ ์ฅ ์๋ฃ! ๐")
} else if let error = error {
print("์ ์ฅ ์คํจ: \(error)")
}
}
}
}
iPhone ๊ธฐ๋ณธ ์นด๋ฉ๋ผ ์ฑ์ด ์ฌ์ฉํ๋ ํฌ๋งท์ด์ผ.
JPEG: ํธํ์ฑ ์ต๊ณ . ๋ค๋ฅธ ํ๋ซํผ์ด๋ ์๋ฒ์ ์ ๋ก๋ํ ๋ ์์ ํ ์ ํ.
์ฑ ์ฉ๋์ ๋ฐ๋ผ ์ ํํ๋ฉด ๋ผ. ์๋ฒ ์ ๋ก๋๊ฐ ๋ง์ผ๋ฉด JPEG, ๋ก์ปฌ ์ ์ฅ ์์ฃผ๋ฉด HEIF ์ถ์ฒ! ๐
Live Photo๋ ์ฌ์ง ์ฐ๊ธฐ ์ ํ 1.5์ด ์์์ ๊ฐ์ด ์ ์ฅํ๋ ๊ธฐ๋ฅ์ด์ผ.
iPhone 6s๋ถํฐ ์ง์๋๊ณ , ๊ตฌํ๋ ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์์!
// Live Photo ์ค์
func setupLivePhoto() {
let photoSettings = AVCapturePhotoSettings()
// Live Photo ์ง์ ์ฌ๋ถ ํ์ธ
if photoOutput.isLivePhotoCaptureSupported {
// ์์ ํ์ผ ๊ฒฝ๋ก ์ค์
let livePhotoMovieURL = URL(
fileURLWithPath: NSTemporaryDirectory()
).appendingPathComponent("\(UUID().uuidString).mov")
photoSettings.livePhotoMovieFileURL = livePhotoMovieURL
}
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
// Live Photo ์ฒ๋ฆฌ ๋ธ๋ฆฌ๊ฒ์ดํธ
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL,
resolvedSettings: AVCaptureResolvedPhotoSettings
) {
print("Live Photo ์์ ๋
นํ ์๋ฃ: \(outputFileURL)")
}
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL,
duration: CMTime,
photoDisplayTime: CMTime,
resolvedSettings: AVCaptureResolvedPhotoSettings,
error: Error?
) {
if let error = error {
print("Live Photo ์ฒ๋ฆฌ ์คํจ: \(error)")
return
}
print("Live Photo ์์ฑ! ๐ฌ ๊ฒฝ๋ก: \(outputFileURL)")
}
์ฌ์ง ์ฐ๋ ๊ฑด ํด๋ดค์ผ๋ ์ด์ ๋น๋์ค ๋
นํ์ผ!
AVCaptureMovieFileOutput์ ์ฌ์ฉํ๋ฉด MP4/MOV ํ์ผ๋ก ์ง์ ์ ์ฅํ ์ ์์ด.
์ค์๊ฐ ์ฒ๋ฆฌ ์์ด ํ์ผ๋ก ๋ฐ๋ก ์ ์ฅํ๋ ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ด์ง ๐
class VideoRecordingViewController: UIViewController {
private let captureSession = AVCaptureSession()
private let movieOutput = AVCaptureMovieFileOutput()
private let sessionQueue = DispatchQueue(label: "video.session.queue")
func setupVideoSession() {
sessionQueue.async {
self.captureSession.beginConfiguration()
self.captureSession.sessionPreset = .high
// ๋น๋์ค ์
๋ ฅ ์ถ๊ฐ
guard let videoDevice = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back
),
let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
self.captureSession.canAddInput(videoInput) else { return }
self.captureSession.addInput(videoInput)
// ์ค๋์ค ์
๋ ฅ ์ถ๊ฐ (๋ง์ดํฌ)
guard let audioDevice = AVCaptureDevice.default(for: .audio),
let audioInput = try? AVCaptureDeviceInput(device: audioDevice),
self.captureSession.canAddInput(audioInput) else { return }
self.captureSession.addInput(audioInput)
// ๋น๋์ค ํ์ผ ์ถ๋ ฅ ์ถ๊ฐ
if self.captureSession.canAddOutput(self.movieOutput) {
self.captureSession.addOutput(self.movieOutput)
// ๋น๋์ค ์์ ํ ์ค์ (์๋จ๋ฆผ ๋ณด์ )
if let connection = self.movieOutput.connection(with: .video) {
if connection.isVideoStabilizationSupported {
connection.preferredVideoStabilizationMode = .auto
}
}
}
self.captureSession.commitConfiguration()
self.captureSession.startRunning()
}
}
// ๋
นํ ์์
func startRecording() {
guard !movieOutput.isRecording else { return }
// ์์ ํ์ผ ๊ฒฝ๋ก ์์ฑ
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("\(UUID().uuidString).mov")
movieOutput.startRecording(to: outputURL, recordingDelegate: self)
print("๋
นํ ์์! ๐ด")
}
// ๋
นํ ์ข
๋ฃ
func stopRecording() {
guard movieOutput.isRecording else { return }
movieOutput.stopRecording()
print("๋
นํ ์ข
๋ฃ! โน๏ธ")
}
}
// ๋
นํ ์๋ฃ ๋ธ๋ฆฌ๊ฒ์ดํธ
extension VideoRecordingViewController: AVCaptureFileOutputRecordingDelegate {
func fileOutput(
_ output: AVCaptureFileOutput,
didFinishRecordingTo outputFileURL: URL,
from connections: [AVCaptureConnection],
error: Error?
) {
if let error = error {
print("๋
นํ ์คํจ: \(error) ๐ญ")
return
}
print("๋
นํ ์๋ฃ! ํ์ผ ๊ฒฝ๋ก: \(outputFileURL)")
// ์ฌ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ฅ
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(
atFileURL: outputFileURL
)
}) { success, error in
// ์์ ํ์ผ ์ญ์
try? FileManager.default.removeItem(at: outputFileURL)
if success {
print("๋น๋์ค ์ ์ฅ ์๋ฃ! ๐")
}
}
}
}
}
}
movieOutput.maxRecordedDuration์ผ๋ก ์ต๋ ๋
นํ ์๊ฐ์ ์ค์ ํ ์ ์์ด.movieOutput.maxRecordedFileSize๋ก ์ต๋ ํ์ผ ํฌ๊ธฐ๋ ์ ํ ๊ฐ๋ฅ.๋ฌด์ ํ์ผ๋ก ๋๋ฉด ์ ์ฅ ๊ณต๊ฐ ๋ค ์ก์๋จน์ ์ ์์ผ๋ ์ฃผ์ํด! ๐
์ด์ ์ง์ง ๊ณ ๊ธ ๊ธฐ๋ฅ์ด์ผ!
AVCaptureVideoDataOutput์ ์ฌ์ฉํ๋ฉด ์นด๋ฉ๋ผ์์ ๋ค์ด์ค๋ ๊ฐ ํ๋ ์์ ์ค์๊ฐ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ด.
ํํฐ ์ ์ฉ, ์ผ๊ตด ์ธ์, AR ํจ๊ณผ, ๋จธ์ ๋ฌ๋ ์ถ๋ก ๋ฑ ๋ค ์ฌ๊ธฐ์ ์ด๋ฃจ์ด์ง๊ฑฐ๋ .
์ธ์คํ๊ทธ๋จ ํํฐ, ์ค๋
ธ์ฐ ์ฑ ๊ฐ์ ๊ฒ ๋ค ์ด ๋ฐฉ์์ผ๋ก ๋ง๋ค์ด์ง ๊ฑฐ์ผ ๐
๋ง์! ๊ทธ๋ฆฌ๊ณ Vision ํ๋ ์์ํฌ๋ Core ML์ด๋ ์ฐ๋ํ๋ฉด ์ค์๊ฐ ๊ฐ์ฒด ์ธ์๋ ๊ฐ๋ฅํด.
์ผ๋จ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ถํฐ ๋ณด์!
class RealtimeVideoProcessor: NSObject {
private let captureSession = AVCaptureSession()
private let videoDataOutput = AVCaptureVideoDataOutput()
// ํ๋ ์ ์ฒ๋ฆฌ์ฉ ๋ณ๋ ํ (๋ฉ์ธ ์ค๋ ๋ ๋ธ๋กํน ๋ฐฉ์ง)
private let videoDataOutputQueue = DispatchQueue(
label: "video.data.output",
qos: .userInitiated,
attributes: [],
autoreleaseFrequency: .workItem
)
func setupRealtimeCapture() {
captureSession.beginConfiguration()
captureSession.sessionPreset = .high
// ์นด๋ฉ๋ผ ์
๋ ฅ ์ค์
guard let videoDevice = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .front // ์ ๋ฉด ์นด๋ฉ๋ผ
),
let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
captureSession.canAddInput(videoInput) else { return }
captureSession.addInput(videoInput)
// ๋น๋์ค ๋ฐ์ดํฐ ์ถ๋ ฅ ์ค์
videoDataOutput.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String:
kCVPixelFormatType_32BGRA // BGRA ํฌ๋งท
]
// ์ฒ๋ฆฌ ๋ชปํ ํ๋ ์์ ๋๋กญ (์ค์๊ฐ ์ฒ๋ฆฌ ์ ์ง)
videoDataOutput.alwaysDiscardsLateVideoFrames = true
// ๋ธ๋ฆฌ๊ฒ์ดํธ ์ค์
videoDataOutput.setSampleBufferDelegate(
self,
queue: videoDataOutputQueue
)
if captureSession.canAddOutput(videoDataOutput) {
captureSession.addOutput(videoDataOutput)
}
captureSession.commitConfiguration()
captureSession.startRunning()
}
}
// ์ค์๊ฐ ํ๋ ์ ์ฒ๋ฆฌ ๋ธ๋ฆฌ๊ฒ์ดํธ
extension RealtimeVideoProcessor: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// ์ฌ๊ธฐ์ ๊ฐ ํ๋ ์์ ์ฒ๋ฆฌ!
// sampleBuffer์์ ํฝ์
๋ฒํผ ์ถ์ถ
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
// CIImage๋ก ๋ณํ (Core Image ํํฐ ์ ์ฉ ๊ฐ๋ฅ)
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
// ์: ํ๋ฐฑ ํํฐ ์ ์ฉ
let filter = CIFilter(name: "CIPhotoEffectMono")!
filter.setValue(ciImage, forKey: kCIInputImageKey)
if let outputImage = filter.outputImage {
// ์ฒ๋ฆฌ๋ ์ด๋ฏธ์ง๋ฅผ ํ๋ฉด์ ํ์
DispatchQueue.main.async {
// MTKView๋ GLKView์ ๋ ๋๋ง
self.renderFilteredImage(outputImage)
}
}
// Vision ํ๋ ์์ํฌ๋ก ์ผ๊ตด ๊ฐ์ง
detectFaces(in: pixelBuffer)
}
// ํ๋ ์ ๋๋กญ ์๋ฆผ
func captureOutput(
_ output: AVCaptureOutput,
didDrop sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// ์ฒ๋ฆฌ ์๋๊ฐ ๋๋ ค์ ํ๋ ์ ๋๋กญ ๋ฐ์
// ์ฒ๋ฆฌ ๋ก์ง ์ต์ ํ ํ์ ์ ํธ!
print("โ ๏ธ ํ๋ ์ ๋๋กญ ๋ฐ์!")
}
}
import Vision
func detectFaces(in pixelBuffer: CVPixelBuffer) {
// ์ผ๊ตด ๊ฐ์ง ์์ฒญ ์์ฑ
let faceDetectionRequest = VNDetectFaceRectanglesRequest { request, error in
if let error = error {
print("์ผ๊ตด ๊ฐ์ง ์ค๋ฅ: \(error)")
return
}
guard let observations = request.results as? [VNFaceObservation] else {
return
}
DispatchQueue.main.async {
// ๊ฐ์ง๋ ์ผ๊ตด ์ ์ถ๋ ฅ
print("๊ฐ์ง๋ ์ผ๊ตด: \(observations.count)๊ฐ ๐")
// ๊ฐ ์ผ๊ตด์ ์์น ์ ๋ณด ์ฒ๋ฆฌ
for face in observations {
// boundingBox๋ 0~1 ์ฌ์ด ์ ๊ทํ๋ ์ขํ
let boundingBox = face.boundingBox
print("์ผ๊ตด ์์น: \(boundingBox)")
// ํ๋ฉด ์ขํ๋ก ๋ณํํด์ ๋ฐ์ค ๊ทธ๋ฆฌ๊ธฐ
self.drawFaceBox(boundingBox: boundingBox)
}
}
}
// ์์ฒญ ์คํ
let handler = VNImageRequestHandler(
cvPixelBuffer: pixelBuffer,
orientation: .up,
options: [:]
)
do {
try handler.perform([faceDetectionRequest])
} catch {
print("Vision ์์ฒญ ์คํจ: \(error)")
}
}
1. ํด์๋ ๋ฎ์ถ๊ธฐ: ๋จธ์ ๋ฌ๋ ์ถ๋ก ์ 640ร480์ผ๋ก๋ ์ถฉ๋ถํ ๊ฒฝ์ฐ ๋ง์.
2. ํ๋ ์ ์คํต: ๋งค ํ๋ ์ ์ฒ๋ฆฌ ๋์ 2~3ํ๋ ์๋ง๋ค ์ฒ๋ฆฌํด๋ ๋ผ.
3. Metal ํ์ฉ: Core Image ํํฐ๋ GPU์์ ์คํ๋๋ Metal ๊ธฐ๋ฐ ๋ ๋๋ง ์ฌ์ฉ.
4. autoreleaseFrequency: ํ ์์ฑ ์
.workItem์ผ๋ก ์ค์ ํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ ํจ์จ์ .
์นด๋ฉ๋ผ ์ฑ์์ ํญํด์ ํฌ์ปค์ค ๋ง์ถ๊ฑฐ๋, ํ์น๋ก ์คํ๋ ๊ธฐ๋ฅ ์์์.
๊ทธ๊ฑฐ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์์๋ณด์! ์๊ฐ๋ณด๋ค ์ฝ๋๊ฐ ๊น๋ํด์ ๋๋ ๊ฑฐ์ผ ๐
// ํญ ์ ์ค์ฒ๋ก ํฌ์ปค์ค ์ค์
@objc func handleTapToFocus(_ gesture: UITapGestureRecognizer) {
let tapPoint = gesture.location(in: view)
// ํ๋ฉด ์ขํ๋ฅผ ์นด๋ฉ๋ผ ์ขํ๋ก ๋ณํ
guard let devicePoint = previewLayer?.captureDevicePointConverted(
fromLayerPoint: tapPoint
) else { return }
focusAt(point: devicePoint)
}
func focusAt(point: CGPoint) {
guard let device = videoDeviceInput?.device else { return }
do {
try device.lockForConfiguration()
// ํฌ์ปค์ค ์ค์
if device.isFocusPointOfInterestSupported {
device.focusPointOfInterest = point
device.focusMode = .autoFocus
}
// ๋
ธ์ถ ์ค์
if device.isExposurePointOfInterestSupported {
device.exposurePointOfInterest = point
device.exposureMode = .autoExpose
}
// ํ์ดํธ๋ฐธ๋ฐ์ค ์๋ ์ค์
if device.isWhiteBalanceModeSupported(.autoWhiteBalance) {
device.whiteBalanceMode = .autoWhiteBalance
}
device.unlockForConfiguration()
} catch {
print("์นด๋ฉ๋ผ ์ค์ ์คํจ: \(error)")
}
}
// ํ์น ์ค ๊ตฌํ
@objc func handlePinchZoom(_ gesture: UIPinchGestureRecognizer) {
guard let device = videoDeviceInput?.device else { return }
let maxZoom = min(device.activeFormat.videoMaxZoomFactor, 10.0)
let currentZoom = device.videoZoomFactor
if gesture.state == .changed {
let newZoom = max(1.0, min(currentZoom * gesture.scale, maxZoom))
do {
try device.lockForConfiguration()
device.videoZoomFactor = newZoom
device.unlockForConfiguration()
} catch {
print("์ค ์ค์ ์คํจ: \(error)")
}
gesture.scale = 1.0 // ์ค์ผ์ผ ๋ฆฌ์
}
}
// ๋
ธ์ถ ๋ณด์ ๊ฐ ์ค์ (-3.0 ~ +3.0 EV)
func setExposureCompensation(_ value: Float) {
guard let device = videoDeviceInput?.device else { return }
let clampedValue = max(
device.minExposureTargetBias,
min(value, device.maxExposureTargetBias)
)
do {
try device.lockForConfiguration()
device.setExposureTargetBias(clampedValue) { _ in
print("๋
ธ์ถ ๋ณด์ ์๋ฃ: \(clampedValue) EV")
}
device.unlockForConfiguration()
} catch {
print("๋
ธ์ถ ์ค์ ์คํจ: \(error)")
}
}
// ์ /ํ๋ฉด ์นด๋ฉ๋ผ ์ ํ
func switchCamera() {
guard let currentInput = videoDeviceInput else { return }
let currentPosition = currentInput.device.position
let newPosition: AVCaptureDevice.Position =
currentPosition == .back ? .front : .back
guard let newDevice = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: newPosition
) else { return }
sessionQueue.async {
do {
let newInput = try AVCaptureDeviceInput(device: newDevice)
self.captureSession.beginConfiguration()
self.captureSession.removeInput(currentInput)
if self.captureSession.canAddInput(newInput) {
self.captureSession.addInput(newInput)
self.videoDeviceInput = newInput
}
self.captureSession.commitConfiguration()
print("์นด๋ฉ๋ผ ์ ํ ์๋ฃ! ๐")
} catch {
print("์นด๋ฉ๋ผ ์ ํ ์คํจ: \(error)")
}
}
}
์ด์ ์ง์ง ์ฌ๋ฐ๋ ๋ถ๋ถ์ด์ผ!
Core Image ํ๋ ์์ํฌ๋ฅผ AVFoundation๊ณผ ์ฐ๋ํ๋ฉด ์ค์๊ฐ ํํฐ๋ฅผ ์ ์ฉํ ์ ์์ด.
Apple์ด ์ ๊ณตํ๋ ๋นํธ์ธ ํํฐ๋ง 200๊ฐ๊ฐ ๋๊ฑฐ๋ ๐ฒ
์ธ์คํ๊ทธ๋จ ํํฐ ๊ฐ์ ๊ฑฐ ์ง์ ๋ง๋ค ์ ์๋ค๊ณ !
import CoreImage
import MetalKit
class FilterCameraViewController: UIViewController {
// Metal ๊ธฐ๋ฐ ๋ ๋๋ง ๋ทฐ (์ฑ๋ฅ ์ต๊ณ !)
private var metalView: MTKView!
private var ciContext: CIContext!
private var commandQueue: MTLCommandQueue!
// ํ์ฌ ์ ์ฉ ์ค์ธ ํํฐ
private var currentFilter: CIFilter?
override func viewDidLoad() {
super.viewDidLoad()
setupMetal()
setupCamera()
}
private func setupMetal() {
guard let device = MTLCreateSystemDefaultDevice() else { return }
metalView = MTKView(frame: view.bounds, device: device)
metalView.framebufferOnly = false
metalView.isPaused = true
metalView.enableSetNeedsDisplay = false
view.insertSubview(metalView, at: 0)
commandQueue = device.makeCommandQueue()
ciContext = CIContext(mtlDevice: device)
}
// ํํฐ ์ ์ฉ ๋ฐ ๋ ๋๋ง
func renderFilteredImage(_ image: CIImage) {
guard let drawable = metalView.currentDrawable,
let commandBuffer = commandQueue.makeCommandBuffer() else { return }
// ํํฐ ์ ์ฉ
var filteredImage = image
if let filter = currentFilter {
filter.setValue(image, forKey: kCIInputImageKey)
filteredImage = filter.outputImage ?? image
}
// Metal drawable์ ๋ ๋๋ง
let renderDestination = CIRenderDestination(
width: Int(metalView.drawableSize.width),
height: Int(metalView.drawableSize.height),
pixelFormat: metalView.colorPixelFormat,
commandBuffer: commandBuffer
) { () -> MTLTexture in
return drawable.texture
}
try? ciContext.startTask(toRender: filteredImage, to: renderDestination)
commandBuffer.present(drawable)
commandBuffer.commit()
}
// ํํฐ ๋ณ๊ฒฝ
func applyFilter(named filterName: String) {
currentFilter = CIFilter(name: filterName)
print("ํํฐ ์ ์ฉ: \(filterName) โจ")
}
}
// ์ธ๊ธฐ ํํฐ ๋ชฉ๋ก ์์
extension FilterCameraViewController {
func applyVintageFilter() {
// ๋นํฐ์ง ํจ๊ณผ (์ฌ๋ฌ ํํฐ ์ฒด์ด๋)
let sepia = CIFilter(name: "CISepiaTone")!
sepia.setValue(0.8, forKey: kCIInputIntensityKey)
currentFilter = sepia
}
func applyBlurFilter(radius: Float = 10.0) {
// ๊ฐ์ฐ์์ ๋ธ๋ฌ
let blur = CIFilter(name: "CIGaussianBlur")!
blur.setValue(radius, forKey: kCIInputRadiusKey)
currentFilter = blur
}
func applyColorInvertFilter() {
// ์์ ๋ฐ์
currentFilter = CIFilter(name: "CIColorInvert")
}
func applyVignetteFilter() {
// ๋น๋คํ
ํจ๊ณผ
let vignette = CIFilter(name: "CIVignette")!
vignette.setValue(1.5, forKey: kCIInputIntensityKey)
vignette.setValue(1.0, forKey: kCIInputRadiusKey)
currentFilter = vignette
}
}
| ํํฐ ์ด๋ฆ | ํจ๊ณผ | ์ฃผ์ ํ๋ผ๋ฏธํฐ |
|---|---|---|
CISepiaTone |
์ธํผ์ ๋นํฐ์ง ํจ๊ณผ | inputIntensity (0~1) |
CIPhotoEffectMono |
ํ๋ฐฑ ๋ณํ | ์์ |
CIGaussianBlur |
๊ฐ์ฐ์์ ๋ธ๋ฌ | inputRadius |
CIVignette |
๋น๋คํ (๊ฐ์ฅ์๋ฆฌ ์ด๋ก๊ฒ) | inputIntensity, inputRadius |
CIColorControls |
๋ฐ๊ธฐ/๋๋น/์ฑ๋ ์กฐ์ | inputBrightness, inputContrast, inputSaturation |
CIPhotoEffectChrome |
ํฌ๋กฌ ํํฐ (์ ๋ช ํ ์๊ฐ) | ์์ |
CIBloom |
๋น ๋ฒ์ง ํจ๊ณผ | inputRadius, inputIntensity |
์ดฌ์ํ ๋น๋์ค๋ฅผ ํธ์งํ๋ ๊ฒ๋ AVFoundation์ผ๋ก ํ ์ ์์ด!
AVAsset, AVMutableComposition์ ์ฌ์ฉํ๋ฉด ํธ๋ฆฌ๋ฐ, ํฉ์น๊ธฐ, ํ
์คํธ ์ค๋ฒ๋ ์ด๊น์ง ๊ฐ๋ฅํด.
์ง์ง ๋ฏธ๋ ์์ ํธ์ง ์ฑ์ ๋ง๋ค ์ ์๋ ๊ฑฐ์ง ๐ฌ
import AVFoundation
func trimVideo(
inputURL: URL,
startTime: Double,
endTime: Double,
completion: @escaping (URL?) -> Void
) {
let asset = AVAsset(url: inputURL)
// ๋ด๋ณด๋ด๊ธฐ ์ธ์
์์ฑ
guard let exportSession = AVAssetExportSession(
asset: asset,
presetName: AVAssetExportPresetHighestQuality
) else {
completion(nil)
return
}
// ์ถ๋ ฅ ํ์ผ ๊ฒฝ๋ก
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("\(UUID().uuidString).mp4")
// ํธ๋ฆฌ๋ฐ ๋ฒ์ ์ค์
let startCMTime = CMTime(seconds: startTime, preferredTimescale: 600)
let endCMTime = CMTime(seconds: endTime, preferredTimescale: 600)
let timeRange = CMTimeRange(start: startCMTime, end: endCMTime)
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.timeRange = timeRange
// ๋ด๋ณด๋ด๊ธฐ ์คํ
exportSession.exportAsynchronously {
switch exportSession.status {
case .completed:
print("ํธ๋ฆฌ๋ฐ ์๋ฃ! โ๏ธ")
completion(outputURL)
case .failed:
print("ํธ๋ฆฌ๋ฐ ์คํจ: \(exportSession.error?.localizedDescription ?? "")")
completion(nil)
case .cancelled:
print("ํธ๋ฆฌ๋ฐ ์ทจ์๋จ")
completion(nil)
default:
break
}
}
}
// ๋น๋์ค ํฉ์น๊ธฐ (Composition)
func mergeVideos(
urls: [URL],
completion: @escaping (URL?) -> Void
) {
let composition = AVMutableComposition()
guard let videoTrack = composition.addMutableTrack(
withMediaType: .video,
preferredTrackID: kCMPersistentTrackID_Invalid
),
let audioTrack = composition.addMutableTrack(
withMediaType: .audio,
preferredTrackID: kCMPersistentTrackID_Invalid
) else { return }
var currentTime = CMTime.zero
for url in urls {
let asset = AVAsset(url: url)
let duration = asset.duration
let timeRange = CMTimeRange(start: .zero, duration: duration)
do {
// ๋น๋์ค ํธ๋ ์ถ๊ฐ
if let assetVideoTrack = asset.tracks(withMediaType: .video).first {
try videoTrack.insertTimeRange(
timeRange,
of: assetVideoTrack,
at: currentTime
)
}
// ์ค๋์ค ํธ๋ ์ถ๊ฐ
if let assetAudioTrack = asset.tracks(withMediaType: .audio).first {
try audioTrack.insertTimeRange(
timeRange,
of: assetAudioTrack,
at: currentTime
)
}
currentTime = CMTimeAdd(currentTime, duration)
} catch {
print("ํธ๋ ์ถ๊ฐ ์คํจ: \(error)")
}
}
// ํฉ์ณ์ง ๋น๋์ค ๋ด๋ณด๋ด๊ธฐ
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("\(UUID().uuidString).mp4")
guard let exportSession = AVAssetExportSession(
asset: composition,
presetName: AVAssetExportPresetHighestQuality
) else { return }
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.exportAsynchronously {
if exportSession.status == .completed {
print("๋น๋์ค ํฉ์น๊ธฐ ์๋ฃ! ๐ฌ")
completion(outputURL)
} else {
completion(nil)
}
}
}
Apple์ ๋งค๋
AVFoundation์ ์
๋ฐ์ดํธํ๊ฑฐ๋ .
์ต์ iOS์์ ์ถ๊ฐ๋ ๊ธฐ๋ฅ๋ค๋ ์์๋๋ฉด ์ข์! ๐ฑ
| ๋ฒ์ | ์ฃผ์ ๊ธฐ๋ฅ |
|---|---|
| iOS 16+ | Cinematic Mode API ๊ณต๊ฐ, AVCaptureDevice.RotationCoordinator ์ถ๊ฐ |
| iOS 17+ | Deferred Photo Delivery, ํฅ์๋ ํฌํธ๋ ์ดํธ ๋ชจ๋ API |
| iOS 18+ | Camera Control API, ํฅ์๋ ProRes ์ง์ |
// iOS 16+ Cinematic Mode ์ง์ ํ์ธ
if #available(iOS 16.0, *) {
// ์๋ค๋งํฑ ๋ชจ๋ ์ง์ ์ฌ๋ถ ํ์ธ
let cinematicFormats = videoDevice.formats.filter { format in
format.isCinematicVideoCaptureSupported
}
if !cinematicFormats.isEmpty {
print("์๋ค๋งํฑ ๋ชจ๋ ์ง์! ๐ฌ")
// ์๋ค๋งํฑ ํฌ๋งท ์ค์
if let cinematicFormat = cinematicFormats.last {
try? videoDevice.lockForConfiguration()
videoDevice.activeFormat = cinematicFormat
videoDevice.unlockForConfiguration()
}
}
}
// iOS 17+ Deferred Photo Delivery
if #available(iOS 17.0, *) {
if photoOutput.isAutoDeferredPhotoDeliverySupported {
photoOutput.isAutoDeferredPhotoDeliveryEnabled = true
print("์ง์ฐ ์ฌ์ง ์ ๋ฌ ํ์ฑํ! ๐ธ")
}
}
์ฌ๋ฅ๋ท(jaenung.net)์์ iOS ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ณผ ์ ์์ด.
๋ค์ํ Swift ๊ฐ๋ฐ์๋ค์ด ์นด๋ฉ๋ผ ์ฑ, ์์ ์ฒ๋ฆฌ ์ฑ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๊ณ ์๊ฑฐ๋ ! ๐
AVFoundation ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ๋ง๋ฅ๋จ๋ฆฌ๋ ์๋ฌ๋ค์ด ์์ด.
๋ฏธ๋ฆฌ ์์๋๋ฉด ์ฝ์ง ์๊ฐ์ ์์ฒญ ์ค์ผ ์ ์์ด! ๐ช
์์ธ: Info.plist์ ๊ถํ ์ค๋ช ํค ๋๋ฝ
ํด๊ฒฐ: NSCameraUsageDescription, NSMicrophoneUsageDescription ์ถ๊ฐ
ํด๊ฒฐ 1: sessionQueue์์ startRunning() ํธ์ถ
์์ธ 2: previewLayer.frame์ด ์ค์ ์ ๋จ
ํด๊ฒฐ 2: viewDidLayoutSubviews์์ frame ์ ๋ฐ์ดํธ
ํด๊ฒฐ: UIImage์ imageOrientation ์ฒ๋ฆฌ ๋๋ fixedOrientation() ๋ฉ์๋ ๊ตฌํ
// ์ด๋ฏธ์ง ํ์ ๋ฌธ์ ํด๊ฒฐ
extension UIImage {
func fixedOrientation() -> UIImage {
if imageOrientation == .up { return self }
UIGraphicsBeginImageContextWithOptions(size, false, scale)
draw(in: CGRect(origin: .zero, size: size))
let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return normalizedImage
}
}
// ์ฌ์ฉ ์์
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else { return }
// ํ์ ๋ณด์ ํ ์ฌ์ฉ
let fixedImage = image.fixedOrientation()
DispatchQueue.main.async {
self.imageView.image = fixedImage
}
}
ํด๊ฒฐ: ์นด๋ฉ๋ผ ๊ธฐ๋ฅ์ ๋ฐ๋์ ์ค์ ๊ธฐ๊ธฐ์์ ํ ์คํธํด์ผ ํด!
Xcode 14.0+์์๋ ์๋ฎฌ๋ ์ดํฐ์์ ์๋ฎฌ๋ ์ด์ ๋ ์นด๋ฉ๋ผ ์ผ๋ถ ์ง์ํ๊ธด ํจ
์นด๋ฉ๋ผ ์ฑ์ ๋ฐฐํฐ๋ฆฌ์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ง์ด ์ฐ๋ ์ฑ์ด์ผ.
์ต์ ํ ์ ํ๋ฉด ๋ฐฐํฐ๋ฆฌ ๋นจ๋ฆฌ ๋ณ๊ณ ์ฑ์ด ๋๋ ค์ง๊ฑฐ๋ .
์ด ํ๋ค ๊ผญ ์ฑ๊ฒจ๊ฐ! ๐
.photo๋ ์ฌ์ง ์ดฌ์์ฉ, .high๋ ๋น๋์ค์ฉ.ํ์ ์ด์์ผ๋ก ๋์ ํ๋ฆฌ์ ์ฐ๋ฉด ๋ฐฐํฐ๋ฆฌ ๋ญ๋น์ผ. ์ฉ๋์ ๋ง๊ฒ ์ ํ!
์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๊ฐ๋ฉด ๋ฐ๋์
captureSession.stopRunning() ํธ์ถ.์ ํ๋ฉด ๋ฐฐํฐ๋ฆฌ ๊ณ์ ์๋ชจ๋๊ณ iOS๊ฐ ์ฑ ๊ฐ์ ์ข ๋ฃ์ํฌ ์ ์์ด.
alwaysDiscardsLateVideoFrames = true ์ค์ ์ผ๋ก ๋ฆ์ ํ๋ ์ ๋๋กญ.์ฒ๋ฆฌ ๋ก์ง์ด ๋ฌด๊ฑฐ์ฐ๋ฉด ๋งค ํ๋ ์ ๋์ Nํ๋ ์๋ง๋ค ์ฒ๋ฆฌํ๋ ๋ฐฉ์ ์ฌ์ฉ.
Core Image ํํฐ ๊ฒฐ๊ณผ๋ฅผ ํ๋ฉด์ ํ์ํ ๋
MTKView ์ฌ์ฉ.UIImageView์ ์ง์ ๋ฃ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ณ ๋ถ๋๋ฌ์.
CMSampleBuffer์ CVPixelBuffer๋ ๋ฉ๋ชจ๋ฆฌ ์ง์ฝ์ ์ด์ผ.์ฒ๋ฆฌ ์๋ฃ ํ ์ฐธ์กฐ๋ฅผ ๋น ๋ฅด๊ฒ ํด์ ํ๊ณ , autorelease pool ํ์ฉ ๊ถ์ฅ.
// ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ธ ํ๋ ์ ์ฒ๋ฆฌ ํจํด
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// autorelease pool๋ก ๋ฉ๋ชจ๋ฆฌ ์ฆ์ ํด์
autoreleasepool {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
// ํ๋ ์ ์ฒ๋ฆฌ ๋ก์ง
processFrame(pixelBuffer)
// pixelBuffer๋ ์ด ๋ธ๋ก ๋์์ ์๋ ํด์
}
}
// ์ธ์
ํ๋ฆฌ์
์ ํ ๊ฐ์ด๋
func selectSessionPreset(for purpose: String) -> AVCaptureSession.Preset {
switch purpose {
case "photo":
return .photo // ์ต๊ณ ํ์ง ์ฌ์ง
case "video_4k":
return .hd4K3840x2160 // 4K ๋น๋์ค (iPhone 6s+)
case "video_1080p":
return .hd1920x1080 // 1080p ๋น๋์ค
case "video_720p":
return .hd1280x720 // 720p ๋น๋์ค
case "realtime":
return .medium // ์ค์๊ฐ ์ฒ๋ฆฌ์ฉ (๋ฎ์ ํด์๋)
default:
return .high
}
}
์, ์ค๋ AVFoundation์ผ๋ก iOS ์นด๋ฉ๋ผ ๊ฐ๋ฐํ๋ ๊ฑฐ ์ ์ฒด์ ์ผ๋ก ํ์ด๋ดค์ด!
์ฒ์์ ๋ณต์กํด ๋ณด์ฌ๋ ๊ตฌ์กฐ๋ฅผ ์ดํดํ๋ฉด ์๊ฐ๋ณด๋ค ์ฒด๊ณ์ ์ด๊ณ ๊น๋ํ๋ค๋ ๊ฑฐ ๋๊ผ์ง? ๐
ํต์ฌ์ ๋ค์ ์ ๋ฆฌํ๋ฉด:
โก ์ค๋ ๋ ๊ด๋ฆฌ: ์ธ์ ์์ ์ sessionQueue, UI๋ ๋ฉ์ธ ์ค๋ ๋
โข ๊ถํ ์ฒ๋ฆฌ: Info.plist ์ค์ + ๋ฐํ์ ๊ถํ ์์ฒญ ํ์
โฃ ์๋ช ์ฃผ๊ธฐ: viewWillAppear/Disappear์์ ์ธ์ ์์/์ค์ง
โค ์ค์๊ฐ ์ฒ๋ฆฌ: VideoDataOutput + Core Image + Metal ์กฐํฉ
โฅ ์ต์ ํ: ์ ์ ํ ํ๋ฆฌ์ , ํ๋ ์ ๋๋กญ ํ์ฉ, Metal ๋ ๋๋ง
AVFoundation์ ์ ๋ง ๊ฐ๋ ฅํ ํ๋ ์์ํฌ์ผ.
์ฌ๊ธฐ์ ๋ฐฐ์ด ๊ธฐ์ด๋ฅผ ๋ฐํ์ผ๋ก Vision, Core ML, ARKit๊ณผ ์ฐ๋ํ๋ฉด
์ง์ง ์์ฒญ๋ ์ฑ๋ค์ ๋ง๋ค ์ ์์ด!
ํผ์ ๊ณต๋ถํ๊ธฐ ๋ง๋งํ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ Swift/iOS ๊ฐ๋ฐ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ.
๋ค์ํ ๊ฐ๋ฐ์๋ค์ด 1:1 ์ฝ๋ ๋ฆฌ๋ทฐ๋ ํ๋ก์ ํธ ๋์์ ์ ๊ณตํ๊ณ ์๊ฑฐ๋ ๐
์, ์ด์ ์ง์ ๋ง๋ค์ด๋ด! ์ฝ๋๋ ์ง์ ์ณ๋ด์ผ ์ค๋ ฅ์ด ๋๋ ๊ฑฐ ์์ง? ใ
ใ
ํ์ดํ
! ๐ช๐ฅ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.