iOS 视频捕获系列Swift之AVFoundation(一)

iOS 视频捕获系列之AVFoundation(一)

AVCaptureMovieFileOutput系列

在iOS开发过程中,或多或少的都涉及视频的操作。
尤其在去年直播行业的带动下,移动端对视频的处理也愈来愈发要求严格。
本文也是在 这篇 中参考而来。

Swift 版本哦!

  • 本文 demo 均可在 GitHub 获得
  • 本篇文章主要是让你学会以AVCaptureMovieFileOutput式的输出
  • 欢迎关注个人 博客 blog.aiyinyu.com

本篇不涉及 :
视频输出质量
帧率设置
具体的设备格式
像素格式
光学防抖
...等等
这些都会在下一篇中带你去认识。如果还不会用,就想了解这么多。就如同还不会走就要跑一样,是要跌大跟头滴!

正文:

在iOS当中对视频捕捉一般是 :

  • UIImagePickerController
  • 或者 AVFoundation

本文主要内容是: AVFoundation

  • AVFoundationUIImagePickerController 的区别在于 在于对视频流的处理,显然前者会更高级一点。
  • AVFoundation中对视频的输出处理 又分为 AVCaptureMovieFileOutputAVAssetWriter 。这里如果想要对视频的输出给出更多的操作那选择 AVAssetWriter 是个不错的选择。
  • 所以这里重点介绍 AVFoundation 更多的区别,还是在代码中体验比较好,说太多都没用。就是
为了更好的查看其关系,下面的图比较直观一点:
t
t

首先我们新建一个工程

并在工程中的 plist 文件中添加访问 权限
Privacy - Camera Usage Description
Privacy - Microphone Usage Description
Privacy - Photo Library Usage Description
Privacy - Media Library Usage Description

先来研究

AVCaptureMovieFileOutput

关于 AVCaptureMovieFileOutput 看上图对号入座

首先新建一个 fileOutputViewController 控制器
控制器上放俩按钮: Close Record
你可以用 storyboard 拖拽也可以用代码实现其点击事件

由上图我们可以看到输出方式有两种 AVCaptureMovieFileOutputAVAssetWriter ,在输出之前他们对视频的操作是一样的,所以我们可以把 它俩公共的部分抽象出来一个类,对使用不同的输出方式进行继承这个类就 OK

相同的部分抽象成 一个继承 NSObjectCaptureSessionCoordinator 公共类
该公共类不对采集后的视频不做输出处理,因为输出有两种不同的处理结果。
每一种处理正是其继承 CaptureSessionCoordinator 类的 子类完成其处理

公共类:CaptureSessionCoordinator

AVCaptureSession 类进行处理,相关属性如下:

AVCaptureSession
AVCaptureDevice
代理
视图

如下:

因为使用到线程,故对资源的加锁问题,在 Swift 中没法直接向 Oc 那样直接使用: synchronized 故在此利用闭包的特性达到相同的效果:

如何使用看文中代码

func synchronized(_ lock: AnyObject,dispose: ()->()) {
    objc_sync_enter(lock)
    dispose()
    objc_sync_exit(lock)
}

由于对视频的处理都不是在主控制器fileOutputViewController里面执行的。故,对视频的输出都是需要代理来回调到控制器里面执行后续的相关操作。

所以这里需要一个代理:

protocol captureSessionCoordinatorDelegate: class {
    func coordinatorDidBeginRecording(coordinator: CaptureSessionCoordinator)
    func didFinishRecording(coordinator: CaptureSessionCoordinator)
}

上面的铺垫后,下面开始对 AVCaptureSession 进行相应的操作:

以我们的常识,该类中必须有这些方法:

  • 开始运行 startRunning

  • 结束运行 stopRunning

  • 开始记录 startRecording

  • 结束记录 stopRecording

  • 初始化初始化 AVCaptureVideoPreviewLayer

其他的方法可以在初始中进行,也可以进行模块化拆分
该类一个完整的代码如下:

class CaptureSessionCoordinator: NSObject {
    
    var captureSession: AVCaptureSession?
    var cameraDevice: AVCaptureDevice?
    var delegateCallQueue: DispatchQueue?
    weak var delegate: captureSessionCoordinatorDelegate?
    
