Recently while working on a project, I need to record system screen with the App audio as well as microphone audio.
I tried following solutions.
With the help of Replay kit and its function startRecording
Before Calling Function
recorder.isMicrophoneEnabled = true
startRecording(r: recorder)
func startRecording(_ r: RPScreenRecorder) {
r.startRecording(handler: { (error: Error?) -> Void in
if error == nil { // Recording has started
// sender.title = "Stop"
self.recorder.isMicrophoneEnabled = true
} else {
// Handle error
print(error?.localizedDescription ?? "Unknown error")
}
})
}
The problem is that the video records and saved to camera roll but neither App Audio and nor Mic Audio Records in the saved video
Then I tried with the screenCapture Function Of Replay Kit. The code is given below
//MARK: Screen Recording
func startRecording(withFileName fileName: String, recordingHandler:#escaping (Error?)-> Void)
{
if #available(iOS 11.0, *)
{
let fileURL = URL(fileURLWithPath: ReplayFileUtil.filePath(fileName))
// let updatedFileUrl = fileURL.appendingPathExtension(".mp4")
// removeFile(fileURL)
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0] as String
let filePath : String = "\(documentsDirectory)/Replays/\(fileName)"
if FileManager.default.fileExists(atPath: filePath) {
print("sucess")
}
do {
assetWriter = try? AVAssetWriter.init(url: fileURL, fileType: .mp4)
} catch let error {
print(error.localizedDescription)
}
var videoCleanApertureSettings = [
AVVideoCleanApertureWidthKey : 320,
AVVideoCleanApertureHeightKey : 480,
AVVideoCleanApertureHorizontalOffsetKey : 10,
AVVideoCleanApertureVerticalOffsetKey : 10
]
var codecSettings = [
AVVideoAverageBitRateKey : 960000,
AVVideoMaxKeyFrameIntervalKey : 1,
AVVideoCleanApertureKey : videoCleanApertureSettings
] as [String:Any]
var videoOutputSettings = [
AVVideoCodecKey : AVVideoCodecType.jpeg,
AVVideoCompressionPropertiesKey : codecSettings,
AVVideoWidthKey : 300,
AVVideoHeightKey : 540
] as [String:Any]
var channelLayout = AudioChannelLayout.init()
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_D
let audioOutputSettings: [String : Any] = [
AVNumberOfChannelsKey: 6,
AVFormatIDKey: kAudioFormatMPEG4AAC_HE,
AVSampleRateKey: 44100,
AVChannelLayoutKey: NSData(bytes: &channelLayout, length: MemoryLayout.size(ofValue: channelLayout)),
]
audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioOutputSettings)
videoInput = AVAssetWriterInput (mediaType: AVMediaType.video, outputSettings: videoOutputSettings)
videoInput.expectsMediaDataInRealTime = true
audioInput.expectsMediaDataInRealTime = true
if assetWriter.canAdd(videoInput) {
print("Added video Input")
assetWriter.add(videoInput)
}
assetWriter.add(audioInput)
self.assetWriter.startWriting()
RPScreenRecorder.shared().isMicrophoneEnabled = true
let time = CMTime.init(value: 10, timescale: 1)
self.assetWriter.startSession(atSourceTime: time)
RPScreenRecorder.shared().startCapture(handler: { (sample, bufferType, error) in
recordingHandler(error)
if CMSampleBufferDataIsReady(sample)
{
if self.assetWriter.status == AVAssetWriterStatus.unknown
{
// self.assetWriter.startWriting()
print(self.assetWriter.status)
}
print(self.assetWriter.status)
if self.assetWriter.status == AVAssetWriterStatus.failed {
print("Asset Writer failed")
print("Error occured, status = \(self.assetWriter.status.rawValue), \(self.assetWriter.error!.localizedDescription) \(String(describing: self.assetWriter.error))")
return
}
if (bufferType == .video)
{
if self.videoInput.isReadyForMoreMediaData
{
print("Buffer Video Print")
self.videoInput.append(sample)
}
}
if (bufferType == .audioApp || bufferType == .audioMic)
{
if self.audioInput.isReadyForMoreMediaData
{
print("Audio Buffer Came")
self.audioInput.append(sample)
}
}
}
}) { (error) in
recordingHandler(error)
print(error?.localizedDescription)
}
} else
{
// Fallback on earlier versions
}
}
Please suggest me what I doing wrong.
//
// ScreenRecorder.swift
// BugReporterTest
//
// Created by Giridhar on 09/06/17.
// Copyright © 2017 Giridhar. All rights reserved.
//
import Foundation
import ReplayKit
import AVKit
import Photos
class ScreenRecorder
{
var assetWriter:AVAssetWriter!
var videoInput:AVAssetWriterInput!
var audioInput:AVAssetWriterInput!
var startSesstion = false
// let viewOverlay = WindowUtil()
//MARK: Screen Recording
func startRecording(withFileName fileName: String, recordingHandler:#escaping (Error?)-> Void)
{
if #available(iOS 11.0, *)
{
let fileURL = URL(fileURLWithPath: ReplayFileUtil.filePath(fileName))
assetWriter = try! AVAssetWriter(outputURL: fileURL, fileType:
AVFileType.mp4)
let videoOutputSettings: Dictionary<String, Any> = [
AVVideoCodecKey : AVVideoCodecType.h264,
AVVideoWidthKey : UIScreen.main.bounds.size.width,
AVVideoHeightKey : UIScreen.main.bounds.size.height,
// AVVideoCompressionPropertiesKey : [
// AVVideoAverageBitRateKey :425000, //96000
// AVVideoMaxKeyFrameIntervalKey : 1
// ]
];
var channelLayout = AudioChannelLayout.init()
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_D
let audioOutputSettings: [String : Any] = [
AVNumberOfChannelsKey: 6,
AVFormatIDKey: kAudioFormatMPEG4AAC_HE,
AVSampleRateKey: 44100,
AVChannelLayoutKey: NSData(bytes: &channelLayout, length: MemoryLayout.size(ofValue: channelLayout)),
]
videoInput = AVAssetWriterInput(mediaType: AVMediaType.video,outputSettings: videoOutputSettings)
audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio,outputSettings: audioOutputSettings)
videoInput.expectsMediaDataInRealTime = true
audioInput.expectsMediaDataInRealTime = true
assetWriter.add(videoInput)
assetWriter.add(audioInput)
RPScreenRecorder.shared().startCapture(handler: { (sample, bufferType, error) in
recordingHandler(error)
if CMSampleBufferDataIsReady(sample)
{
DispatchQueue.main.async { [weak self] in
if self?.assetWriter.status == AVAssetWriterStatus.unknown {
print("AVAssetWriterStatus.unknown")
if !(self?.assetWriter.startWriting())! {
return
}
self?.assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sample))
self?.startSesstion = true
}
// if self.assetWriter.status == AVAssetWriterStatus.unknown
// {
// self.assetWriter.startWriting()
// self.assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sample))
// self?.startSesstion = true
}
if self.assetWriter.status == AVAssetWriterStatus.failed {
print("Error occured, status = \(String(describing: self.assetWriter.status.rawValue)), \(String(describing: self.assetWriter.error!.localizedDescription)) \(String(describing: self.assetWriter.error))")
recordingHandler(self.assetWriter.error)
return
}
if (bufferType == .video)
{
if(self.videoInput.isReadyForMoreMediaData) && self.startSesstion {
self.videoInput.append(sample)
}
}
if (bufferType == .audioApp)
{
if self.audioInput.isReadyForMoreMediaData
{
//print("Audio Buffer Came")
self.audioInput.append(sample)
}
}
}
}) { (error) in
recordingHandler(error)
// debugPrint(error)
}
} else
{
// Fallback on earlier versions
}
}
func stopRecording(isBack: Bool, aPathName: String ,handler: #escaping (Error?) -> Void)
{
//var isSucessFullsave = false
if #available(iOS 11.0, *)
{
self.startSesstion = false
RPScreenRecorder.shared().stopCapture{ (error) in
self.videoInput.markAsFinished()
self.audioInput.markAsFinished()
handler(error)
if error == nil{
self.assetWriter.finishWriting{
self.startSesstion = false
print(ReplayFileUtil.fetchAllReplays())
if !isBack{
self.PhotosSaveWithAurtorise(aPathName: aPathName)
}else{
self.deleteDirectory()
}
}
}else{
self.deleteDirectory()
}
}
}else {
// print("Fallback on earlier versions")
}
}
func PhotosSaveWithAurtorise(aPathName: String) {
if PHPhotoLibrary.authorizationStatus() == .authorized {
self.SaveToCamera(aPathName: aPathName)
} else {
PHPhotoLibrary.requestAuthorization({ (status) in
if status == .authorized {
self.SaveToCamera(aPathName: aPathName)
}
})
}
}
func SaveToCamera(aPathName: String){
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: (ReplayFileUtil.fetchAllReplays().last)!)
}) { saved, error in
if saved {
addScreenCaptureVideo(aPath: aPathName)
print("Save")
}else{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "isScreenRecordFaildToSave"), object: nil)
print("error to save - \(error)")
}
}
}
func deleteDirectory() {
ReplayFileUtil.delete()
}
}
Related
I'm working on a project that needs to record video segments and then merge these segments in one video. I'm using AVFoundation framework to do so.
The problem is that when I merge the segments, there's always a black frame or no sound between the chunks. Also, sometimes the sound is not synchronised after merging.
I've tried many options to solve this problem but didn't find a good solution.
I've tried many solutions on Stack overflow but none of them worked.
I've also tried to use MKOVideoMerge but I still have the problem.
I've made a small View controller below that records video camera and create a new segment every 10 seconds. When the user taps on "stop", all the segments are merged and saved to camera roll :
If anyone have managed to merge two video segments without dropping frame or sound, help would be much appreciated :)
import UIKit
import AVFoundation
import Photos
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate {
private enum SessionSetupResult {
case success
case notAuthorized
case configurationFailed
}
#IBOutlet weak var btnStartStop: UIButton!
#IBOutlet weak var previewView: PreviewView!
private let session = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "session queue")
private let mergeQueue = DispatchQueue(label: "merge queue")
private var setupResult: SessionSetupResult = .success
private var videoDeviceInput: AVCaptureDeviceInput!
lazy private var movieBufferOutput = AVCaptureVideoDataOutput()
lazy private var audioBufferOutput = AVCaptureAudioDataOutput()
private var movieConnection: AVCaptureConnection!
private var audioConnection: AVCaptureConnection!
private var assetWriter: AVAssetWriter! = nil
private var assetWriterInput: AVAssetWriterInput! = nil
private var audioWriterInput: AVAssetWriterInput! = nil
private var chunkNumber = 0
private let chunkMaxDuration = 10.0
private var chunkStartTime: CMTime! = nil
private var chunkOutputURL: URL! = nil
private var stopRecording: Bool = false
#IBAction func startStop(_ sender: Any) {
stopRecording = true
btnStartStop.setTitle("recording", for: .normal)
}
override func viewDidLoad() {
super.viewDidLoad()
previewView.session = session
previewView.videoPreviewLayer.videoGravity = .resizeAspectFill
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
break
case .notDetermined:
sessionQueue.suspend()
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
if !granted {
self.setupResult = .notAuthorized
}
self.sessionQueue.resume()
})
default:
setupResult = .notAuthorized
}
sessionQueue.async {
self.configureSession()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.cleanTempDirectory()
sessionQueue.async {
switch self.setupResult {
case .success:
break
case .notAuthorized:
DispatchQueue.main.async {
let changePrivacySetting = "Not authorized"
let message = NSLocalizedString(changePrivacySetting, comment: "Alert message when the user has denied access to the camera")
let alertController = UIAlertController(title: "Not authorized", message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"),
style: .cancel,
handler: nil))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Settings", comment: "Alert button to open Settings"),
style: .`default`,
handler: { _ in
UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!, options: [:], completionHandler: nil)
}))
self.present(alertController, animated: true, completion: nil)
}
case .configurationFailed:
DispatchQueue.main.async {
let alertMsg = "Error"
let message = NSLocalizedString("Error", comment: alertMsg)
let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"),
style: .cancel,
handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
}
}
private func configureSession() {
if setupResult != .success {
return
}
session.beginConfiguration()
session.sessionPreset = .high
do {
var defaultVideoDevice: AVCaptureDevice?
if let dualCameraDevice = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
defaultVideoDevice = dualCameraDevice
} else if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
defaultVideoDevice = backCameraDevice
} else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
defaultVideoDevice = frontCameraDevice
}
let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice!)
if session.canAddInput(videoDeviceInput) {
session.addInput(videoDeviceInput)
self.videoDeviceInput = videoDeviceInput
DispatchQueue.main.async {
self.previewView.videoPreviewLayer.connection?.videoOrientation = .landscapeRight
}
} else {
setupResult = .configurationFailed
session.commitConfiguration()
return
}
} catch {
setupResult = .configurationFailed
session.commitConfiguration()
return
}
do {
let audioDevice = AVCaptureDevice.default(for: .audio)
let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!)
if session.canAddInput(audioDeviceInput) {
session.addInput(audioDeviceInput)
}
} catch {
}
movieBufferOutput.videoSettings = [
String(kCVPixelBufferPixelFormatTypeKey) : Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
]
if self.session.canAddOutput(movieBufferOutput) {
self.session.addOutput(movieBufferOutput)
if let connection = self.movieBufferOutput.connection(with: .video) {
movieConnection = connection
connection.videoOrientation = .landscapeRight
if connection.isVideoStabilizationSupported {
connection.preferredVideoStabilizationMode = .auto
}
}
} else {
setupResult = .configurationFailed
session.commitConfiguration()
return
}
if self.session.canAddOutput(audioBufferOutput) {
self.session.addOutput(audioBufferOutput)
if let connection = self.audioBufferOutput.connection(with: .audio) {
audioConnection = connection
}
} else {
print("Could not add audio output to the session")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
let queue: DispatchQueue = DispatchQueue(label: "MediaOutputQueue")
let audioQueue: DispatchQueue = DispatchQueue(label: "AudioOutputQueue")
self.movieBufferOutput.setSampleBufferDelegate(self, queue: queue)
self.audioBufferOutput.setSampleBufferDelegate(self, queue: audioQueue)
self.movieBufferOutput.alwaysDiscardsLateVideoFrames = true
session.commitConfiguration()
self.session.startRunning()
}
func createWriterInput(for presentationTimeStamp: CMTime) {
self.stopRecording = false
let fileManager = FileManager.default
let outputFileName = "chunk\(chunkNumber)"
let outputFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((outputFileName as NSString).appendingPathExtension("mp4")!)
chunkOutputURL = URL(fileURLWithPath: outputFilePath)
try? fileManager.removeItem(at: chunkOutputURL)
assetWriter = try! AVAssetWriter(outputURL: chunkOutputURL, fileType: .mp4)
let outputSettings: [String: Any] = [AVVideoCodecKey:AVVideoCodecH264, AVVideoWidthKey: 1280, AVVideoHeightKey: 720]
assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
assetWriterInput.expectsMediaDataInRealTime = true
assetWriter.add(assetWriterInput)
let audioSettings = [
AVFormatIDKey : kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey : 1,
AVSampleRateKey : 44100.0,
AVEncoderBitRateKey: 192000
] as [String : Any]
audioWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)
audioWriterInput.expectsMediaDataInRealTime = true;
assetWriter.add(audioWriterInput)
chunkNumber += 1
chunkStartTime = presentationTimeStamp
assetWriter.startWriting()
assetWriter.startSession(atSourceTime: chunkStartTime)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeRight
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .landscapeRight
}
func cleanTempDirectory() {
let tempVideosPath = NSTemporaryDirectory()
var isDirectory = ObjCBool(true)
if !FileManager.default.fileExists(atPath: tempVideosPath, isDirectory: &isDirectory) {
return
}
let tempVideosURL = URL(fileURLWithPath: tempVideosPath)
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: tempVideosURL, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles,.skipsSubdirectoryDescendants])
let mp4Files = directoryContents.filter{ $0.pathExtension == "mp4" }.map { url in
(url, (try? url.resourceValues(forKeys: [.creationDateKey]))?.creationDate ?? Date.distantPast)
}.sorted(by: { $0.1 < $1.1 })
for mp4 in mp4Files {
try? FileManager.default.removeItem(at: mp4.0)
}
} catch {
}
}
func getTempVideos() -> [URL] {
let tempVideosPath = NSTemporaryDirectory()
var isDirectory = ObjCBool(true)
if !FileManager.default.fileExists(atPath: tempVideosPath, isDirectory: &isDirectory) {
return []
}
var videosURL: [URL] = []
let videosUrl = URL(fileURLWithPath: tempVideosPath)
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: videosUrl, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles,.skipsSubdirectoryDescendants])
let mp4Files = directoryContents.filter{ $0.pathExtension == "mp4" }.map { url in
(url, (try? url.resourceValues(forKeys: [.creationDateKey]))?.creationDate ?? Date.distantPast)
}.sorted(by: { $0.1 > $1.1 })
var i = 0
for mp4 in mp4Files {
videosURL.append(mp4.0)
i = i + 1
if i > 5 {
break
}
}
} catch {
return []
}
return videosURL
}
func getRecordedVideoURL() -> URL {
var tempVideosPath = NSTemporaryDirectory()
tempVideosPath = (tempVideosPath as NSString).appendingPathComponent("videos")
var isDirectory = ObjCBool(true)
if !FileManager.default.fileExists(atPath: tempVideosPath, isDirectory: &isDirectory) {
do {
try FileManager.default.createDirectory(at: URL(fileURLWithPath: tempVideosPath), withIntermediateDirectories: true, attributes: nil)
} catch {
}
}
let outputFileName = "test-" + NSUUID().uuidString
let outputFileURL = URL(fileURLWithPath: tempVideosPath).appendingPathComponent(outputFileName).appendingPathExtension("mp4")
try? FileManager.default.removeItem(at: outputFileURL)
return outputFileURL
}
func mergeVideos(urls:[URL], excludedUrl: URL, completion:#escaping (_ exporter: AVAssetExportSession?) -> ()) -> Void {
let mainComposition = AVMutableComposition()
let compositionVideoTrack = mainComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
let soundtrackTrack = mainComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
let assetOptions = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
var insertTime = kCMTimeZero
var audioInsertTime = kCMTimeZero
var videos: [(asset: AVURLAsset, videoTrack: AVAssetTrack, videoDuration: CMTime)] = []
for url in urls {
if url.path != excludedUrl.path {
let videoAsset = AVURLAsset(url: url, options : assetOptions)
if videoAsset.tracks(withMediaType: .video).count > 0 && videoAsset.tracks(withMediaType: .audio).count > 0 {
let videoTrack = videoAsset.tracks(withMediaType: .video)[0]
let videoDuration = videoTrack.timeRange.duration
videos.append((asset: videoAsset, videoTrack: videoTrack, videoDuration: videoDuration))
} else {
break
}
}
}
var hasError: Bool = false
for video in videos.reversed() {
let audioTrack = video.asset.tracks(withMediaType: .audio)[0]
let audioDuration = audioTrack.timeRange.duration
do {
try compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, video.videoDuration), of: video.videoTrack, at: insertTime)
} catch let error {
hasError = true
print(error)
}
do {
try soundtrackTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, audioDuration), of: audioTrack, at: audioInsertTime)
} catch let error {
hasError = true
print(error)
}
insertTime = CMTimeAdd(insertTime, video.videoDuration)
audioInsertTime = CMTimeAdd(audioInsertTime, audioDuration)
}
if videos.count == 0 {
hasError = true
}
if !hasError {
let outputFileURL = getRecordedVideoURL()
let exporter = AVAssetExportSession(asset: mainComposition, presetName: AVAssetExportPresetHighestQuality)
exporter?.outputURL = outputFileURL
exporter?.outputFileType = AVFileType.mp4
exporter?.shouldOptimizeForNetworkUse = true
exporter?.exportAsynchronously {
DispatchQueue.main.async {
completion(exporter!)
}
}
} else {
completion(nil)
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
if connection == self.audioConnection {
if let audioInput = self.audioWriterInput, audioInput.isReadyForMoreMediaData {
if !audioInput.append(sampleBuffer) {
print("Error writing audio buffer");
}
}
} else {
if let videoInput = self.assetWriterInput, videoInput.isReadyForMoreMediaData {
if !videoInput.append(sampleBuffer) {
print("Error writing video buffer");
}
}
}
if connection == movieConnection {
let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
if assetWriter == nil {
createWriterInput(for: presentationTimeStamp)
} else {
let currentChunkDuration = CMTimeGetSeconds(CMTimeSubtract(presentationTimeStamp, chunkStartTime))
if currentChunkDuration >= chunkMaxDuration || self.stopRecording == true {
let chunkAssetWriter = assetWriter!
let assetWriterInput = self.assetWriterInput
let audioWriterInput = self.audioWriterInput
let stopRecording = self.stopRecording
createWriterInput(for: presentationTimeStamp)
assetWriterInput?.markAsFinished()
audioWriterInput?.markAsFinished()
chunkAssetWriter.endSession(atSourceTime: presentationTimeStamp)
chunkAssetWriter.finishWriting {
DispatchQueue.main.async {
self.btnStartStop.setTitle("stop", for: .normal)
}
if stopRecording {
self.mergeQueue.async {
self.mergeVideos(urls: self.getTempVideos(), excludedUrl: self.chunkOutputURL!, completion: { exportSession in
if let exportSession = exportSession {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: exportSession.outputURL!)
}) { saved, error in
DispatchQueue.main.async {
if saved {
let alertController = UIAlertController(title: "Your video was successfully saved", message: nil, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
}
}
}
}
})
}
}
}
}
}
}
}}
Thanks for your help :)
Scenario 1:
Fire up AVAssetWriter with Audio / Video writer input.
Use RPScreenRecorder to start recording with no microphone and process the sample buffers.
File writes out fine to Photos on first try.
Scenario 2:
Fire up AVAssetWriter with Audio / Video writer input.
Use RPScreenRecorder to start recording with microphone enabled and process the sample buffers.
File writes fails to write out on the first try.
UserInfo={NSLocalizedRecoverySuggestion=Try saving again., NSLocalizedDescription=Cannot Save, NSUnderlyingError=0x1c464f3c0 {Error Domain=NSOSStatusErrorDomain Code=-12412 "(null)"}}
2017-10-26 23:25:16.896673-0400 [2135:771655] Status FAILS!: 3 Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save"
2nd try works fine.
What am I doing wrong?
import Foundation
import ReplayKit
import AVKit
import Photos
class ScreenRecorder
{
var assetWriter:AVAssetWriter!
var videoInput:AVAssetWriterInput!
var audioInput:AVAssetWriterInput!
var startSesstion = false
// let viewOverlay = WindowUtil()
//MARK: Screen Recording
func startRecording(withFileName fileName: String, recordingHandler:#escaping (Error?)-> Void)
{
if #available(iOS 11.0, *)
{
let fileURL = URL(fileURLWithPath: ReplayFileUtil.filePath(fileName))
assetWriter = try! AVAssetWriter(outputURL: fileURL, fileType:
AVFileType.mp4)
let videoOutputSettings: Dictionary<String, Any> = [
AVVideoCodecKey : AVVideoCodecType.h264,
AVVideoWidthKey : UIScreen.main.bounds.size.width,
AVVideoHeightKey : UIScreen.main.bounds.size.height,
// AVVideoCompressionPropertiesKey : [
// AVVideoAverageBitRateKey :425000, //96000
// AVVideoMaxKeyFrameIntervalKey : 1
// ]
];
var channelLayout = AudioChannelLayout.init()
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_D
let audioOutputSettings: [String : Any] = [
AVNumberOfChannelsKey: 6,
AVFormatIDKey: kAudioFormatMPEG4AAC_HE,
AVSampleRateKey: 44100,
AVChannelLayoutKey: NSData(bytes: &channelLayout, length: MemoryLayout.size(ofValue: channelLayout)),
]
videoInput = AVAssetWriterInput(mediaType: AVMediaType.video,outputSettings: videoOutputSettings)
audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio,outputSettings: audioOutputSettings)
videoInput.expectsMediaDataInRealTime = true
audioInput.expectsMediaDataInRealTime = true
assetWriter.add(videoInput)
assetWriter.add(audioInput)
RPScreenRecorder.shared().startCapture(handler: { (sample, bufferType, error) in
recordingHandler(error)
if CMSampleBufferDataIsReady(sample)
{
DispatchQueue.main.async { [weak self] in
if self?.assetWriter.status == AVAssetWriterStatus.unknown {
print("AVAssetWriterStatus.unknown")
if !(self?.assetWriter.startWriting())! {
return
}
self?.assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sample))
self?.startSesstion = true
}
// if self.assetWriter.status == AVAssetWriterStatus.unknown
// {
// self.assetWriter.startWriting()
// self.assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sample))
// self?.startSesstion = true
}
if self.assetWriter.status == AVAssetWriterStatus.failed {
print("Error occured, status = \(String(describing: self.assetWriter.status.rawValue)), \(String(describing: self.assetWriter.error!.localizedDescription)) \(String(describing: self.assetWriter.error))")
recordingHandler(self.assetWriter.error)
return
}
if (bufferType == .video)
{
if(self.videoInput.isReadyForMoreMediaData) && self.startSesstion {
self.videoInput.append(sample)
}
}
if (bufferType == .audioApp)
{
if self.audioInput.isReadyForMoreMediaData
{
//print("Audio Buffer Came")
self.audioInput.append(sample)
}
}
}
}) { (error) in
recordingHandler(error)
// debugPrint(error)
}
} else
{
// Fallback on earlier versions
}
}
func stopRecording(isBack: Bool, aPathName: String ,handler: #escaping (Error?) -> Void)
{
//var isSucessFullsave = false
if #available(iOS 11.0, *)
{
self.startSesstion = false
RPScreenRecorder.shared().stopCapture{ (error) in
self.videoInput.markAsFinished()
self.audioInput.markAsFinished()
handler(error)
if error == nil{
self.assetWriter.finishWriting{
self.startSesstion = false
print(ReplayFileUtil.fetchAllReplays())
if !isBack{
self.PhotosSaveWithAurtorise(aPathName: aPathName)
}else{
self.deleteDirectory()
}
}
}else{
self.deleteDirectory()
}
}
}else {
// print("Fallback on earlier versions")
}
}
func PhotosSaveWithAurtorise(aPathName: String) {
if PHPhotoLibrary.authorizationStatus() == .authorized {
self.SaveToCamera(aPathName: aPathName)
} else {
PHPhotoLibrary.requestAuthorization({ (status) in
if status == .authorized {
self.SaveToCamera(aPathName: aPathName)
}
})
}
}
func SaveToCamera(aPathName: String){
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: (ReplayFileUtil.fetchAllReplays().last)!)
}) { saved, error in
if saved {
addScreenCaptureVideo(aPath: aPathName)
print("Save")
}else{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "isScreenRecordFaildToSave"), object: nil)
print("error to save - \(error)")
}
}
}
func deleteDirectory() {
ReplayFileUtil.delete()
}
}
Added retry logic to circumvent the issue. Not the greatest solution but it works.
[self.screenRecorder startCaptureWithHandler:^(CMSampleBufferRef _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
if(CMSampleBufferDataIsReady(sampleBuffer) == false || self.assetWriter == nil)
{
return;
}
if (self.assetWriter.status == AVAssetWriterStatusFailed) {
NSLog(#"AVWriter Failed!");
return;
}
if (CMSampleBufferDataIsReady(sampleBuffer)) {
if(self.assetWriter.status == AVAssetWriterStatusWriting) {
if (bufferType == RPSampleBufferTypeVideo) {
if (!self.startedSession) {
dispatch_async(dispatch_get_main_queue(), ^{
_startDate = [NSDate date];
_recordingTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(updateRecordingTime) userInfo:nil repeats:YES];
// Disable the idle timer while recording
[UIApplication sharedApplication].idleTimerDisabled = YES;
});
CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
[self.assetWriter startSessionAtSourceTime:pts];
self.startedSession = YES;
NSLog(#"MP4Writer: started session in appendVideoSample");
}
if (CMTimeCompare(kCMTimeInvalid, self.firstVideoFrameTime) == 0) {
self.firstVideoFrameTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
}
if (self.assetWriterVideoInput.readyForMoreMediaData) {
#try {
[self.assetWriterVideoInput appendSampleBuffer:sampleBuffer];
}
#catch(NSException *expection) {
NSLog(#"Missed Video Buffer: %#", self.assetWriter.error);
}
}
}
if (bufferType == RPSampleBufferTypeAudioMic) {
if (CMTimeCompare(kCMTimeInvalid, self.firstVideoFrameTime) == 0 ||
CMTimeCompare(self.firstVideoFrameTime, CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) == 1) {
return;
}
if (!self.startedSession) {
CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
[self.assetWriter startSessionAtSourceTime:pts];
self.startedSession = YES;
NSLog(#"MP4Writer: started session in appendAudioSample");
}
if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
#try {
[self.assetWriterAudioInput appendSampleBuffer:sampleBuffer];
}
#catch(NSException *expection) {
NSLog(#"Missed Audio Buffer: %#", self.assetWriter.error);
}
}
}
}
}
} completionHandler:^(NSError * _Nullable error) {
if (!error) {
NSLog(#"Recording started successfully.");
}
}];
I am making a sample application that utilizes AVFoundation to record video. The whole point is so I can have more control over how the video is recorded. In my sample project I have the video capturing but am struggling with handling orientation correctly.
I have done a lot of searching around the web and found that others are suggesting that I should NOT allow my capture view or capture session to rotate based on orientation, but rather set a transformation to rotate the video during playback. I have this working fine on iOS and Mac devices, but am wondering if I will have issues on other platforms such as Windows or Android.
Also, when I view the recorded video's metadata I see that the width and height are not set properly for the orientation. This makes sense as I am only transforming the presentation of the video and not it's actual resolution.
My question here is how do I correctly support portrait and landscape orientations and have it reflected correctly in the video file output? I need these videos to play on all platforms correctly so I am thinking the resolution is going to matter a great deal.
Below is the full source I have written thus far. I appreciate any advice you all can provide.
import UIKit
import AVFoundation
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
//MARK: - Outlet
#IBOutlet weak var previewView: UIView!
#IBOutlet var playStopButton: UIButton!
//MARK: - Private Variables
let videoQueue = DispatchQueue(label: "VideoQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
private let captureSession = AVCaptureSession()
var outputUrl: URL {
get {
if let url = _outputUrl {
return url
}
_outputUrl = outputDirectory.appendingPathComponent("video.mp4")
return _outputUrl!
}
}
private var _outputUrl: URL?
var outputDirectory: URL {
get {
if let url = _outputDirectory {
return url
}
_outputDirectory = getDocumentsDirectory().appendingPathComponent("recording")
return _outputDirectory!
}
}
private var _outputDirectory: URL?
private var assetWriter: AVAssetWriter?
private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput?
private var videoOutput: AVCaptureVideoDataOutput?
private var audioOutput: AVCaptureAudioDataOutput?
private var isRecording = false
private var isWriting = false
private var videoSize = CGSize(width: 640, height: 480)
//MARK: - View Life-cycle
override func viewDidLoad() {
super.viewDidLoad()
videoQueue.async {
do {
try self.configureCaptureSession()
try self.configureAssetWriter()
DispatchQueue.main.async {
self.configurePreview()
}
} catch {
DispatchQueue.main.async {
self.showAlert("Unable to configure video output")
}
}
}
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
//MARK: - Capture Session
private func configureCaptureSession() throws {
do {
// configure the session
if captureSession.canSetSessionPreset(AVCaptureSessionPreset640x480) {
captureSession.sessionPreset = AVCaptureSessionPreset640x480
}
// configure capture devices
let camDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
let micDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio)
let camInput = try AVCaptureDeviceInput(device: camDevice)
let micInput = try AVCaptureDeviceInput(device: micDevice)
if captureSession.canAddInput(camInput) {
captureSession.addInput(camInput)
}
if captureSession.canAddInput(micInput) {
captureSession.addInput(micInput)
}
// configure audio/video output
videoOutput = AVCaptureVideoDataOutput()
videoOutput?.alwaysDiscardsLateVideoFrames = false // TODO: is this necessary?
videoOutput?.setSampleBufferDelegate(self, queue: videoQueue)
if let v = videoOutput {
captureSession.addOutput(v)
}
audioOutput = AVCaptureAudioDataOutput()
audioOutput?.setSampleBufferDelegate(self, queue: videoQueue)
if let a = audioOutput {
captureSession.addOutput(a)
}
// configure audio session
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try audioSession.setActive(true)
var micPort: AVAudioSessionPortDescription?
if let inputs = audioSession.availableInputs {
for port in inputs {
if port.portType == AVAudioSessionPortBuiltInMic {
micPort = port
break;
}
}
}
if let port = micPort, let dataSources = port.dataSources {
for source in dataSources {
if source.orientation == AVAudioSessionOrientationFront {
try audioSession.setPreferredInput(port)
break
}
}
}
} catch {
print("Failed to configure audio/video capture session")
throw error
}
}
private func configureAssetWriter() throws {
prepareVideoFile()
do {
assetWriter = try AVAssetWriter(url: outputUrl, fileType: AVFileTypeMPEG4)
guard let writer = assetWriter else {
print("Asset writer not created")
return
}
let vidSize = videoSize
let videoSettings: [String: Any] = [AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: NSNumber(value: Float(vidSize.width)),
AVVideoHeightKey: NSNumber(value: Float(vidSize.height))]
videoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)
videoInput?.expectsMediaDataInRealTime = true
videoInput?.transform = getVideoTransform()
var channelLayout = AudioChannelLayout()
memset(&channelLayout, 0, MemoryLayout<AudioChannelLayout>.size);
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
let audioSettings: [String: Any] = [AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 2]
audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: audioSettings)
audioInput?.expectsMediaDataInRealTime = true
guard let vi = videoInput else {
print("Video input not configured")
return
}
guard let ai = audioInput else {
print("Audio input not configured")
return
}
if writer.canAdd(vi) {
writer.add(vi)
}
if writer.canAdd(ai) {
writer.add(ai)
}
} catch {
print("Failed to configure asset writer")
throw error
}
}
private func prepareVideoFile() {
if FileManager.default.fileExists(atPath: outputUrl.path) {
do {
try FileManager.default.removeItem(at: outputUrl)
} catch {
print("Unable to remove file at URL \(outputUrl)")
}
}
if !FileManager.default.fileExists(atPath: outputDirectory.path) {
do {
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Unable to create directory at URL \(outputDirectory)")
}
}
}
private func configurePreview() {
if let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) {
previewLayer.frame = previewView.bounds
previewView.layer.addSublayer(previewLayer)
}
}
private func getVideoSize() -> CGSize {
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
if videoSize.width > videoSize.height {
return videoSize
} else {
return CGSize(width: videoSize.height, height: videoSize.width)
}
} else {
if videoSize.width < videoSize.height {
return videoSize
} else {
return CGSize(width: videoSize.height, height: videoSize.width)
}
}
}
private func getVideoTransform() -> CGAffineTransform {
switch UIDevice.current.orientation {
case .portraitUpsideDown:
return CGAffineTransform(rotationAngle: CGFloat((M_PI * -90.0)) / 180.0)
case .landscapeLeft:
return CGAffineTransform(rotationAngle: CGFloat((M_PI * -180.0)) / 180.0) // TODO: Add support for front facing camera
// return CGAffineTransform(rotationAngle: CGFloat((M_PI * 0.0)) / 180.0) // TODO: For front facing camera
case .landscapeRight:
return CGAffineTransform(rotationAngle: CGFloat((M_PI * 0.0)) / 180.0) // TODO: Add support for front facing camera
// return CGAffineTransform(rotationAngle: CGFloat((M_PI * -180.0)) / 180.0) // TODO: For front facing camera
default:
return CGAffineTransform(rotationAngle: CGFloat((M_PI * 90.0)) / 180.0)
}
}
//MARK: - Controls
private func startRecording() {
videoQueue.async {
self.captureSession.startRunning()
}
isRecording = true
playStopButton.setTitle("Stop Recording", for: .normal)
print("Recording did start")
}
private func stopRecording() {
if !isRecording {
return
}
videoQueue.async {
self.assetWriter?.finishWriting {
print("Asset writer did finish writing")
self.isWriting = false
}
self.captureSession.stopRunning()
}
isRecording = false
playStopButton.setTitle("Start Recording", for: .normal)
print("Recording did stop")
}
//MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
guard let w = assetWriter else {
print("Asset writer not configured")
return
}
guard let vo = videoOutput else {
print("Video output not configured")
return
}
guard let ao = audioOutput else {
print("Audio output not configured")
return
}
guard let vi = videoInput else {
print("Video input not configured")
return
}
guard let ai = audioInput else {
print("Audio input not configured")
return
}
let st = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
print("Writer status \(w.status.rawValue)")
if let e = w.error {
print("Writer error \(e)")
stopRecording()
return
}
switch w.status {
case .unknown:
if !isWriting {
isWriting = true
w.startWriting()
w.startSession(atSourceTime: st)
}
return
case .completed:
print("Video writing completed")
return
case .cancelled:
print("Video writing cancelled")
return
case .failed:
print("Video writing failed")
return
default:
print("Video is writing")
}
if vo == captureOutput {
if !vi.append(sampleBuffer) {
print("Unable to write to video buffer")
}
} else if ao == captureOutput {
if !ai.append(sampleBuffer) {
print("Unable to write to audio buffer")
}
}
}
//MARK: Helpers
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
//MARK: Actions
#IBAction func startStopTapped(sender: AnyObject) {
if isRecording {
stopRecording()
} else {
startRecording()
}
}
}
Video orientation is handled by the AVAssetWriterInput.transform, looks like the getVideoTransform() implementation is not correct - CGAffineTransform expects the rotation angle to be in radians, so need to change to something like this:
private func getVideoTransform() -> CGAffineTransform {
switch UIDevice.current.orientation {
case .portrait:
return .identity
case .portraitUpsideDown:
return CGAffineTransform(rotationAngle: .pi)
case .landscapeLeft:
return CGAffineTransform(rotationAngle: .pi/2)
case .landscapeRight:
return CGAffineTransform(rotationAngle: -.pi/2)
default:
return .identity
}
}
From Apple Technical Q&A:
https://developer.apple.com/library/archive/qa/qa1744/_index.html
If you are using an AVAssetWriter object to write a movie file, you
can use the transform property of the associated AVAssetWriterInput to
specify the output file orientation. This will write a display
transform property into the output file as the preferred
transformation of the visual media data for display purposes. See the
AVAssetWriterInput.h interface file for the details.
I have found a solution to my problem. The solution is to export the video using AVAssetExportSession to handle setting the video size and then handling the rotation at the time of export and not during recording. I still have an issue were I need to fix the scale factor to go from my original video size to a smaller 640x480 resolution, but at least I solved my rotation issues. Please see updated code below.
import UIKit
import AVFoundation
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
//MARK: - Outlet
#IBOutlet weak var previewView: UIView!
#IBOutlet var playStopButton: UIButton!
//MARK: - Private Variables
let videoQueue = DispatchQueue(label: "VideoQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
private let captureSession = AVCaptureSession()
var outputUrl: URL {
get {
if let url = _outputUrl {
return url
}
_outputUrl = outputDirectory.appendingPathComponent("video.mp4")
return _outputUrl!
}
}
private var _outputUrl: URL?
var exportUrl: URL {
get {
if let url = _exportUrl {
return url
}
_exportUrl = outputDirectory.appendingPathComponent("video_encoded.mp4")
return _exportUrl!
}
}
private var _exportUrl: URL?
var outputDirectory: URL {
get {
if let url = _outputDirectory {
return url
}
_outputDirectory = getDocumentsDirectory().appendingPathComponent("recording")
return _outputDirectory!
}
}
private var _outputDirectory: URL?
private var assetWriter: AVAssetWriter?
private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput?
private var videoOutput: AVCaptureVideoDataOutput?
private var audioOutput: AVCaptureAudioDataOutput?
private var isRecording = false
private var isWriting = false
private var videoSize = CGSize(width: 640, height: 480)
private var exportPreset = AVAssetExportPreset640x480
//MARK: - View Life-cycle
override func viewDidLoad() {
super.viewDidLoad()
videoQueue.async {
do {
try self.configureCaptureSession()
DispatchQueue.main.sync {
self.configurePreview()
}
} catch {
DispatchQueue.main.async {
self.showAlert("Unable to configure capture session")
}
}
}
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
//MARK: - Capture Session
private func configureCaptureSession() throws {
do {
// configure capture devices
let camDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
let micDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio)
let camInput = try AVCaptureDeviceInput(device: camDevice)
let micInput = try AVCaptureDeviceInput(device: micDevice)
if captureSession.canAddInput(camInput) {
captureSession.addInput(camInput)
}
if captureSession.canAddInput(micInput) {
captureSession.addInput(micInput)
}
// configure audio/video output
videoOutput = AVCaptureVideoDataOutput()
videoOutput?.alwaysDiscardsLateVideoFrames = false // TODO: is this necessary?
videoOutput?.setSampleBufferDelegate(self, queue: videoQueue)
if let v = videoOutput {
captureSession.addOutput(v)
}
audioOutput = AVCaptureAudioDataOutput()
audioOutput?.setSampleBufferDelegate(self, queue: videoQueue)
if let a = audioOutput {
captureSession.addOutput(a)
}
// configure audio session
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try audioSession.setActive(true)
var micPort: AVAudioSessionPortDescription?
if let inputs = audioSession.availableInputs {
for port in inputs {
if port.portType == AVAudioSessionPortBuiltInMic {
micPort = port
break;
}
}
}
if let port = micPort, let dataSources = port.dataSources {
for source in dataSources {
if source.orientation == AVAudioSessionOrientationFront {
try audioSession.setPreferredInput(port)
break
}
}
}
} catch {
print("Failed to configure audio/video capture session")
throw error
}
}
private func configureAssetWriter() throws {
prepareVideoFile()
do {
if assetWriter != nil {
assetWriter = nil
videoInput = nil
audioInput = nil
}
assetWriter = try AVAssetWriter(url: outputUrl, fileType: AVFileTypeMPEG4)
guard let writer = assetWriter else {
print("Asset writer not created")
return
}
let videoSettings: [String: Any] = [AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: NSNumber(value: Float(videoSize.width)),
AVVideoHeightKey: NSNumber(value: Float(videoSize.height))]
videoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)
videoInput?.expectsMediaDataInRealTime = true
var channelLayout = AudioChannelLayout()
memset(&channelLayout, 0, MemoryLayout<AudioChannelLayout>.size);
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
let audioSettings: [String: Any] = [AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 2]
audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: audioSettings)
audioInput?.expectsMediaDataInRealTime = true
guard let vi = videoInput else {
print("Video input not configured")
return
}
guard let ai = audioInput else {
print("Audio input not configured")
return
}
if writer.canAdd(vi) {
writer.add(vi)
}
if writer.canAdd(ai) {
writer.add(ai)
}
} catch {
print("Failed to configure asset writer")
throw error
}
}
private func prepareVideoFile() {
if FileManager.default.fileExists(atPath: outputUrl.path) {
do {
try FileManager.default.removeItem(at: outputUrl)
} catch {
print("Unable to remove file at URL \(outputUrl)")
}
}
if !FileManager.default.fileExists(atPath: outputDirectory.path) {
do {
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Unable to create directory at URL \(outputDirectory)")
}
}
}
private func configurePreview() {
if let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) {
previewLayer.frame = previewView.bounds
previewView.layer.addSublayer(previewLayer)
}
}
private func getVideoSize() -> CGSize {
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
if videoSize.width > videoSize.height {
return videoSize
} else {
return CGSize(width: videoSize.height, height: videoSize.width)
}
} else {
if videoSize.width < videoSize.height {
return videoSize
} else {
return CGSize(width: videoSize.height, height: videoSize.width)
}
}
}
//MARK: - Controls
private func startRecording() {
videoQueue.async {
do {
try self.configureAssetWriter()
self.captureSession.startRunning()
} catch {
print("Unable to start recording")
DispatchQueue.main.async { self.showAlert("Unable to start recording") }
}
}
isRecording = true
playStopButton.setTitle("Stop Recording", for: .normal)
print("Recording did start")
}
private func stopRecording() {
if !isRecording {
return
}
videoQueue.async {
self.assetWriter?.finishWriting {
print("Asset writer did finish writing")
self.isWriting = false
}
self.captureSession.stopRunning()
do {
try self.export()
} catch {
print("Export failed")
DispatchQueue.main.async { self.showAlert("Unable to export video") }
}
}
isRecording = false
playStopButton.setTitle("Start Recording", for: .normal)
print("Recording did stop")
}
//MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
guard let w = assetWriter else {
print("Asset writer not configured")
return
}
guard let vo = videoOutput else {
print("Video output not configured")
return
}
guard let ao = audioOutput else {
print("Audio output not configured")
return
}
guard let vi = videoInput else {
print("Video input not configured")
return
}
guard let ai = audioInput else {
print("Audio input not configured")
return
}
let st = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
print("Writer status \(w.status.rawValue)")
if let e = w.error {
print("Writer error \(e)")
stopRecording()
return
}
switch w.status {
case .unknown:
if !isWriting {
isWriting = true
w.startWriting()
w.startSession(atSourceTime: st)
}
return
case .completed:
print("Video writing completed")
return
case .cancelled:
print("Video writing cancelled")
return
case .failed:
print("Video writing failed")
return
default:
print("Video is writing")
}
if vo == captureOutput {
if !vi.append(sampleBuffer) {
print("Unable to write to video buffer")
}
} else if ao == captureOutput {
if !ai.append(sampleBuffer) {
print("Unable to write to audio buffer")
}
}
}
//MARK: - Export
private func getVideoComposition(asset: AVAsset, videoSize: CGSize) -> AVMutableVideoComposition? {
guard let videoTrack = asset.tracks(withMediaType: AVMediaTypeVideo).first else {
print("Unable to get video tracks")
return nil
}
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = videoSize
let seconds: Float64 = Float64(1.0 / videoTrack.nominalFrameRate)
videoComposition.frameDuration = CMTimeMakeWithSeconds(seconds, 600);
let layerInst = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
var transforms = asset.preferredTransform
var isPortrait = true;
if (transforms.a == 0.0 && transforms.b == 1.0 && transforms.c == -1.0 && transforms.d == 0)
|| (transforms.a == 0.0 && transforms.b == -1.0 && transforms.c == 1.0 && transforms.d == 0) {
isPortrait = false;
}
if isPortrait {
transforms = transforms.concatenating(CGAffineTransform(rotationAngle: CGFloat(90.0.degreesToRadians)))
transforms = transforms.concatenating(CGAffineTransform(translationX: videoSize.width, y: 0))
}
layerInst.setTransform(transforms, at: kCMTimeZero)
let inst = AVMutableVideoCompositionInstruction()
inst.backgroundColor = UIColor.black.cgColor
inst.layerInstructions = [layerInst]
inst.timeRange = CMTimeRange(start: kCMTimeZero, duration: asset.duration)
videoComposition.instructions = [inst]
return videoComposition
}
private func export() throws {
let videoAsset = AVURLAsset(url: outputUrl)
if FileManager.default.fileExists(atPath: exportUrl.path) {
try FileManager.default.removeItem(at: exportUrl)
}
let videoSize = getVideoSize()
guard let encoder = AVAssetExportSession(asset: videoAsset, presetName: exportPreset) else {
print("Unable to create encoder")
return
}
guard let vidcomp = getVideoComposition(asset: videoAsset, videoSize: videoSize) else {
print("Unable to create video composition")
return
}
encoder.videoComposition = vidcomp
encoder.outputFileType = AVFileTypeMPEG4 // MP4 format
encoder.outputURL = exportUrl
encoder.shouldOptimizeForNetworkUse = true
encoder.exportAsynchronously(completionHandler: {
print("Video exported successfully")
})
}
//MARK: Helpers
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
//MARK: Actions
#IBAction func startStopTapped(sender: AnyObject) {
if isRecording {
stopRecording()
} else {
startRecording()
}
}
}
I found that the easiest is to set the preferredTransform on the video composition track depending on the required orientation.
Solution
// Devices orientation
var orientation = UIDevice.current.orientation
// The composition
let audioVideoComposition = AVMutableComposition()
// The video track of the composition
let videoCompositionTrack = audioVideoComposition
.addMutableTrack(withMediaType: .video, preferredTrackID: .init())!
// Set preferred transform
videoCompositionTrack.preferredTransform = getVideoTransform()
Helper function and extension
func getVideoTransform() -> CGAffineTransform {
switch orientation {
case .portrait:
return CGAffineTransform(rotationAngle: 90.degreesToRadians)
case .portraitUpsideDown:
return CGAffineTransform(rotationAngle: 180)
case .landscapeLeft:
return CGAffineTransform(rotationAngle: 0.degreesToRadians)
case .landscapeRight:
return CGAffineTransform(rotationAngle: 180.degreesToRadians)
default:
return CGAffineTransform(rotationAngle: 90.degreesToRadians)
}
}
extension BinaryInteger {
var degreesToRadians: CGFloat { CGFloat(self) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { self * .pi / 180 }
var radiansToDegrees: Self { self * 180 / .pi }
}
just swap width and height in writer settings
and don't forget about the HEVC
assetWriter = try AVAssetWriter(url: outputUrl, fileType: AVFileTypeMPEG4)
https://developer.apple.com/videos/play/wwdc2017/503
https://developer.apple.com/videos/play/wwdc2017/511
- (BOOL)
configureWriterInput {
const BOOL isError = YES;
AVFileType
mov = AVFileTypeQuickTimeMovie;
NSDictionary <NSString *, id> *settings;
// HEVC
if (#available(iOS 11.0, *)) {
NSArray <AVVideoCodecType> *available = [self.sessionOutput availableVideoCodecTypesForAssetWriterWithOutputFileType:
mov];
const BOOL isHEVC = [available containsObject:AVVideoCodecTypeHEVC];
if (isHEVC) {
settings = [self.sessionOutput recommendedVideoSettingsForVideoCodecType:
AVVideoCodecTypeHEVC
assetWriterOutputFileType:
mov];
}
else {
settings = [self.sessionOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:
mov];
}
}
else {
settings = [self.sessionOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:
mov];
}
if ([writer
canApplyOutputSettings:settings forMediaType:AVMediaTypeVideo]) {
// swap width and height to fix orientation
NSMutableDictionary <NSString *, id> *rotate =
[settings mutableCopy];
if (YES
&& settings[AVVideoHeightKey]
&& settings[AVVideoWidthKey]
) {
rotate[AVVideoHeightKey] = settings[AVVideoWidthKey];
rotate[AVVideoWidthKey] = settings[AVVideoHeightKey];
if ([writer
canApplyOutputSettings:rotate forMediaType:AVMediaTypeVideo]) {
settings = rotate;
}
else {
}
}
else {
}
}
else {
return isError;
}
writerInput = [AVAssetWriterInput
assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:settings];
{
// AVCaptureConnection *con =
// [self.sessionOutput connectionWithMediaType:AVMediaTypeVideo];
// const AVCaptureVideoOrientation o = con.videoOrientation;
// writerInput.transform = [[self class] configureOrientationTransform:o];
}
if ([writer canAddInput:writerInput]) {
[writer addInput:writerInput];
return ! isError;
}
else {
return isError;
}
}
This question already has answers here:
What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean?
(16 answers)
Closed 6 years ago.
I have downloaded a project from https://github.com/doberman/speaker-gender-detect--ios. I followed the instructions but when I run the app, I get a message that says: fatal error: unexpectedly found nil while unwrapping an Optional value. How can I fix the crash so that the app works.
The app crashes here:
let genderEqualityRatios = self.calcGenderEquality (String (response.result.value!))
This is my code:
import AVFoundation
import Alamofire
import SwiftyJSON
protocol AudioRecorderDelegate {
func audioRecorder(audioRecorder: AudioRecorder?, updatedLevel: Float)
func audioRecorder(audioRecorder: AudioRecorder?, updatedGenderEqualityRatio: (male: Float, female: Float))
}
class AudioRecorder: NSObject {
static let sharedInstance: AudioRecorder = AudioRecorder()
private let kRemoteURL: NSURL = NSURL(string: "xxx.xxx.xxx.xxx")! // change to your API endpoint URL
private let kPostAudioInterval: NSTimeInterval = 10.0 // change to post to API more/less frequently
var delegate: AudioRecorderDelegate?
private let recorderSettings = [
AVSampleRateKey: NSNumber(float: Float(16000.0)),
AVFormatIDKey: NSNumber(int: Int32(kAudioFormatMPEG4AAC)),
AVNumberOfChannelsKey: NSNumber(int: 1),
AVEncoderAudioQualityKey: NSNumber(int: Int32(AVAudioQuality.High.rawValue))
]
private var recorder: AVAudioRecorder?
private var checkLevelsTimer: NSTimer?
private var postTimer: NSTimer?
private var maleDuration: Float = 0.0
private var femaleDuration: Float = 0.0
override init() {
super.init()
do {
let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(AVAudioSessionCategoryRecord)
} catch let err as NSError {
print("Failed to initialize AudioRecorder: \(err)")
}
}
func startRecording() {
// print("startRecording")
if self.recorder != nil && self.recorder!.recording {
self.stopRecording()
}
let audioURL: NSURL = self.getAudioURL()
// print("got audioURL: '\(audioURL)'")
do {
self.recorder = try AVAudioRecorder(URL: audioURL, settings: self.recorderSettings)
self.recorder?.meteringEnabled = true
self.recorder?.prepareToRecord()
} catch let err as NSError {
print("Failed to set up AVAudioRecorder instance: \(err)")
}
guard self.recorder != nil else { return }
self.recorder?.record()
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
self.checkLevelsTimer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: "checkLevels", userInfo: nil, repeats: true)
self.postTimer = NSTimer.scheduledTimerWithTimeInterval(kPostAudioInterval, target: self, selector: "onPostTimerTrigger", userInfo: nil, repeats: true)
} catch let err as NSError {
print("Failed to activate audio session (or failed to set up checkLevels timer): \(err)")
}
}
func stopRecording(shouldSubmitAudioAfterStop: Bool = false) {
// print("stopRecording")
guard self.recorder != nil else {
print("`self.recorder` is `nil` - no recording to stop")
return
}
self.recorder?.stop()
if let t = self.checkLevelsTimer {
t.invalidate()
self.checkLevelsTimer = nil
}
if let t = self.postTimer {
t.invalidate()
self.postTimer = nil
}
let audioURL: NSURL = self.recorder!.url
self.recorder = nil
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(false)
if shouldSubmitAudioAfterStop {
self.postAudio(audioURL)
} else {
// print("`shouldSubmitAudioAfterStop` is `false` - I won't post audio")
}
} catch let err as NSError {
print("Failed to deactivate audio session (or failed to post audio): \(err)")
}
}
// MARK: -
func checkLevels() {
guard self.recorder != nil else {
print("`self.recorder` is `nil` - can't check levels")
return
}
self.recorder?.updateMeters()
let averagePower: Float = self.recorder!.averagePowerForChannel(0)
if let d = self.delegate {
d.audioRecorder(self, updatedLevel: averagePower)
} else {
print("AudioRecorder - averagePower: \(averagePower)")
}
}
func onPostTimerTrigger() {
// print("onPostTimerTrigger")
guard let r = self.recorder else {
print("`self.recorder` is `nil` - no audio to post")
return
}
if !r.recording {
print("not recording - no audio to post")
}
self.stopRecording(true)
self.startRecording()
}
// MARK: -
private func getAudioURL(filename: String = "recording") -> NSURL {
let fileManager: NSFileManager = NSFileManager.defaultManager()
let urls: [NSURL] = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let documentDirectory: NSURL = urls[0] as NSURL
let uniqueFilename = "\(filename)_\(NSDate().timeIntervalSince1970)"
let audioURL: NSURL = documentDirectory.URLByAppendingPathComponent("\(uniqueFilename).m4a")
return audioURL
}
private func postAudio(audioURL: NSURL) {
// print("AudioRecorder.postAudio - audioURL: \(audioURL.absoluteString)")
Alamofire.upload(Method.POST, kRemoteURL, multipartFormData: { multipartFormData in
multipartFormData.appendBodyPart(fileURL: audioURL, name: "file")
}, encodingCompletion: { encodingResult in
switch encodingResult {
case .Success (let upload, _, _):
upload.responseString { response in
//print("response: \(response)")
let genderEqualityRatios = self.calcGenderEquality(String(response.result.value!))
if let eq = genderEqualityRatios, let d = self.delegate {
d.audioRecorder(self, updatedGenderEqualityRatio: eq)
}
}
case .Failure(let encodingError):
print("encodingError: \(encodingError)")
}
})
}
private func calcGenderEquality(response: String) -> (male: Float, female: Float)? {
guard let dataFromString = response.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) else {
return nil
}
let json = JSON(data: dataFromString)
for selection in json["selections"].arrayValue {
if selection["gender"] == "M" {
self.maleDuration = self.maleDuration + (selection["endTime"].floatValue - selection["startTime"].floatValue)
} else if selection["gender"] == "F" {
self.femaleDuration = self.maleDuration + (selection["endTime"].floatValue - selection["startTime"].floatValue)
}
}
let spokenDuration = self.maleDuration + self.femaleDuration
let maleFactor = self.maleDuration / spokenDuration
let femaleFactor = self.femaleDuration / spokenDuration
guard !maleFactor.isNaN else {
print("Failed to calculate gender equality (`maleFactor` is `NaN`)")
return nil
}
guard !femaleFactor.isNaN else {
print("Failed to calculate gender equality (`femaleFactor` is `NaN`)")
return nil
}
return (male: maleFactor, female: femaleFactor)
}
}
Try it like this:
if let genderEqualityRatios = response.result.value as? String {
self.calcGenderEquality(genderEqualityRatios)
} else {
print("a problem occurred and we couldn't call calcGenderEquality")
}
I am having trouble recording audio and displaying it in a tableview. I am able to record and immediately play it back, but the audio doesn't seem to actually be stored to the device permanently, so I am unable to call it from the tableview. The directory also seems to change each time the app is open. How can I correct my code for permanent save and recall when populating tableview rows?
func record() {
let audioSession:AVAudioSession = AVAudioSession.sharedInstance()
if (audioSession.respondsToSelector("requestRecordPermission:")) {
AVAudioSession.sharedInstance().requestRecordPermission({(granted: Bool)-> Void in
if granted {
print("granted")
try! audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try! audioSession.setActive(true)
let documentsDirectory = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let fullPath = (documentsDirectory as NSString).stringByAppendingPathComponent("Mobile.PCM")
let url = NSURL.fileURLWithPath(fullPath)
print(fullPath)
let settings: [String : AnyObject] = [
AVFormatIDKey:Int(kAudioFormatAppleIMA4),
AVSampleRateKey:44100.0,
AVNumberOfChannelsKey:2,
AVEncoderBitRateKey:12800,
AVLinearPCMBitDepthKey:16,
AVEncoderAudioQualityKey:AVAudioQuality.Max.rawValue
]
try! self.audioRecorder = AVAudioRecorder(URL: url, settings: settings)
self.audioRecorder.meteringEnabled = true
self.audioRecorder.record()
} else{
print("not granted")
}
})
}
}
I am able to record and save with this:
#IBAction func record(sender: UIButton) {
if player != nil && player.playing {
player.stop()
}
if recorder == nil {
print("recording. recorder nil")
// recordButton.setTitle("Pause", forState:.Normal)
playButton.enabled = false
stopButton.enabled = true
recordWithPermission(true)
return
}
if recorder != nil && recorder.recording {
print("pausing")
recorder.pause()
recordButton.setTitle("Continue", forState:.Normal)
} else {
print("recording")
// recordButton.setTitle("Pause", forState:.Normal)
playButton.enabled = false
stopButton.enabled = true
recordWithPermission(false)
}
}
func setupRecorder() {
let format = NSDateFormatter()
format.dateFormat="yyyy-MM-dd-HH-mm-ss"
let currentFileName = "recording-\(format.stringFromDate(NSDate())).caf"
print(currentFileName)
let documentsDirectory = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
self.soundFileURL = documentsDirectory.URLByAppendingPathComponent(currentFileName)
if NSFileManager.defaultManager().fileExistsAtPath(soundFileURL.absoluteString) {
print("soundfile \(soundFileURL.absoluteString) exists")
}
let recordSettings:[String : AnyObject] = [
AVFormatIDKey: NSNumber(unsignedInt:kAudioFormatAppleLossless),
AVEncoderAudioQualityKey : AVAudioQuality.Max.rawValue,
AVEncoderBitRateKey : 320000,
AVNumberOfChannelsKey: 2,
AVSampleRateKey : 44100.0
]
do {
recorder = try AVAudioRecorder(URL: soundFileURL, settings: recordSettings)
recorder.delegate = self
recorder.meteringEnabled = true
recorder.prepareToRecord() soundFileURL
} catch let error as NSError {
recorder = nil
print(error.localizedDescription)
}
}
func recordWithPermission(setup:Bool) {
let session:AVAudioSession = AVAudioSession.sharedInstance()
if (session.respondsToSelector("requestRecordPermission:")) {
AVAudioSession.sharedInstance().requestRecordPermission({(granted: Bool)-> Void in
if granted {
print("Permission to record granted")
self.setSessionPlayAndRecord()
if setup {
self.setupRecorder()
}
self.recorder.record()
self.meterTimer = NSTimer.scheduledTimerWithTimeInterval(0.1,
target:self,
selector:"updateAudioMeter:",
userInfo:nil,
repeats:true)
} else {
print("Permission to record not granted")
}
})
} else {
print("requestRecordPermission unrecognized")
}
}
I am able to recall it in a TableView with this:
override func viewDidLoad() {
super.viewDidLoad()
tableView.reloadData()
listRecordings()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell")!
cell.textLabel!.text = recordings[indexPath.row].lastPathComponent
return cell
}
func listRecordings() {
let documentsDirectory = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
do {
let urls = try NSFileManager.defaultManager().contentsOfDirectoryAtURL(documentsDirectory, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles)
self.recordings = urls.filter( { (name: NSURL) -> Bool in
return name.lastPathComponent!.hasSuffix("caf")
})
} catch let error as NSError {
print(error.localizedDescription)
} catch {
print("something went wrong")
}
}