资讯专栏INFORMATION COLUMN

AVFoundation 视频常用套路: 视频合成与导出,拍视频手电筒,拍照闪光灯

陈江龙 / 2514人阅读

摘要:每次拍照,都要新建具有原子性拍视频用手电筒,用配置的是直接修改的属性苹果设计的很好。拍照用闪光灯,是按瞬间动作配置。直接写入到相册,对应的是视频合成,并导出到相册。相关拍照聚焦和曝光,简明教程推荐资源苹果文档视频教程

拍视频,把视频文件导出到相册
-

处理 AVFoundation,套路就是配置 session, 添加输入输出, 把视频流的管道打通。
用 device 作为输入,获取信息,用 session 作为输入输出的桥梁,控制与调度,最后指定我们想要的输出类型。
拍视频与拍照不同,会有声音,输入源就要加上麦克风了 AVCaptureDevice.default(for: .audio),视频流的输出就要用到 AVCaptureMovieFileOutput 类了。

拍视频的代码如下:

func captureMovie() {
        //  首先,做一个确认与切换。当前摄像头不在拍摄中,就拍摄
        guard movieOutput.isRecording == false else {
            print("movieOutput.isRecording
")
            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.AVCapturePhotoSettings 具有原子性 atomic.

拍视频用手电筒, 用 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))")
                }
                
            }
            
        }
    }

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

合成视频, 操作的就是视频资源, 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/... More:

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

相关:
拍照聚焦和曝光,AVFoundation 简明教程


推荐资源:

WWDC 2016: Advances in iOS Photography

AVFoundation Programming Guide 苹果文档

视频教程

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/17184.html

相关文章

  • AVKit 播放(AVFoundation, AVKit, 音视频, Swift 4, 配代码)

    摘要:播放视频,苹果设计的很简单,代码如下拿一个建立一个实例你的再建立一个实例这里有一个闭包,出现了,再播放。苹果文档上说,用于管理播放器播放的资源的计时和呈现状态。推荐资源苹果文档视频教程大佬博客,本地网络视频播放相关 音视频,简单点,上手就用,当然是 AVKit.更加灵活的控制,就要用到 AVFoundation 了。 要点: 使用资源(一般就是照片库里面的视频,图片,live ph...

    Jenny_Tong 评论0 收藏0
  • 腾讯云被索赔一亿,“她”是碰瓷还是维权?

    摘要:并且腾讯云具体提到了重要的几条一人脸融合服务接口是针对所有伙伴进行升级的,没有针对她拍进行技术打击。三腾讯云一贯重视客户数据的隐私与安全。她拍正式在深圳及北京两地中级人民法院告状腾讯云违约,索赔一个亿。之前,我看到一个笑话:说的是现在的碰瓷技术日渐高涨,昨晚在小区门口一个老奶奶让我帮她拍照,刚拍一张她就一把拽住我,哭着喊着:你的闪光灯太闪,闪坏了我的腰!笑话归笑话,现在一个叫女性短视频社区她...

    leap_frog 评论0 收藏0
  • 视频直播技术之iOS端推流

    摘要:网易云信推出一系列文章,对视频直播技术进行深入讲解,本篇文章将向大家介绍端的推流技术。目前视频的采集源主要来自摄像头采集屏幕录制从视频文件读取推流。音视频处理前处理模块也是主观影响主播观看效果最主要的环节。音视频发送推流使用的流媒体协议是。 随着网络基础建设的发展和资费的下降,在这个内容消费升级的时代,文字、图片无法满足人们对视觉的需求,因此视频直播应运而生。承载了实时性Real-Ti...

    Youngdze 评论0 收藏0
  • iOS 音视频学习 - 视频录制阶段

    摘要:前言伴随着大火的短视频应用,正好自己也有点时间,就稍微学习了一下视频相关的内容。视频录制我上网看了很多的文章,总结起来实现视频的录制有三种方法可以用。还有在写入文件时,视频和音频是分开处理的。 前言 伴随着大火的短视频应用,正好自己也有点时间,就稍微学习了一下视频相关的内容。 这种多媒体技术并没有想象的那么简单,这算是一个技术方向了。我把这些视频相关的技术分为了两部分,暂且叫做应用层面...

    taohonghui 评论0 收藏0
  • iOS 音视频学习 - 视频录制阶段

    摘要:前言伴随着大火的短视频应用,正好自己也有点时间,就稍微学习了一下视频相关的内容。视频录制我上网看了很多的文章,总结起来实现视频的录制有三种方法可以用。还有在写入文件时,视频和音频是分开处理的。 前言 伴随着大火的短视频应用,正好自己也有点时间,就稍微学习了一下视频相关的内容。 这种多媒体技术并没有想象的那么简单,这算是一个技术方向了。我把这些视频相关的技术分为了两部分,暂且叫做应用层面...

    wwq0327 评论0 收藏0

发表评论

0条评论

陈江龙

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<