iOS开发Swift

iPhone端录像 利用AVAssetWriter生成H264文件Demo

iPhone利用AVFoundation录像时,Default生成H265的视频文件,这个文件只在Safari浏览器中能直接解码,如果想要在谷歌,火狐浏览器直接播放的话,需要转成H264的编码,IOS提供了AVAssetWriter可以直接生成H264的视频文件。

Demo1:以下是AVAssetWriterDemo:https://github.com/yinweisu/AVAssetWriterDemo

这个Demo没有捕捉音频,没有声音,可以看第二个Demo。

  • ViewController.swift
//
//  ViewController.swift
//  AVAssetWriterDemo
//
//  Created by Weisu Yin on 5/6/20.
//  Copyright © 2020 UCDavis. All rights reserved.

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
    
    @IBOutlet weak var previewView: UIView!
    @IBOutlet weak var recordButton: UIButton!
    var recording = false {
        didSet {
            recording ? self.start() : self.stop()
        }
    }
    
    let captureSession = AVCaptureSession()
    lazy var previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
    
    var videoDataOutput: AVCaptureVideoDataOutput?
    var assetWriter: AVAssetWriter?
    var assetWriterInput: AVAssetWriterInput?
    var filePath: URL?
    var sessionAtSourceTime: CMTime?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.recording = false
        self.requestCameraPermission()
    }
    
    @IBAction func recordButtonPressed(_ sender: Any) {
        recording.toggle()
    }
    
    @IBAction func playRecordedVideo(_ sender: Any) {
        guard let url = filePath else {
            print("Can't get video url")
            return
        }
         let player = AVPlayer(url: url)
         let playerController = AVPlayerViewController()
         playerController.player = player
         present(playerController, animated: true) {
             player.play()
         }
    }
    // 判断摄像头权限
    func requestCameraPermission() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            self.setupCaptureSession()
            
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { granted in
                if granted {
                    DispatchQueue.main.async {
                        self.setupCaptureSession()
                    }
                }
            }
            
        case .denied:
            return
            
        case .restricted:
            return
        @unknown default:
            fatalError()
        }
    }
    
    // 初始化 CaptureSession
    func setupCaptureSession() {
        self.captureSession.beginConfiguration()
        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
        
        // 添加视频 输入设备
        guard let videoDeviceInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), self.captureSession.canAddInput(videoDeviceInput)
            else { return }
        self.captureSession.addInput(videoDeviceInput)
        
        // 设定输出是AVCaptureVideoDataOutput类型
        let tempVideoDataOutput = AVCaptureVideoDataOutput()
        tempVideoDataOutput.alwaysDiscardsLateVideoFrames = true
        tempVideoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
        tempVideoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
        
        self.captureSession.addOutput(tempVideoDataOutput)
        
        self.captureSession.commitConfiguration()
        self.captureSession.startRunning()
        
        self.videoDataOutput = tempVideoDataOutput
        self.previewLayer.frame = self.previewView.frame
        self.previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        self.view.layer.addSublayer(previewLayer)
        self.setUpWriter()
    }
    
     // This mothod will overwrite previous video files
    func videoFileLocation() -> URL {
        let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
        let videoOutputUrl = URL(fileURLWithPath: documentsPath.appendingPathComponent("videoFile")).appendingPathExtension("mov")
        do {
        if FileManager.default.fileExists(atPath: videoOutputUrl.path) {
            try FileManager.default.removeItem(at: videoOutputUrl)
            print("file removed")
        }
        } catch {
            print(error)
        }

        return videoOutputUrl
    }
    
    // 设置AVAssetWriter
    func setUpWriter() {
        do {
            filePath = videoFileLocation()
            assetWriter = try AVAssetWriter(outputURL: filePath!, fileType: AVFileType.mov)

            // add video input
            let settings = self.videoDataOutput?.recommendedVideoSettingsForAssetWriter(writingTo: .mp4)
            assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: [
            AVVideoCodecKey : AVVideoCodecType.h264,
            AVVideoWidthKey : 720,
            AVVideoHeightKey : 1280,
            AVVideoCompressionPropertiesKey : [
                AVVideoAverageBitRateKey : 2300000,
                ],
            ])
            guard let assetWriterInput = assetWriterInput, let assetWriter = assetWriter else { return }
            assetWriterInput.expectsMediaDataInRealTime = true
//            assetWriterInput.transform = CGAffineTransform(rotationAngle: .pi/2) // Adapt to portrait mode
            
            if assetWriter.canAdd(assetWriterInput) {
                assetWriter.add(assetWriterInput)
                print("asset input added")
            } else {
                print("no input added")
            }

            assetWriter.startWriting()
            
            self.assetWriter = assetWriter
            self.assetWriterInput = assetWriterInput
        } catch let error {
            debugPrint(error.localizedDescription)
        }
    }

    func canWrite() -> Bool {
        return recording && assetWriter != nil && assetWriter?.status == .writing
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        connection.videoOrientation = .portrait
        guard self.recording else { return }
        
        let writable = self.canWrite()

        if writable, self.sessionAtSourceTime == nil {
            // start writing
            sessionAtSourceTime = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer)
            self.assetWriter?.startSession(atSourceTime: sessionAtSourceTime!)
        }
        guard let assetWriterInput = self.assetWriterInput else { return }
        if writable, assetWriterInput.isReadyForMoreMediaData {
            // write video buffer
            assetWriterInput.append(sampleBuffer)
        }

    }

    func start() {
        self.recordButton.setTitle("Stop", for: .normal)
        self.sessionAtSourceTime = nil
        self.setUpWriter()
        switch self.assetWriter?.status {
        case .writing:
            print("status writing")
        case .failed:
            print("status failed")
        case .cancelled:
            print("status cancelled")
        case .unknown:
            print("status unknown")
        default:
            print("status completed")
        }

    }

    func stop() {
        self.recordButton.setTitle("Record", for: .normal)
        self.assetWriterInput?.markAsFinished()
        print("marked as finished")
        self.assetWriter?.finishWriting { [weak self] in
            self?.sessionAtSourceTime = nil
        }
        print("finished writing \(self.filePath)")
    }

}
  • info.plist
