美文网首页0
AVFoundation 视频常用套路: 拍照聚焦、曝光、闪光灯

AVFoundation 视频常用套路: 拍照聚焦、曝光、闪光灯

作者: plantAtree_dAp | 来源:发表于2018-09-23 16:05 被阅读52次
    AV

    看图可知, AVFoundation 拍照功能的中心类是 AVCaptureSession 类,管理视频输入输出流。

    音视频,上手就用,直接 AVKit
    更加灵活的控制,就用到 AVFoundation


    要点:

    • 使用资源(一般就是照片库里面的视频,图片,live photo ),
    • 播放,
    • 捕捉(拍照和录视频)
    • 导出资源( 把处理编辑过的资源,拍的照片,编辑的视频,导出到相册)
    • 编辑 (视频合成, 添加动画)
      AVComposition

    AVFoundation , 视频的加载与导出,大量使用异步。
    简单的发消息, 肯定是不行的。阻塞当前线程, 卡顿很久很久。
    AVFoundation 就是为了充分利用64位的硬件和多线程设计的。


    首先是播放,

    播放本地的视频文件, 和远程的视频与流媒体。

    本地文件,单个播放

    先讲 AVKit 里面的 AVPlayerViewController.
    AVPlayerViewController 是 ViewController 的子类,

    AVPlayerViewController

    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 的子类。
    按顺序,播放多个资源。


    AVQueuePlayer

    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 支持后台音效,
    AppdelegatedidFinishLaunchingWithOptions 中添加下面的这段代码,使用后台模式,
    首先在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()
                }
            }
        }
    

    拍照片,下面的代码是静态图,不是 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() 提交了,才生效。
    类似的还有 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 功能。

    // 把屏幕 UI 坐标,转化为预览图层的坐标。
        @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,套路就是配置 session, 添加输入输出, 把视频流的管道打通。
    用 device 作为输入,获取信息,用 session 作为输入输出的桥梁,控制与调度,最后指定我们想要的输出类型。
    拍视频与拍照不同,会有声音,输入源就要加上麦克风了 AVCaptureDevice.default(for: .audio),视频流的输出就要用到 AVCaptureMovieFileOutput 类了。

    拍视频的代码如下:

    func captureMovie() {
            //  首先,做一个确认与切换。当前摄像头不在拍摄中,就拍摄
            guard movieOutput.isRecording == false else {
                print("movieOutput.isRecording\n")
                stopRecording()
                return;
            }
            //  获取视频输出的连接
            let connection = movieOutput.connection(with: .video)
            //    控制连接的方位,视频的横竖屏比例与手机的一致  
            //    点击拍摄按钮拍摄的这一刻,根据当前设备的方向来设置录像的方向
            if (connection?.isVideoOrientationSupported)!{
                connection?.videoOrientation = currentVideoOrientation()
            }
            // 设置连接的视频自动稳定,手机会选择合适的拍摄格式和帧率
            if (connection?.isVideoStabilizationSupported)!{
                connection?.preferredVideoStabilizationMode = AVCaptureVideoStabilizationMode.auto
            }
            
            let device = activeInput.device
            //  因为需要摄像头能够灵敏地聚焦
            if device.isSmoothAutoFocusSupported{
                do{
                    try device.lockForConfiguration()
                    device.isSmoothAutoFocusEnabled = false
                    // 如果设置为 true,   lens movements  镜头移动会慢一些
                    device.unlockForConfiguration()
                }catch{
                    print("Error setting configuration: \(String(describing: error.localizedDescription))")
                }
            }
            let output = URL.tempURL
            movieOutput.startRecording(to: output!, recordingDelegate: self)
        }
    

    与拍照不同,录像使用的是连接, movieOutput.connection(with: .video).

    拍视频,自然会有完成的时候,

    AVCaptureFileOutputRecordingDelegate 类的代理方法里面,保存视频文件,更新 UI

    outputFileURL 参数, 是系统代理完成回调给开发者的,系统把视频文件写入 app 沙盒的资源定位符。要做的是把沙盒里面的视频文件,拷贝到系统相册。

    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
            if let error = error{
                print("Error, recording movie: \(String(describing: error.localizedDescription))")
            }
            else{
                // 保存到相册, 具体代码见 github repo
                saveMovieToLibrary(movieURL: outputFileURL)
                // 更改 UI
                captureButton.setImage(UIImage(named: "Capture_Butt"), for: .normal)
                //  停止计时器
                stopTimer()
            }
        }
    
    
    拍视频的时候,能够知道录的怎么样了,比较好。

    用计时器记录,有一个 Label 展示

    func startTimer(){
            // 销毁旧的
            if updateTimer != nil {
                updateTimer.invalidate()
            }
            //  开启新的
            updateTimer = Timer(timeInterval: 0.5, target: self, selector: #selector(self.updateTimeDisplay), userInfo: nil, repeats: true)
            RunLoop.main.add(updateTimer, forMode: .commonModes)
        }
    

    拍照环境较暗,就要亮灯了,都是调整 AVCaptureDevice 类里的属性。

    拍照用闪光灯, 用 flashMode, 配置 AVCapturePhotoSettings。
    每次拍照,都要新建 AVCapturePhotoSettings.
    拍视频用手电筒, 用 TorchMode, 配置的是 device.torchMode
    直接修改 AVCaptureDevice 的属性
    苹果设计的很好。输出类型决定亮灯模式。
    拍照用闪光灯,是按瞬间动作配置。
    拍视频,就是长亮了。

    // MARK: Flash Modes (Still Photo), 闪光灯
        func setFlashMode(isCancelled: Bool = false) {
            let device = activeInput.device
            // 闪光灯, 只有后置摄像头有。 前置摄像头是,增加屏幕亮度
            if device.isFlashAvailable{
    
                //  这段代码, 就是控制闪光灯的 off, auto , on 三种状态, 来回切换
                var currentMode = currentFlashOrTorchMode().mode
                currentMode += 1
                if currentMode > 2 || isCancelled == true{
                    currentMode = 0
                }
    
                let new_mode = AVCaptureDevice.FlashMode(rawValue: currentMode)
                self.outputSetting.flashMode = new_mode!;
                flashLabel.text = currentFlashOrTorchMode().name
            }
        }
    
    // MARK: Torch Modes (Video), 手电筒
        
        func setTorchMode(isCancelled: Bool = false) {
            let device = activeInput.device
            if device.hasTorch{
    
              //  这段代码, 就是控制手电筒的 off, auto , on 三种状态, 来回切换
                var currentMode = currentFlashOrTorchMode().mode
                currentMode += 1
                if currentMode > 2 || isCancelled == true{
                    currentMode = 0
                }
    
                let new_mode = AVCaptureDevice.TorchMode(rawValue: currentMode)
                if device.isTorchModeSupported(new_mode!){
                    do{
                        // 与前面操作类似,需要 lock 一下
                        try device.lockForConfiguration()
                        device.torchMode = new_mode!
                        device.unlockForConfiguration()
                        flashLabel.text = currentFlashOrTorchMode().name
                        
                    }catch{
                        print("Error setting flash mode: \(String(describing: error.localizedDescription))")
                    }
                    
                }
                
            }
        }
    

    视频合成,将多个音频、视频片段合成为一个视频文件。给视频增加背景音乐

    AVComposition

    合成视频, 操作的就是视频资源, AVAsset . AVAsset 的有一个子类 AVComposition . 一般通过 AVComposition 的子类 AVMutableComposition 合成视频。
    AVComposition 可以把多个资源媒体文件,在时间上自由安排,合成想要的视频。
    具体的就是借助一组音视频轨迹 AVMutableCompositionTrack。

    AVCompositionTrack 包含一组轨迹的片段。AVCompositionTrack 的子类 AVMutableCompositionTrack,可以增删他的轨迹片段,也可以调整轨迹的时间比例。
    拿 AVMutableCompositionTrack 添加视频资源 AVAsset, 为轨迹的片段。

    用 AVPlayer 的实例预览合成的视频资源 AVCompositions, 用 AVAssetExportSession 导出合成的文件。

    预览合成的视频

    套路就是把资源的 URL 封装成 AVAsset。
    拍的视频 AVAsset 包含音频信息(背景音,说话的声音, 单纯的噪音)和视频信息。
    用 用 AVComposition 的子类 AVMutableComposition,添加音轨 composition.addMutableTrack(withMediaType: .audio 和视频轨迹 composition.addMutableTrack(withMediaType: .video

        var previewURL: URL?
        // 记录直接合成的文件地址
    
        @IBAction func previewComposition(_ sender: UIButton) {
            // 首先要合成,
            //  要合成,就得有资源, 并确保当前没有合成的任务正在进行
            guard videoURLs.count > 0 , activityIndicator.isAnimating == false else{
                return
            }
            // 最后就很简单了, 拿资源播放
            var player: AVPlayer!
            defer {
                let playerViewController = AVPlayerViewController()
                playerViewController.allowsPictureInPicturePlayback = true
                playerViewController.player = player
                present(playerViewController, animated: true) {
                    playerViewController.player!.play()
                }
            }
            
            guard previewURL == nil else {
                player = AVPlayer(url: previewURL!)
                return
            }
            //  之前, 没合成写入文件, 就合成预览
            var videoAssets = [AVAsset]() 
            //  有了 视频资源的 URL,  AVMutableComposition 使用的是  AVAsset
            //  拿视频资源的 URL , 逐个创建 AVAsset
            for urlOne in videoURLs{
                let av_asset = AVAsset(url: urlOne)
                videoAssets.append(av_asset)
            }
            // 用 AVComposition 的子类 AVMutableComposition, 来修改合成的轨迹
            let composition = AVMutableComposition()
            //  创建两条轨迹, 音轨轨迹和视频轨迹
            let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
            let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
    
            var startTime = kCMTimeZero
           // 遍历刚才创建的 AVAsset, 放入 AVComposition 添加的音轨和视频轨迹中
            for asset in videoAssets{
                do{
                   // 插入视频轨迹
                    try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .video)[0], at: startTime)
                }catch{
                    print("插入合成视频轨迹, 视频有错误")
                }
                do{
                   // 插入音轨, 
                    try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .audio)[0], at: startTime)
                }catch{
                    print("插入合成视频轨迹, 音频有错误")
                }
                //  让媒体文件一个接一个播放,更新音轨和视频轨迹中的开始时间
                startTime = CMTimeAdd(startTime, asset.duration)
            }
            let playItem = AVPlayerItem(asset: composition)
            player = AVPlayer(playerItem: playItem)
        }
    

    合成视频中,更加精细的控制, 通过 AVMutableVideoCompositionLayerInstruction

    AVMutableVideoCompositionLayerInstruction 这个类, 可以调整合成轨迹的变形(平移和缩放)裁剪和透明度等属性。
    设置 AVMutableVideoCompositionLayerInstruction 一般需要两个参数,
    AVMutableVideoCompositionLayerInstruction 通过轨迹来创建
    let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track).
    通过资源文件 AVAsset 的信息配置。

    一般拍照的屏幕是 375X667 , 相对视频的文件的长度比较小。视频的文件宽度高度, 远超屏幕 1280.0 X 720.0。需要做一个缩小
        func videoCompositionInstructionForTrack(track: AVCompositionTrack, asset: AVAsset) -> AVMutableVideoCompositionLayerInstruction{
            let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
            let assetTrack = asset.tracks(withMediaType: .video)[0]
            // 通过视频文件 asset 的 preferredTransform 属性,了解视频是竖着的,还是横着的,区分处理
            let transfrom = assetTrack.preferredTransform
            //  orientationFromTransform() 方法,见 github repo 
            let assetInfo = transfrom.orientationFromTransform()
            //  为了屏幕能够呈现高清的横向视频
            var scaleToFitRatio = HDVideoSize.width / assetTrack.naturalSize.width
     
            if assetInfo.isPortrait  {
                // 竖向
                scaleToFitRatio = HDVideoSize.height / assetTrack.naturalSize.width
                let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
                let concatTranform = assetTrack.preferredTransform.concatenating(scaleFactor)
                instruction.setTransform(concatTranform, at: kCMTimeZero)
            }
            else{
                //  横向
                let scale_factor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
                let scale_factor_two = CGAffineTransform(rotationAngle: .pi/2.0)
                let concat_transform = assetTrack.preferredTransform.concatenating(scale_factor).concatenating(scale_factor_two)
                instruction.setTransform(concat_transform, at: kCMTimeZero)
            }
            // 将处理好的 AVMutableVideoCompositionLayerInstruction 返回
            return instruction
        }
    
    视频合成,并导出到相册。 这是一个耗时操作

    导出的套路是拿 AVMutableComposition, 创建 AVAssetExportSession, 用 AVAssetExportSession 对象的 exportAsynchronously 方法导出。
    直接写入到相册,对应的 URL 是 session.outputURL

    //  视频合成,并导出到相册。 这是一个耗时操作
        private func mergeAndExportVideo(){
            activityIndicator.isHidden = false
            //  亮一朵菊花, 给用户反馈
            activityIndicator.startAnimating()
            
            //  把记录的 previewURL 置为 nil
            //  视频合成, 导出成功, 就赋新值
            previewURL = nil
            
            // 先创建资源 AVAsset
            var videoAssets = [AVAsset]()
            for url_piece in videoURLs{
                let av_asset = AVAsset(url: url_piece)
                videoAssets.append(av_asset)
            }
            // 创建合成的 AVMutableComposition 对象
            let composition = AVMutableComposition()
            //  创建 AVMutableComposition 对象的音轨
            let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
            
            // 通过 AVMutableVideoCompositionInstruction ,调整合成轨迹的比例、位置、裁剪和透明度等属性。
            // AVMutableVideoCompositionInstruction 对象, 控制一组 layer 对象 AVMutableVideoCompositionLayerInstruction
            let mainInstruction = AVMutableVideoCompositionInstruction()
            var startTime = kCMTimeZero
            // 遍历每一个视频资源,添加到 AVMutableComposition 的音轨和视频轨迹
            for asset in videoAssets{
                //  因为 AVMutableVideoCompositionLayerInstruction 对象适用于整个视频轨迹,
                //  所以这里一个资源,对应一个轨迹
                let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
                do{
                    try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .video)[0], at: startTime)
                }catch{
                    print("Error creating Video track.")
                }
                
                // 有背景音乐,就不添加视频自带的声音了
                if musicAsset == nil {
                    // 插入音频
                    do{
                        try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .audio)[0], at: startTime)
                    }
                    catch{
                        print("Error creating Audio track.")
                    }
                    
                }   
                // 添加了资源,就创建配置文件  AVMutableVideoCompositionLayerInstruction
                let instruction = videoCompositionInstructionForTrack(track: videoTrack!, asset: asset)
                instruction.setOpacity(1.0, at: startTime)
                if asset != videoAssets.last{
                    instruction.setOpacity(0.0, at: CMTimeAdd(startTime, asset.duration))
                    //  视频片段之间, 都添加了过渡, 避免片段之间的干涉
                }
                mainInstruction.layerInstructions.append(instruction)
                // 这样, mainInstruction 就添加好了
                startTime = CMTimeAdd(startTime, asset.duration)
            }
            let totalDuration = startTime
            // 有背景音乐,给合成资源插入音轨
            if musicAsset != nil {
                do{
                    try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, totalDuration), of: musicAsset!.tracks(withMediaType: .audio)[0], at: kCMTimeZero)
                }
                catch{
                    print("Error creating soundtrack total.")
                }
            }
    
            // 设置 mainInstruction 的时间范围
            mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, totalDuration)
    
            //  AVMutableVideoComposition 沿着时间线,设置视频轨迹如何合成
            //  AVMutableVideoComposition 配置了大小、持续时间,合成视频帧的渲染间隔, 渲染尺寸
    
            let videoComposition = AVMutableVideoComposition()
            videoComposition.instructions = [mainInstruction]
            videoComposition.frameDuration = CMTimeMake(1, 30)
            videoComposition.renderSize = HDVideoSize
            videoComposition.renderScale = 1.0
            
            //  拿 composition ,创建 AVAssetExportSession
            let exporter: AVAssetExportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!
            // 配置输出的 url
            exporter.outputURL = uniqueURL
            // 设定输出格式, quick time movie file
            exporter.outputFileType = .mov
            //  优化网络播放
            exporter.shouldOptimizeForNetworkUse = true
            exporter.videoComposition = videoComposition
            // 开启输出会话
            exporter.exportAsynchronously {
                DispatchQueue.main.async {
                    self.exportDidFinish_deng(session: exporter)
                }
            }
        }
    
    

    全部代码见: https://github.com/BoxDengJZ/AVFoundation_ray

    More:


    最后是,关于给视频添加图形覆盖和动画。


    推荐资源:
    AVFoundation Programming Guide 苹果文档

    视频教程

    大佬博客, AVPlayer 本地、网络视频播放相关

    相关文章

      网友评论

        本文标题:AVFoundation 视频常用套路: 拍照聚焦、曝光、闪光灯

        本文链接:https://www.haomeiwen.com/subject/bhcqoftx.html