草庐IT

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

哎呀我Qu 2024-05-05 原文

概述

  • 音视频采集是直播架构的第一步
  • 音视频采集包括两部分
    • 视频采集
    • 音频采集
  • 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 音视频采集与文件写入的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