直接看拍照聚焦和曝光html
上手就用 AVKit 搞音視頻。靈活的控制,就要用到 AVFoundationgit
AVFoundation , 視頻的加載與導出,大量使用異步。 簡單的發消息, 確定是不行的。阻塞當前線程, 形成卡頓。 AVFoundation 就是爲了充分利用64位的硬件和多線程設計的。github
播放本地的視頻文件, 和遠程的視頻與流媒體。bash
先講 AVKit 裏面的 AVPlayerViewController. AVPlayerViewController 是 ViewController 的子類,網絡
AVPlayerViewController 在 TV OS 上,很是強大。(本文僅介紹 iOS 平臺下)session
蘋果自帶的 AVPlayerViewController 裏面有不少播放的控件。 回播中,就是播放本地文件中,能夠播放、暫停、快進、快退,調整視頻的長寬比例( 即畫面在屏幕中適中,或者鋪滿屏幕)。多線程
播放視頻,蘋果設計的很簡單,代碼以下:閉包
// 拿一個 url , 創建一個 AVPlayer 實例
let player = AVPlayer(url: "你的 url")
// 再創建一個 AVPlayerViewController 實例
let playerViewController = AVPlayerViewController()
playerViewController.player = queuePlayer
present(playerViewController, animated: true) {
playerViewController.player!.play()
}// 這裏有一個閉包, 界面出現了,再播放。
複製代碼
連着放,使用 AVQueuePlayer,把多個視頻放在一個視頻隊列中,依次連續播放 AVQueuePlayer 是 AVPlayer 的子類。 按順序,播放多個資源。 app
AVPlayerItem 包含不少視頻資源信息,除了資源定位 URI , 還有軌跡信息,視頻的持續時長等。異步
蘋果文檔上說, AVPlayerItem 用於管理播放器播放的資源的計時和呈現狀態。他有一個 AVAsset 播放資源的屬性。
var queue = [AVPlayerItem]()
let videoClip = AVPlayerItem(url: url)
queue.append(videoClip)
// queue 隊列能夠繼續添加 AVPlayerItem 實例
let queuePlayer = AVQueuePlayer(items: queue)
let playerViewController = AVPlayerViewController()
playerViewController.player = queuePlayer
present(playerViewController, animated: true) {
playerViewController.player!.play()
}
複製代碼
iPad 中的畫中畫功能,經過給 AVAudioSession 支持後臺音效, 在 Appdelegate
的 didFinishLaunchingWithOptions
中添加下面的這段代碼,使用後臺模式, 首先在Xcode 的 target 的 Capability 中勾選相關的後臺功能。
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayback)
try session.setActive(true)
} catch let error {
print("AVFoundation configuration error: \(error.localizedDescription) \n\n AV 配置 有問題")
}
// 頗有必要這樣,由於畫中畫的視頻功能,apple 是當後臺任務處理的。
複製代碼
本地的資源路徑 URL ,替換爲網絡的 URL, 就能夠了。
override func viewDidLoad() {
super.viewDidLoad()
// 添加播放完成的監聽
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
}
// 執行退出的界面控制
@objc func playerItemDidReachEnd(){
self.presentedViewController?.dismiss(animated: true, completion: {})
}
複製代碼
設置先後攝像頭,聚焦與曝光,拍照(靜態圖片)
攝像用到的核心類是 AVCaptureSession ,應用和 iOS 創建一個視頻流的會話。 AVCaptureSession 做爲調度中心, 控制設備的輸入/輸出流, 具體就是相機和麥克風。
AVCaptureDeviceInput 類是視頻流的輸入源,預覽界面呈現的就是他的數據,導出的視頻文件也是他負責的。 視頻流 session 對象生成後,能夠從新配置。這就能夠動態修改視頻流 session 的配置信息。視頻流 session 的輸入輸出的路由,也能夠動態改。例如,只須要一個 session. 能夠經過 AVCapturePhotoOutput 導出照片,能夠導出視頻文件 AVCaptureMovieFileOutput.
captureSession.startRunning() 以前,先要添加輸入 AVCaptureDeviceInput 和輸出 AVCapturePhotoOutput/AVCaptureMovieFileOutput,準備預覽界面 AVCaptureVideoPreviewLayer
// 有一個 captureSession 對象
let captureSession = AVCaptureSession()
// 兩個輸出,輸出照片, 和輸出視頻
let imageOutput = AVCapturePhotoOutput()
let movieOutput = AVCaptureMovieFileOutput()
func setupSession() -> Bool{
captureSession.sessionPreset = AVCaptureSession.Preset.high
// 首先設置 session 的分辨率 。sessionPreset 屬性,設置了輸出的視頻的質量
let camera = AVCaptureDevice.default(for: .video)
// 默認的相機是 back-facing camera 朝前方拍攝, 不是自拍的。
do {
let input = try AVCaptureDeviceInput(device: camera!)
if captureSession.canAddInput(input){
captureSession.addInput(input)
activeInput = input
// 添加拍照, 錄像的輸入
}
} catch {
print("Error settings device input: \(error)")
return false
}
// 設置麥克風
let microphone = AVCaptureDevice.default(for: .audio)
do{
let micInput = try AVCaptureDeviceInput(device: microphone!)
if captureSession.canAddInput(micInput){
captureSession.addInput(micInput)
// 添加麥克風的輸入
}
}catch{
print("Error setting device audio input: \(String(describing: error.localizedDescription))")
fatalError("Mic")
}
// 添加兩個輸出,輸出照片, 和輸出視頻
if captureSession.canAddOutput(imageOutput){
captureSession.addOutput(imageOutput)
}
if captureSession.canAddOutput(movieOutput){
captureSession.addOutput(movieOutput)
}
return true
}
複製代碼
AVCaptureVideoPreviewLayer 是 CALayer 的子類,用於展現相機拍的界面。
func setupPreview() {
// 配置預覽界面 previewLayer
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
// previewLayeris 經過 captureSession 初始化
// 再設置相關屬性, 尺寸和視頻播放時的拉伸方式 videoGravity
previewLayer.frame = camPreview.bounds
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
camPreview.layer.addSublayer(previewLayer)
// camPreview 是一個 UIView ,鋪在 self.view 上面
}
複製代碼
啓動視頻流的方法,啓動了,就不用管。沒啓動,就處理。 啓動視頻流是耗時操做,爲不阻塞主線程,通常用系統默認線程隊列做異步。
let videoQueue = DispatchQueue.global(qos: .default)
func startSession(){
if !captureSession.isRunning{
videoQueue.async {
self.captureSession.startRunning()
}
}
}
複製代碼
var outputSetting = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
// 靜態圖的配置
func capturePhoto() {
guard PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized else{
PHPhotoLibrary.requestAuthorization(requestAuthorizationHander)
return
}
let settings = AVCapturePhotoSettings(from: outputSetting)
imageOutput.capturePhoto(with: settings, delegate: self)
// imageOutput 輸出流裏面的採樣緩衝中,捕獲出靜態圖
}
extension ViewController: AVCapturePhotoCaptureDelegate{
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
// 若是視頻流的採樣緩衝裏面有數據,就拆包
if let imageData = photo.fileDataRepresentation(){
let image = UIImage(data: imageData)
let photoBomb = image?.penguinPhotoBomb(image: image!)
self.savePhotoToLibrary(image: photoBomb!)
// 最後,合成照片保存到系統相冊
// 這裏有一個照片合成,具體見下面的 Github Repo.
}
else{
print("Error capturing photo: \(String(describing: error?.localizedDescription))")
}
}
}
複製代碼
首先,要確認手機要有多個攝像頭。有多個,才能夠切換攝像頭輸入。 具體套路就是開始配置,修改,與提交修改。
captureSession.beginConfiguration() ,接着寫修改,直到 captureSession.commitConfiguration() 提交了,才生效。
相似的還有 UIView 渲染機制。 CATransaction, 開始,設置,提交,就能夠在屏幕上看到刷新的界面了。
// 配置拍攝前面(自拍),拍後面
@IBAction func switchCameras(_ sender: UIButton) {
guard movieOutput.isRecording == false else{
return
}
// 確認手機要有多個攝像頭
guard let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front), let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else{
return;
}
// 建立新的 AVCaptureDeviceInput ,來切換。更新 captureSession 的配置。
do{
var input: AVCaptureDeviceInput?
// 經過識別當前的攝像頭,找出另外一個(咱們須要的)
if activeInput.device == frontCamera{
input = try AVCaptureDeviceInput(device: backCamera)
}
else{
input = try AVCaptureDeviceInput(device: frontCamera)
}
// 獲得了新的輸入源,就能夠開始配置了
captureSession.beginConfiguration()
// 去掉舊的輸入源,即不讓當前的攝像頭輸入
captureSession.removeInput(activeInput)
// 增長新的輸入源,即讓其餘的攝像頭輸入
if captureSession.canAddInput(input!){
captureSession.addInput(input!)
activeInput = input
}
// captureSession.beginConfiguration() 以後,就開始修改,直到下一句提交了,才生效。
captureSession.commitConfiguration()
}catch{
print("Error , switching cameras: \(String(describing: error))")
}
}
複製代碼
具體實現是把屏幕 UI 座標,也就是預覽圖層的座標,轉換到相機的座標系中, 再用預覽圖層的座標點,設置聚焦的 point 和 mode 。
配置聚焦,屬於用戶輸入,並要用到手機的攝像頭硬件。配置 POI 的時候,可能有干擾 ( 好比後臺進程的影響 ),這樣就要用設備鎖定了。 device.lockForConfiguration()
注意: 自拍是不能夠聚焦的。前置攝像頭,沒有 POI 功能。
@objc
func tapToFocus(recognizer: UIGestureRecognizer){
if activeInput.device.isFocusPointOfInterestSupported{
// 獲得屏幕中點擊的座標,轉化爲預覽圖層裏的座標點
let point = recognizer.location(in: camPreview)
// 將預覽圖層中的座標點,轉換到相機的座標系中
let pointOfInterest = previewLayer.captureDevicePointConverted(fromLayerPoint: point)
// 自由設置相關 UI
showMarkerAtPoint(point: point, marker: focusMarker)
focusAtPoint(pointOfInterest)
}
}
// 用預覽圖層的座標點,配置聚焦。
func focusAtPoint(_ point: CGPoint){
let device = activeInput.device
// 首先判斷手機能不能聚焦
if device.isFocusPointOfInterestSupported , device.isFocusModeSupported(.autoFocus){
do{
// 鎖定設備來配置
try device.lockForConfiguration()
device.focusPointOfInterest = point
device.focusMode = .autoFocus
device.unlockForConfiguration()
// 配置完成,解除鎖定
}
catch{
print("Error focusing on POI: \(String(describing: error.localizedDescription))")
}
}
}
複製代碼
相似聚焦,具體實現是把屏幕 UI 座標,也就是預覽圖層的座標,轉換到相機的座標系中, 再用預覽圖層的座標點,設置曝光的 point 和 mode 。 同聚焦不同,曝光要改兩次 mode.
mode 從默認鎖定的 .locked 到選定座標點的連續自動曝光 .continuousAutoExposure, 最後系統調好了,再切換回默認的鎖定 .locked 。 由於不知道系統何時連續自動曝光處理好,因此要用到 KVO. 監聽 activeInput.device 的 adjustingExposure 屬性。 當曝光調節結束了,就鎖定曝光模式。
( 調用時機挺好的, 雙擊屏幕,手機攝像頭自動曝光的時候,就防止干擾。曝光完成後,立刻改曝光模式爲鎖定 。這樣就不會總是處在曝光中。)
(這個有點像監聽鍵盤,那裏通常用系統通知。)
配置曝光,屬於用戶輸入,並要用到手機的攝像頭硬件。配置曝光的時候,可能有干擾 ( 好比後臺進程的影響 ),這樣就要用鎖了。 device.lockForConfiguration()
其餘: 自拍是有曝光效果的
// 單指雙擊,設置曝光, 更多見下面的 github repo
@objc
func tapToExpose(recognizer: UIGestureRecognizer){
if activeInput.device.isExposurePointOfInterestSupported{
// 與聚焦同樣,獲得屏幕中點擊的座標,轉化爲預覽圖層裏的座標點
let point = recognizer.location(in: camPreview)
// 將預覽圖層中的座標點,轉換到相機的座標系中
let pointOfInterest = previewLayer.captureDevicePointConverted(fromLayerPoint: point)
showMarkerAtPoint(point: point, marker: exposureMarker)
exposeAtPoint(pointOfInterest)
}
}
private var adjustingExposureContext: String = "Exposure"
private let kExposure = "adjustingExposure"
func exposeAtPoint(_ point: CGPoint){
let device = activeInput.device
if device.isExposurePointOfInterestSupported, device.isFocusModeSupported(.continuousAutoFocus){
do{
try device.lockForConfiguration()
device.exposurePointOfInterest = point
device.exposureMode = .continuousAutoExposure
// 先判斷手機,能不能鎖定曝光。能夠就監聽手機攝像頭的調整曝光屬性
if device.isFocusModeSupported(.locked){
// 同聚焦不同,曝光要改兩次 mode.
// 這裏有一個不受控制的耗時操做( 不清楚何時系統處理好),須要用到 KVO
device.addObserver(self, forKeyPath: kExposure, options: .new, context: &adjustingExposureContext)
// 變化好了, 操做結束
device.unlockForConfiguration()
}
}
catch{
print("Error Exposing on POI: \(String(describing: error.localizedDescription))")
}
}
}
// 使用 KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// 先確認,監聽到的是指定的上下文
if context == &adjustingExposureContext {
let device = object as! AVCaptureDevice
// 若是手機攝像頭不處於曝光調整中,也就是完成曝光了,就能夠處理了
if !device.isAdjustingExposure , device.isExposureModeSupported(.locked){
// 觀察屬性,變化了, 一次性注入調用, 就銷燬 KVO
// 而後到主隊列中異步配置
device.removeObserver(self, forKeyPath: kExposure, context: &adjustingExposureContext)
DispatchQueue.main.async {
do{
// 完成後,將曝光狀態復原
try device.lockForConfiguration()
device.exposureMode = .locked
device.unlockForConfiguration()
}
catch{
print("Error exposing on POI: \(String(describing: error.localizedDescription))")
}
}
}
}
else{
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
複製代碼
AVFoundation 視頻經常使用套路: 視頻合成與導出,拍視頻手電筒,拍照閃光燈
推薦資源:
WWDC 2016: Advances in iOS Photography