一段废话
最近有点懒,好久没写东西了,学习了一下音视频采集,这里简单的做下记录,现学现卖
概述
- 音视频采集是直播架构的第一步
- 音视频采集包括两部分
- 视频采集
- 音频采集
- 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