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

你可能感兴趣的:(iOS-Swift 音视频采集与文件写入)