直接看拍照聚焦和曝光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