内容简介:AVFoundation , 视频的加载与导出,大量使用异步。 简单的发消息, 肯定是不行的。阻塞当前线程, 造成卡顿。 AVFoundation 就是为了充分利用64位的硬件和多线程设计的。播放本地的视频文件, 和远程的视频与流媒体。先讲 AVKit 里面的 AVPlayerViewController. AVPlayerViewController 是 ViewController 的子类,
- 使用资源(一般就是照片库里面的视频,图片,live photo),
- 播放,
- 编辑,
- 捕捉(拍照和录视频)
- 导出资源(就处理过的资源,拍的照片,编辑的视频,导出到相册)
AVFoundation , 视频的加载与导出,大量使用异步。 简单的发消息, 肯定是不行的。阻塞当前线程, 造成卡顿。 AVFoundation 就是为了充分利用64位的硬件和多线程设计的。
首先是播放,
播放本地的视频文件, 和远程的视频与流媒体。
本地文件,单个播放
先讲 AVKit 里面的 AVPlayerViewController. AVPlayerViewController 是 ViewController 的子类,
AVPlayerViewController 在 TV OS 上,非常强大。(本文仅介绍 iOS 平台下)
苹果自带的 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 的子类。 按顺序,播放多个资源。
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 中的画中画功能
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: {}) } 复制代码
接着来, 拍照, 设置捕捉的 session ,并实时预览。
设置前后摄像头,聚焦与曝光,拍照(静态图片)
摄像用到的核心类是 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 上面 } 复制代码
拍, startSession
启动视频流的方法,启动了,就不用管。没启动,就处理。 启动视频流是耗时操作,为不阻塞主线程,一般用系统默认线程队列作异步。
let videoQueue = DispatchQueue.global(qos: .default) func startSession(){ if !captureSession.isRunning{ videoQueue.async { self.captureSession.startRunning() } } } 复制代码
拍照片,下面的代码是拍摄静态照片 JPEG,不是 Live Photo.
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))") } } } 复制代码
到自拍了,就是支持前置摄像头,front-facing camera.
首先,要确认手机要有多个摄像头。有多个,才可以切换摄像头输入。 具体套路就是开始配置,修改,与提交修改。
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))") } } 复制代码
聚焦功能 POI : 点击屏幕,拍照聚焦到兴趣点
具体实现是把屏幕 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) } } 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Game Engine Architecture, Second Edition
Jason Gregory / A K Peters/CRC Press / 2014-8-15 / USD 69.95
A 2010 CHOICE outstanding academic title, this updated book covers the theory and practice of game engine software development. It explains practical concepts and techniques used by real game studios,......一起来看看 《Game Engine Architecture, Second Edition》 这本书的介绍吧!