iOS-Swift 音视频采集与文件写入

概述

  • 音视频采集是直播架构的第一步
  • 音视频采集包括两部分
    • 视频采集
    • 音频采集
  • iOS 开发中,同音视频采集相关 API 都封装在 AVFoundation 中,导入该框架,即可实现音频、视频的同步采集

采集步骤

采集步骤文字描述
  • 导入框架
    • 同采集相关 API 在 AVFoundation 中,因此需要先导入框架
  • 创建捕捉会话(AVCaptureSession)
    • 会话:用于连接输入源、输出源
    • 输入源:摄像头、麦克风
    • 输出源:对应的视频、音频数据
  • 设置视频输入源、输出源
    • 输入源(AVCaptureDeviceInput):从摄像头输入(前置/后置)
    • 输出源(AVCaptureVideoDataOutput):可从代理方法中拿到数据
    • 将输入源、输出源添加到会话中
  • 设置音频输入源、输出源
    • 输入源(AVCaptureDeviceInput):从麦克风输入
    • 输出源(AVCaptureAudioDataOutput):可从代理方法中拿到数据
    • 将输入源、输出源添加到会话中
  • 设置预览图层
    • 将摄像头采集的画面添加到屏幕上
      (不添加也可实现采集,但就一般需求来说应该添加)
  • 开始采集
    • 开始采集方法
    • 结束采集方法
    • 切换摄像头等方法
采集步骤代码实现

视频采集部分

import UIKit
import AVFoundation

class ViewController: UIViewController {
    fileprivate lazy var videoQueue = DispatchQueue.global()
    
    fileprivate lazy var session : AVCaptureSession = AVCaptureSession()
    fileprivate lazy var previewLayer : AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)
}
  • 开始视频采集(从故事板拖了几个 button)
@IBAction func startCapture() {
    // 1.创建捕捉会话
    //  let session = AVCaptureSession()
    //  self.session = session

    // 2.设置输入源(摄像头)
    // 2.1.获取摄像头
    guard let devices = AVCaptureDevice.devices(withMediaType:AVMediaTypeVideo) as? [AVCaptureDevice] else {
        print("摄像头不可用")
        return
    }
    guard let device = devices.filter({ $0.position == .front }).first else { return }
    // 2.2.通过 device 创建 AVCaptureInput 对象
    guard let videoInput = try? AVCaptureDeviceInput(device: device) else { return }
    // 2.3.将 input 添加到会话中
    session.addInput(videoInput)

    // 3.设置输出源
    let videoOutput = AVCaptureVideoDataOutput()
    videoOutput.setSampleBufferDelegate(self, queue: videoQueue)
    session.addOutput(videoOutput)

    // 4.设置预览图层
    //  let previewLayer = AVCaptureVideoPreviewLayer(session: session)
    //  previewLayer?.frame = view.bounds
    //  view.layer.addSublayer(previewLayer!)
    previewLayer.frame = view.bounds
    view.layer.insertSublayer(previewLayer, at: 0)

    // 5.开始采集
    session.startRunning()
}
  • 停止采集
@IBAction func stopCapture() {
    // 停止采集
    session.stopRunning()
    previewLayer.removeFromSuperlayer()
    print("停止采集")
}
  • 遵守协议
extension ViewController : AVCaptureVideoDataOutSampleBufferDelegate {
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        // sampleBuffer 就是我们拿到的画面,美颜等操作都是对 sampleBuffer 进行的
        print("已经采集到视频")
    }
}

获取摄像头时,也可以这样遍历

var device : AVCaptureDevice!
for d in devices {
    if d.position == .front {
        device = d
        break
    }
}

或者通过闭包

let device = devices.filter { (device : AVCaptureDevice) -> Bool in
    return device.position == .front
}.first

不过还是推荐第一种,比较简洁,一行代码就搞定了( $0 表示数组内第一个元素)

guard let device = devices.filter({ $0.position == .front }).first else { return }

音频采集部分

  • 先对之前的代码进行一下抽取
extension ViewController {
    @IBAction func startCapture() {
        // 1.设置视频输入、输出
        setupVideo()
        
        // 2.设置音频输入、输出
        setupAudio()

        // 3.设置预览图层
        previewLayer.frame = view.bounds
        view.layer.insertSublayer(previewLayer, at: 0)
        
        // 4.开始采集
        session.startRunning()
    }
    
    @IBAction func stopCapture() {
        // 停止采集
        session.stopRunning()
        previewLayer.removeFromSuperlayer()
        print("停止采集")
    }
}

extension ViewController {
    fileprivate func setupVideo() {
        // 1.设置输入源(摄像头)
        // 1.1.获取摄像头设备
        guard let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as? [AVCaptureDevice] else {
            print("摄像头不可用")
            return
        }
        guard let device = devices.filter({ $0.position == .front }).first else { return }
        // 1.2.通过 device 创建 AVCaptureInput 对象
        guard let videoInput = try? AVCaptureDeviceInput(device: device) else { return }
        // 1.3.将 input 添加到会话中
        session.addInput(videoInput)
        
        // 2.设置输出源
        let videoOutput = AVCaptureVideoDataOutput()
        videoOutput.setSampleBufferDelegate(self, queue: videoQueue)
        session.addOutput(videoOutput)
    }

    fileprivate func setupAudio() {
    }
}
  • 音频采集,也就是对 setupAudio() 的实现
import UIKit
import AVFoundation

class ViewController: UIViewController {
    fileprivate lazy var videoQueue = DispatchQueue.global()
    fileprivate lazy var audioQueue = DispatchQueue.global()
    