Demo2:这个Demo录制的视频文件有声音

https://stackoverflow.com/questions/48569738/swift-4-avfoundation-screen-and-audio-recording-using-avassetwriter-on-mac-os

//
//  ViewController.swift
//  CustomCamera
//
//  Created by Taras Chernyshenko on 6/27/17.
//  Copyright © 2017 Taras Chernyshenko. All rights reserved.
//
import AVFoundation
import Photos

class NewRecorder: NSObject,
  AVCaptureAudioDataOutputSampleBufferDelegate,
AVCaptureVideoDataOutputSampleBufferDelegate {

  private var session: AVCaptureSession = AVCaptureSession()
  private var deviceInput: AVCaptureScreenInput?
  private var previewLayer: AVCaptureVideoPreviewLayer?
  private var videoOutput: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput()
  private var audioOutput: AVCaptureAudioDataOutput = AVCaptureAudioDataOutput()

  //private var videoDevice: AVCaptureDevice = AVCaptureScreenInput(displayID: 69731840) //AVCaptureDevice.default(for: AVMediaType.video)!
  private var audioConnection: AVCaptureConnection?
  private var videoConnection: AVCaptureConnection?

  private var assetWriter: AVAssetWriter?
  private var audioInput: AVAssetWriterInput?
  private var videoInput: AVAssetWriterInput?

  private var fileManager: FileManager = FileManager()
  private var recordingURL: URL?

  private var isCameraRecording: Bool = false
  private var isRecordingSessionStarted: Bool = false

  private var recordingQueue = DispatchQueue(label: "recording.queue")


  func setup() {
    self.session.sessionPreset = AVCaptureSession.Preset.high

    self.recordingURL = URL(fileURLWithPath: "\(NSTemporaryDirectory() as String)/file.mp4")
    if self.fileManager.isDeletableFile(atPath: self.recordingURL!.path) {
      _ = try? self.fileManager.removeItem(atPath: self.recordingURL!.path)
    }

    self.assetWriter = try? AVAssetWriter(outputURL: self.recordingURL!,
                                          fileType: AVFileType.mp4)
    self.assetWriter!.movieFragmentInterval = kCMTimeInvalid
    self.assetWriter!.shouldOptimizeForNetworkUse = true

    let audioSettings = [
      AVFormatIDKey : kAudioFormatMPEG4AAC,
      AVNumberOfChannelsKey : 2,
      AVSampleRateKey : 44100.0,
      AVEncoderBitRateKey: 192000
      ] as [String : Any]

    let videoSettings = [
      AVVideoCodecKey : AVVideoCodecType.h264,
      AVVideoWidthKey : 1920,
      AVVideoHeightKey : 1080
      /*AVVideoCompressionPropertiesKey: [
        AVVideoAverageBitRateKey:  NSNumber(value: 5000000)
      ]*/
      ] as [String : Any]

    self.videoInput = AVAssetWriterInput(mediaType: AVMediaType.video,
                                         outputSettings: videoSettings)
    self.audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio,
                                         outputSettings: audioSettings)

    self.videoInput?.expectsMediaDataInRealTime = true
    self.audioInput?.expectsMediaDataInRealTime = true

    if self.assetWriter!.canAdd(self.videoInput!) {
      self.assetWriter?.add(self.videoInput!)
    }

    if self.assetWriter!.canAdd(self.audioInput!) {
      self.assetWriter?.add(self.audioInput!)
    }

    //self.deviceInput = try? AVCaptureDeviceInput(device: self.videoDevice)
    self.deviceInput = AVCaptureScreenInput(displayID: 724042646)
    self.deviceInput!.minFrameDuration = CMTimeMake(1, Int32(30))
    self.deviceInput!.capturesCursor = true
    self.deviceInput!.capturesMouseClicks = true

    if self.session.canAddInput(self.deviceInput!) {
      self.session.addInput(self.deviceInput!)
    }

    self.previewLayer = AVCaptureVideoPreviewLayer(session: self.session)

    //importent line of code what will did a trick
    //self.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill

    //let rootLayer = self.view.layer
    //rootLayer.masksToBounds = true
    //self.previewLayer?.frame = CGRect(x: 0, y: 0, width: 1920, height: 1080)

    //rootLayer.insertSublayer(self.previewLayer!, at: 0)

    self.session.startRunning()

    DispatchQueue.main.async {
      self.session.beginConfiguration()

      if self.session.canAddOutput(self.videoOutput) {
        self.session.addOutput(self.videoOutput)
      }

      self.videoConnection = self.videoOutput.connection(with: AVMediaType.video)
      /*if self.videoConnection?.isVideoStabilizationSupported == true {
        self.videoConnection?.preferredVideoStabilizationMode = .auto
      }*/
      self.session.commitConfiguration()

      let audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
      let audioIn = try? AVCaptureDeviceInput(device: audioDevice!)

      if self.session.canAddInput(audioIn!) {
        self.session.addInput(audioIn!)
      }

      if self.session.canAddOutput(self.audioOutput) {
        self.session.addOutput(self.audioOutput)
      }

      self.audioConnection = self.audioOutput.connection(with: AVMediaType.audio)
    }
  }

  func startRecording() {
    if self.assetWriter?.startWriting() != true {
      print("error: \(self.assetWriter?.error.debugDescription ?? "")")
    }

    self.videoOutput.setSampleBufferDelegate(self, queue: self.recordingQueue)
    self.audioOutput.setSampleBufferDelegate(self, queue: self.recordingQueue)
  }

  func stopRecording() {
    self.videoOutput.setSampleBufferDelegate(nil, queue: nil)
    self.audioOutput.setSampleBufferDelegate(nil, queue: nil)

    self.assetWriter?.finishWriting {
      print("Saved in folder \(self.recordingURL!)")
      exit(0)
    }
  }
  func captureOutput(_ captureOutput: AVCaptureOutput, didOutput
    sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {

    if !self.isRecordingSessionStarted {
      let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
      self.assetWriter?.startSession(atSourceTime: presentationTime)
      self.isRecordingSessionStarted = true
    }

    let description = CMSampleBufferGetFormatDescription(sampleBuffer)!

    if CMFormatDescriptionGetMediaType(description) == kCMMediaType_Audio {
      if self.audioInput!.isReadyForMoreMediaData {
        //print("appendSampleBuffer audio");
        self.audioInput?.append(sampleBuffer)
      }
    } else {
      if self.videoInput!.isReadyForMoreMediaData {
        //print("appendSampleBuffer video");
        if !self.videoInput!.append(sampleBuffer) {
          print("Error writing video buffer");
        }
      }
    }
  }
}