    private var sessionQueue = DispatchQueue(label: "coordinator.Session")
    private var previewLayer: AVCaptureVideoPreviewLayer?
    
    override init() {
        super.init()
        captureSession = setupCaptureSession()
    }
    
    public func setDelegate(capDelegate: captureSessionCoordinatorDelegate,queue: DispatchQueue) {
        synchronized(self) {
            delegate = capDelegate
            if delegateCallQueue != queue {
                delegateCallQueue = queue
            }
        }
    }
    
//MARK:            ________________Session Setup________________
    private func setupCaptureSession() -> AVCaptureSession {
        let session = AVCaptureSession()
        if !addDefaultCameraInputToCaptureSession(capSession: session) {
            printLogDebug("failed to add camera input to capture session")
        }
        if addDefaultMicInputToCaptureSession(capSession: session) {
            printLogDebug("failed to add mic input to capture session")
        }
        return session
    }
    
    private func addDefaultCameraInputToCaptureSession(capSession: AVCaptureSession) -> Bool {
        do {
            let cameraInput = try AVCaptureDeviceInput(device: AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo))
            let success = addInput(input: cameraInput, capSession: capSession)
            cameraDevice = cameraInput.device
            return success
        } catch let error as NSError {
            printLogDebug("error configuring camera input: \(error.localizedDescription)")
            return false
        }
    }
    
    private func addDefaultMicInputToCaptureSession(capSession: AVCaptureSession) -> Bool {
        do {
            let micInput = try AVCaptureDeviceInput(device: AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio))
            let success = addInput(input: micInput, capSession: capSession)
            return success
        } catch let error as NSError {
            printLogDebug("error configuring mic input: \(error.localizedDescription)")
            return false
        }
    }
    
    //MARK:            ________________Public Api________________
    
    func addInput(input: AVCaptureDeviceInput,capSession: AVCaptureSession) -> Bool {
        if capSession.canAddInput(input) {
            capSession.addInput(input)
            return true
        }
        printLogDebug("input error")
        return false
    }
    func addOutput(output: AVCaptureOutput,capSession: AVCaptureSession) -> Bool {
        if capSession.canAddOutput(output) {
            capSession.addOutput(output)
            return true
        }
        printLogDebug("output error")
        return false
    }
    func startRunning() {
        sessionQueue.async {
            self.captureSession?.startRunning()
        }
    }
    func stopRunning() {
        sessionQueue.async {
            self.stopRunning()
            self.captureSession?.stopRunning()
        }
    }
    func startRecording() {
        // 子类继承后重写
    }
    func stopRecording() {
        // 子类继承后重写
    }
}

我们先来: AVCaptureMovieFileOutput

我们创建以 AVCaptureMovieFileOutput 方式输出并继承 CaptureSessionCoordinator 的类:fileOutputCoordinator

由最上面的大图可知,AVFoundation输出有两种:AVCaptureMovieFileOutputAVAssetWriter
AVCaptureMovieFileOutput 是对输出流没有做太多的处理,以AVCaptureMovieFileOutput方式进行视频输出处理的类,不需要太多的处理。
故继承 CaptureSessionCoordinator它的fileOutputCoordinator子类只需如下:
重点便是对输出的处理

class fileOutputCoordinator: CaptureSessionCoordinator,AVCaptureFileOutputRecordingDelegate {
    
    var movieFileOutput: AVCaptureMovieFileOutput?
    
    override init() {
        super.init()
        movieFileOutput = AVCaptureMovieFileOutput()
        _ = addOutput(output: movieFileOutput!, capSession: captureSession!)
    }
    override func startRecording() {
        let fm = YfileManager()
        let tempUrl = fm.tempFileUrl()
        movieFileOutput?.startRecording(toOutputFileURL: tempUrl, recordingDelegate: self)
    }
    override func stopRecording() {
        movieFileOutput?.stopRecording()
    }
    
    func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
        delegate?.didFinishRecording(coordinator: self, url: outputFileURL)
    }
    
    func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) {
        delegate?.coordinatorDidBeginRecording(coordinator: self)
    }
}