    fileprivate lazy var session : AVCaptureSession = AVCaptureSession()
    fileprivate lazy var previewLayer : AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)
}
fileprivate func setupAudio() {
    // 1.设置输入源(麦克风)
    // 1.1.获取麦克风
    guard let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio) else { return }
    // 1.2.根据 device 创建 AVCaptureInput
    guard let audioInput = try? AVCaptureDeviceInput(device: device) else { return }
    // 1.3.将 input 添加到会话中
    session.addInput(audioInput)
    
    // 2.设置输出源
    let audioOutput = AVCaptureAudioDataOutput()
    audioOutput.setSampleBufferDelegate(self, queue: audioQueue)
    session.addOutput(audioOutput)
}
  • 遵守协议
extension ViewController : AVCaptureVideoDataOutSampleBufferDelegate, AVCaptureAudioDataOutSampleBufferDelegate {
    // 获取音频数据的代理方法是一样的
    // 所以为了区分拿到的是视频还是音频数据,我们一般通过 connection 来判断
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) { 
        print("已经采集到音频")
    }
}
  • connection
fileprivate func setupVideo() {
    // 1.设置输入源(摄像头)
    // 1.1.获取摄像头设备
    // 1.2.通过 device 创建 AVCaptureInput 对象
    // 1.3.将 input 添加到会话中
        
    // 2.设置输出源
    
    // 3.获取 video 对应的 connection
    connection = videoOutput.connection(withMediaType: AVMediaTypeVideo)
}

// 因为这的 connection 是个局部变量,在代理方法中拿不到,所以定义一个 connection
class ViewController: UIViewController {
    fileprivate var connection : AVCaptureConnection?
}

  • 遵守协议(设置好 connection 后)
extension ViewController : AVCaptureVideoDataOutSampleBufferDelegate, AVCaptureAudioDataOutSampleBufferDelegate {
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) { 
        if connection == self. connection {
            print("已经采集视频—-video")
        } else {
            print("已经采集音频--audio")
        }
    }
}

切换镜头操作

// 因为切换镜头,需要拿到之前的视频输入源
// 而之前的输入源是局部,切换镜头方法中拿不到,所以定义一个 videoInput
class ViewController: UIViewController {
    fileprivate var videoInput : AVCaptureDeviceInput?
}

// 然后在 setupVideo() 中的 2.2 赋值给 videoInput

// 2.2.通过 device 创建 AVCaptureInput 对象
guard let videoInput = try? AVCaptureDeviceInput(device: device) else { return }
self.videoInput = videoInput

@IBAction func switchScene() {
    // 1.获取当前镜头
    guard var position = videoInput?.device.position else { return }
        
    // 2.获取将要显示镜头
    position = position == .front ? .back : .front
        
    // 3.根据将要显示镜头创建 device
    let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as! [AVCaptureDevice]
    guard let device = devices.filter({ $0.position == position }).first else { return }
        
    // 4.根据 device 创建 input
    guard let videoInput = try? AVCaptureDeviceInput(device: device) else { return }
        
    // 5.在 session 中切换 input
    session.beginConfiguration()
    session.removeInput(self.videoInput!)
    session.addInput(videoInput)
    session.commitConfiguration()
        
    self.videoInput = videoInput
    print("切换镜头")
}

这时运行程序,切换镜头后会发现控制台只打印“已经采集音频--audio”。因为镜头切换,之前获得的 connection 也会改变,所以我们还要进行一个操作,获取新的 connection

fileprivate var connection : AVCaptureConnection?

connection = videoOutput.connection(withMediaType: AVMediaTypeVideo)

然后定义 videoOutput,通过 videoOutput 获取新的 connection

class ViewController: UIViewController {
    fileprivate var videoOutput : AVCaptureVideoDataOutput?
}

// 然后修改 setupVideo() 中的 3 步骤,也就是删除之前获取 connection 的步骤,赋值给 videoOutput

// 3.获取 video 对应的 connection
self.videoOutput = videoOutput
  • 遵守协议(根据 videoOutput 获取 connection 后)
extension ViewController : AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        if connection == videoOutput?.connection(withMediaType: AVMediaTypeVideo) {
            print("已经采集视频—-video")
        } else {
            print("已经采集音频--audio")
        }
    }
}

文件写入部分

  • 定义 movieOutput
class ViewController: UIViewController {
    fileprivate var movieOutput : AVCaptureMovieFileOutput?
}
  • 开始写入文件
@IBAction func startCapture() {
    // 1.设置视频输入、输出
    // 2.设置音频输入、输出
    
    // 3.添加写入文件的 output
    let movieOutput = AVCaptureMovieFileOutput()
    session.addOutput(movieOutput)
    self.movieOutput = movieOutput
    // 设置写入稳定性(不做这一步可能会丢帧)
    let connection = movieOutput.connection(withMediaType: AVMediaTypeVideo)
    connection?.preferredVideoStabilizationMode = .auto

    // 4.设置预览图层
    // 5.开始采集

    // 6.将采集到的画面写入到文件中
    let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/test.mp4"
    let url = URL(fileURLWithPath: path)
    movieOutput.startRecording(toOutputFileURL: url, recordingDelegate: self)
}

  • 停止写入
@IBAction func stopCapture() {
    // 停止写入
    movieOutput?.stopRecording()
    print("停止写入")
    // 停止采集
    session.stopRunning()
    previewLayer.removeFromSuperlayer()
    print("停止采集")
}
  • 遵守代理
extension ViewController : AVCaptureFileOutputRecordingDelegate {
    func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) {
        print("开始写入文件")
    }
    
    func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
        print("结束写入文件")
    }
}

这样就完成了视频的采集,并将视频写入了沙盒。

  • 我好像发了了简书的一个 bug,这篇文章写的时候浏览器崩了好几次,后来时用 macdown 写完粘贴的,好像是大段代码后再写 * 某某某 ,就闪退了。不知道你们出现过这种情况么,我用的是 Chrome😂
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容