上面代码中有一个对文件处理的路径操作类:YfileManager

它主要就是对文件路径的操作,与临时文件存储到系统相册中的操作:以上代码中牵扯到的只有如下:

class YfileManager: NSObject {

    func tempFileUrl() -> URL {
        var path: String = ""
        let fm = FileManager.default
        var i: Int = 0
        while path.isEmpty || fm.fileExists(atPath: path) {
            path = NSTemporaryDirectory() + "output\(i.description).mov"
            i += 1
        }
        return URL(fileURLWithPath: path)
    }
/// 对临时视频文件的存储操作,本方法在iOS9以后被遗弃了
    func copFileToCameraRoll(fileUrl: URL) {
        let library = ALAssetsLibrary()
        if !library.videoAtPathIs(compatibleWithSavedPhotosAlbum: fileUrl) {
            printLogDebug("video error")
        }
        library.writeVideoAtPath(toSavedPhotosAlbum: fileUrl) { (url, error) in
            if (error != nil) {
                printLogDebug("error: \(error?.localizedDescription)")
            } else if url == nil {
                printLogDebug("url is empty")
            }
        }
    }
}
到目前为止涉及非控制器(fileOutputViewController)的代码全部完成,接下来我们来到控制器执行相关的操作

实现fileOutputViewController控制器的方法

首当其冲的是相机视图与执行代理的方法:captureSessionCoordinatorDelegate
相关变量:

    @IBOutlet weak var recordButton: UIBarButtonItem!
    var captureSessionCoordinator: fileOutputCoordinator?
    var recording: Bool = false
    var dismissing: Bool = false

控制器具体代码:

class fileOutputViewController: UIViewController,captureSessionCoordinatorDelegate {

    @IBOutlet weak var recordButton: UIBarButtonItem!
    var captureSessionCoordinator: fileOutputCoordinator?
    var recording: Bool = false
    var dismissing: Bool = false

    override func viewDidLoad() {
        super.viewDidLoad()
        captureSessionCoordinator = fileOutputCoordinator()
        captureSessionCoordinator?.setDelegate(capDelegate: self, queue: DispatchQueue(label: "fileOutputCoordinator"))
        confiureCamper()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    ///  关闭当前视图
    @IBAction func closeCameral(_ sender: Any) {
        if recording {
            dismissing = true
        } else {
            stopPipelineAndDismiss()
        }
    }
    ///  开始记录 与停止记录
    @IBAction func recording(_ sender: Any) {
        if recording {
            captureSessionCoordinator?.stopRecording()
        } else {
            UIApplication.shared.isIdleTimerDisabled = true
        }
        recordButton.isEnabled = false
        recordButton.title = "Stop"
        captureSessionCoordinator?.startRecording()
        recording = true
    }
    
    func confiureCamper() {
        let cameraViewlayer = captureSessionCoordinator?.previewLayerSetting()
        cameraViewlayer?.frame = view.bounds
        view.layer.insertSublayer(cameraViewlayer!, at: 0)
        captureSessionCoordinator?.startRunning()
        
    }
    func stopPipelineAndDismiss() {
        captureSessionCoordinator?.stopRunning()
        dismiss(animated: true, completion: nil)
        dismissing = false
    }
    func coordinatorDidBeginRecording(coordinator: CaptureSessionCoordinator) {
        recordButton.isEnabled = true
    }
    func didFinishRecording(coordinator: CaptureSessionCoordinator, url: URL) {
        UIApplication.shared.isIdleTimerDisabled = false
        recordButton.title = "Record"
        recording = false
        let fm = YfileManager()
        fm.copFileToCameraRoll(fileUrl: url)
        if dismissing {
            stopPipelineAndDismiss()
        }
    }

}

目前为止:到目前为止一个完整的

AVCaptureMovieFileOutput 类型的输出完成

如果你在真机设备调试过程中想查看临时的 tem文件,或者沙河的文件。那就接着如下看:

Xcode上面的导航栏 ->Window->Devices->点击你的设备->找到右下的installed Apps->点击你的要看的项目->点击+ -右边图标->Download下载到桌面即可 然后选择显示包内容就可以看到当前沙盒文件的状态啦!
如图:

如图
如图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容