Currently,I am using SDAVAssetExportSession https://github.com/rs/SDAVAssetExportSession project instead of standard AVAssetExportSession because SDAVAssetExportSession gives me access to set following settings
exportSessionSDA!.outputFileType = AVFileTypeMPEG4
exportSessionSDA!.shouldOptimizeForNetworkUse = true
exportSessionSDA.videoSettings = [AVVideoCodecKey: AVVideoCodecH264, AVVideoWidthKey: 800, AVVideoHeightKey: 600, AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: 6000000, AVVideoProfileLevelKey: AVVideoProfileLevelH264High40]]
exportSessionSDA.audioSettings = [AVFormatIDKey: kAudioFormatMPEG4AAC, AVNumberOfChannelsKey: 2, AVSampleRateKey: 44100, AVEncoderBitRateKey: 128000]
I want these settings in standard AVAssetExportSession as SDAVAssetExportSession is creating an error
SDAVAssetExportSession error
-[AVAssetWriter startSessionAtSourceTime:] invalid parameter not satisfying: CMTIME_IS_NUMERIC(startTime)
Mainly what i want is to compress video size. How to achieve this?
Recorded video source is available as mp4..
mycode
func sdExport(_ videoURL: URL)
{
let avAsset1 = AVURLAsset(url: videoURL, options: nil)
print("AVURLAsset1 ",avAsset1)
let avAsset = AVURLAsset(url: ((videoURL as NSURL) as URL), options: nil)
print("AVURLAsset2 ",avAsset)
let startDate = Foundation.Date()
//Create Export session
// exportSessionSDA = SDAVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough)
exportSessionSDA = SDAVAssetExportSession(asset: avAsset)
// exportSession = AVAssetExportSession(asset: composition, presetName: mp4Quality)
//Creating temp path to save the converted video
let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let myDocumentPath = URL(fileURLWithPath: documentsDirectory).appendingPathComponent("temp.mp4").absoluteString
let url = URL(fileURLWithPath: myDocumentPath)
let documentsDirectory2 = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
let filePath = documentsDirectory2.appendingPathComponent("tempConverted/rendered-Video.mp4")
// deleteFile(filePath)
//Check if the file already exists then remove the previous file
if FileManager.default.fileExists(atPath: myDocumentPath) {
do {
try FileManager.default.removeItem(atPath: myDocumentPath)
}
catch let error {
print(error)
}
}
exportSessionSDA!.outputURL = filePath
exportSessionSDA!.outputFileType = AVFileTypeMPEG4
exportSessionSDA!.shouldOptimizeForNetworkUse = true
exportSessionSDA.videoSettings = [AVVideoCodecKey: AVVideoCodecH264, AVVideoWidthKey: 800, AVVideoHeightKey: 600, AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: 6000000, AVVideoProfileLevelKey: AVVideoProfileLevelH264High40]]
exportSessionSDA.audioSettings = [AVFormatIDKey: kAudioFormatMPEG4AAC, AVNumberOfChannelsKey: 2, AVSampleRateKey: 44100, AVEncoderBitRateKey: 128000]
let start = CMTimeMakeWithSeconds(0.0, 0)
let range = CMTimeRangeMake(start, avAsset.duration)
exportSessionSDA.timeRange = range
exportSessionSDA!.exportAsynchronously(completionHandler: {() -> Void in
switch self.exportSessionSDA!.status {
case .failed:
print("%#",self.exportSessionSDA?.error)
case .cancelled:
print("Export canceled")
case .completed:
//Video conversion finished
let endDate = Foundation.Date()
let time = endDate.timeIntervalSince(startDate)
print(time)
print("Successful!")
guard let data = NSData(contentsOf: (self.singleTonFile?.fileUrl)!) else {
return
}
print("File size before compression: \(Double(data.length / 1048576)) mb")
print("elf.exportSession.outputURL= ",self.exportSessionSDA.outputURL)
guard let compressedData = NSData(contentsOf: self.exportSessionSDA.outputURL!) else {
return
}
print("File size after compression: \(Double(compressedData.length / 1048576)) mb")
let when = DispatchTime.now() + 8
DispatchQueue.main.asyncAfter(deadline: when)
{
print("video_duration", self.singleTon.video_duration)
self.videoUrl() // calling upload
}
// self.mediaPath = self.exportSession.outputURL?.path as NSString!
// self.singleTon.mediaUrl = URL(self.exportSession.outputURL?.path)
// print("self.singleTon.mediaUrl ",self.singleTon.mediaUrl)
//self.mediaPath = String(self.exportSession.outputURL!)
// self.mediaPath = self.mediaPath.substringFromIndex(7)
default:
break
}
})
}
Related
Right now I get a url from a AVMixComposition using AVExportSession. HighestQuality looks great but the file size is too big, a 15 sec video is 27 mb. MediumQuality looks horrible but the file size is only 1 mb. I know I have to use AVAssetWriter and play with the bit rate to find a middle ground.
The problem is I can't figure out a way to get a url from an AVMixComposition without using AVExportSession to pass to the AVAssetWriter.
What I have to do is save the video using exportSession, get the url, pass it to assetWriter, set the bit rate, and then compress it from there. The resulting .mp4 (has to .mp4) assetWriter video doesn't compress the original exportSession videoURL. It's actually bigger which defeats the purpose.
let mixComposition = AVMutableComposition()
// ...
guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
exporter.outputURL = outputFileURL
exporter.outputFileType = AVFileType.mp4
exporter.shouldOptimizeForNetworkUse = true
exporter.exportAsynchronously {
// ...
let data = try! Data(contentsOf: outputFileURL)
print("compression size: \(Double(data.count / 1048576)) mb") // 27 mb
DispatchQueue.main.async { ...
guard let videoURL = exporter.outputURL else { return }
self.passUrlToAssetWriter(urlToCompress: videoURL)
}
func passUrlToAssetWriter(urlToCompress: URL) {
let asset = AVAsset(url: urlToCompress)
guard let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first else { return }
guard let audioTrack = asset.tracks(withMediaType: AVMediaType.audio).first else return }
// dropping the bit rate made no difference
let videoSettings:[String:Any] = [AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: 250000], AVVideoCodecKey: AVVideoCodecType.h264, AVVideoHeightKey: videoTrack.naturalSize.height, AVVideoWidthKey: videoTrack.naturalSize.width]
let audioSettings: [String:Any] = [AVFormatIDKey : kAudioFormatMPEG4AAC, AVNumberOfChannelsKey : 2, AVSampleRateKey : 44100.0, AVEncoderBitRateKey: 128000]
assetWriter = try! AVAssetWriter(outputURL: myOutputURL, fileType: AVFileType.mp4)
// everything else to configure assetWriter ...
assetWriter?.finishWriting(completionHandler: {
let data = try! Data(contentsOf: self.assetWriter!.outputURL)!)
print("compression size: \(Double(data.count / 1048576)) mb") // 27.5 mb, it's actually bigger than the exporter file size
})
}
There is no need to use a url, you can pass the mixComposition to an AVPlayerItem > AVPlayer > player.currentItem.asset:
let mixComposition = AVMutableComposition()
// ...
passMixToAssetWriter(mixComposition)
func passMixToAssetWriter(_ mixComposition: AVMutableComposition) {
let item = AVPlayerItem(asset: composition)
let player = AVPlayer()
player.replaceCurrentItem(with: item)
guard let currentItem = player.currentItem else { return }
let asset = currentItem.asset
guard let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first else { return }
guard let audioTrack = asset.tracks(withMediaType: AVMediaType.audio).first else return }
// raise the bit rate to 11000000
let videoSettings:[String:Any] = [AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: 1100000], AVVideoCodecKey: AVVideoCodecType.h264, AVVideoHeightKey: videoTrack.naturalSize.height, AVVideoWidthKey: videoTrack.naturalSize.width]
let audioSettings: [String:Any] = [AVFormatIDKey : kAudioFormatMPEG4AAC, AVNumberOfChannelsKey : 2, AVSampleRateKey : 44100.0, AVEncoderBitRateKey: 128000]
assetWriter = try! AVAssetWriter(outputURL: myOutputURL, fileType: AVFileType.mp4)
// everything else to configure assetWriter ...
assetWriter?.finishWriting(completionHandler: {
let data = try! Data(contentsOf: self.assetWriter!.outputURL)!)
print("compression size: \(Double(data.count / 1048576)) mb") // 27.5 mb, it's actually bigger than the exporter file size
})
}
I have the following function that takes a url of an existing video file and compresses it into an output file. I am using Avasset Reader and writter.
var assetReader:AVAssetReader?
var assetWriter:AVAssetWriter?
func compressFile(urlToCompress: URL, outputURL: URL, completion:#escaping (URL)->Void){
//video file to make the asset
var audioFinished = false
var videoFinished = false
let asset = AVAsset(url: urlToCompress);
//create asset reader
do{
assetReader = try AVAssetReader(asset: asset)
} catch{
assetReader = nil
}
guard let reader = assetReader else{
fatalError("Could not initalize asset reader probably failed its try catch")
}
let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first!
let audioTrack = asset.tracks(withMediaType: AVMediaType.audio).first!
let videoReaderSettings: [String:Any] = [kCVPixelBufferPixelFormatTypeKey as String!:kCVPixelFormatType_32ARGB ]
// ADJUST BIT RATE OF VIDEO HERE
let width = UIScreen.main.bounds.width - 20
let scale = width / videoTrack.naturalSize.width
let height = Int(videoTrack.naturalSize.height * scale)
let videoSettings:[String:Any] = [
AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey:4000000],
AVVideoCodecKey: AVVideoCodecH264,
AVVideoHeightKey: height,
AVVideoWidthKey: Int(width)
]
let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
if reader.canAdd(assetReaderVideoOutput){
reader.add(assetReaderVideoOutput)
}else{
fatalError("Couldn't add video output reader")
}
if reader.canAdd(assetReaderAudioOutput){
reader.add(assetReaderAudioOutput)
}else{
fatalError("Couldn't add audio output reader")
}
let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil)
let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings)
videoInput.transform = videoTrack.preferredTransform
//we need to add samples to the video input
let videoInputQueue = DispatchQueue(label: "videoQueue")
let audioInputQueue = DispatchQueue(label: "audioQueue")
do{
assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mov)
}catch{
assetWriter = nil
}
guard let writer = assetWriter else{
fatalError("assetWriter was nil")
}
writer.shouldOptimizeForNetworkUse = true
writer.add(videoInput)
writer.add(audioInput)
writer.startWriting()
reader.startReading()
writer.startSession(atSourceTime: CMTime.zero)
let closeWriter:()->Void = {
if (audioFinished && videoFinished){
self.assetWriter?.finishWriting(completionHandler: {
self.checkFileSize(sizeUrl: (self.assetWriter?.outputURL)!, message: "The file size of the compressed file is: ")
completion((self.assetWriter?.outputURL)!)
})
self.assetReader?.cancelReading()
}
}
audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
while(audioInput.isReadyForMoreMediaData){
let sample = assetReaderAudioOutput.copyNextSampleBuffer()
if (sample != nil){
audioInput.append(sample!)
}else{
audioInput.markAsFinished()
DispatchQueue.main.async {
audioFinished = true
closeWriter()
}
break;
}
}
}
videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
//request data here
while(videoInput.isReadyForMoreMediaData){
let sample = assetReaderVideoOutput.copyNextSampleBuffer()
if (sample != nil){
videoInput.append(sample!)
}else{
videoInput.markAsFinished()
DispatchQueue.main.async {
videoFinished = true
closeWriter()
}
break;
}
}
}
}
func checkFileSize(sizeUrl: URL, message:String){
let data = NSData(contentsOf: sizeUrl)!
print(message, (Double(data.length) / 1048576.0), " mb")
}
For some reason i am getting the error bellow. my question is how do i set this key and more importantly why should i set it and to what value. and what is encoder delay? thank you
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[AVAssetWriterInput appendSampleBuffer:] Cannot append sample buffer: First input buffer must have an appropriate kCMSampleBufferAttachmentKey_TrimDurationAtStart since the codec has encoder delay'
TLDR: Skip to the updates. I am looking for a way to compress or lower the quality of video output, preferably after not directly after creating, but if that is the only way then so be it
Also if you know of any good cocoa pods which can accomplish this that would be good.
Update 3:
I am looking for a function which can output the compressed URL, and I should be able to control the compression quality...
Update 2:
After trying to make the function work in its current state it doe not work. Yeilding nil. I think as a result of the following:
let outputURL = urlToCompress
assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mov)
I am trying to compress video in swift. So far all solutions to this have been for use during creation. I am wondering if there is a way to compress after creation? only using the video URL?
If not then how can I make a compression function which compresses the video and returns the compressed URL?
Code I have been working with:
func compressVideo(videoURL: URL) -> URL {
let data = NSData(contentsOf: videoURL as URL)!
print("File size before compression: \(Double(data.length / 1048576)) mb")
let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + NSUUID().uuidString + ".mov")
compressVideoHelperMethod(inputURL: videoURL , outputURL: compressedURL) { (exportSession) in
}
return compressedURL
}
func compressVideoHelperMethod(inputURL: URL, outputURL: URL, handler:#escaping (_ exportSession: AVAssetExportSession?)-> Void) {
let urlAsset = AVURLAsset(url: inputURL, options: nil)
guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else {
handler(nil)
return
}
exportSession.outputURL = outputURL
exportSession.outputFileType = AVFileType.mov
exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronously { () -> Void in
handler(exportSession)
}
}
Update:
So I have found the code below. I am yet to test it, but I don't know how I can make it so that I choose the quality of the compression:
var assetWriter:AVAssetWriter?
var assetReader:AVAssetReader?
let bitrate:NSNumber = NSNumber(value:250000)
func compressFile(urlToCompress: URL, outputURL: URL, completion:#escaping (URL)->Void){
//video file to make the asset
var audioFinished = false
var videoFinished = false
let asset = AVAsset(url: urlToCompress);
let duration = asset.duration
let durationTime = CMTimeGetSeconds(duration)
print("Video Actual Duration -- \(durationTime)")
//create asset reader
do{
assetReader = try AVAssetReader(asset: asset)
} catch{
assetReader = nil
}
guard let reader = assetReader else{
fatalError("Could not initalize asset reader probably failed its try catch")
}
let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first!
let audioTrack = asset.tracks(withMediaType: AVMediaType.audio).first!
let videoReaderSettings: [String:Any] = [(kCVPixelBufferPixelFormatTypeKey as String?)!:kCVPixelFormatType_32ARGB ]
// ADJUST BIT RATE OF VIDEO HERE
if #available(iOS 11.0, *) {
let videoSettings:[String:Any] = [
AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey:self.bitrate],
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoHeightKey: videoTrack.naturalSize.height,
AVVideoWidthKey: videoTrack.naturalSize.width
]
} else {
// Fallback on earlier versions
}
let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
if reader.canAdd(assetReaderVideoOutput){
reader.add(assetReaderVideoOutput)
}else{
fatalError("Couldn't add video output reader")
}
if reader.canAdd(assetReaderAudioOutput){
reader.add(assetReaderAudioOutput)
}else{
fatalError("Couldn't add audio output reader")
}
let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil)
let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoReaderSettings)
videoInput.transform = videoTrack.preferredTransform
//we need to add samples to the video input
let videoInputQueue = DispatchQueue(label: "videoQueue")
let audioInputQueue = DispatchQueue(label: "audioQueue")
do{
assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mov)
}catch{
assetWriter = nil
}
guard let writer = assetWriter else{
fatalError("assetWriter was nil")
}
writer.shouldOptimizeForNetworkUse = true
writer.add(videoInput)
writer.add(audioInput)
writer.startWriting()
reader.startReading()
writer.startSession(atSourceTime: CMTime.zero)
let closeWriter:()->Void = {
if (audioFinished && videoFinished){
self.assetWriter?.finishWriting(completionHandler: {
print("------ Finish Video Compressing")
completion((self.assetWriter?.outputURL)!)
})
self.assetReader?.cancelReading()
}
}
audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
while(audioInput.isReadyForMoreMediaData){
let sample = assetReaderAudioOutput.copyNextSampleBuffer()
if (sample != nil){
audioInput.append(sample!)
}else{
audioInput.markAsFinished()
DispatchQueue.main.async {
audioFinished = true
closeWriter()
}
break;
}
}
}
videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
//request data here
while(videoInput.isReadyForMoreMediaData){
let sample = assetReaderVideoOutput.copyNextSampleBuffer()
if (sample != nil){
let timeStamp = CMSampleBufferGetPresentationTimeStamp(sample!)
let timeSecond = CMTimeGetSeconds(timeStamp)
let per = timeSecond / durationTime
print("Duration --- \(per)")
videoInput.append(sample!)
}else{
videoInput.markAsFinished()
DispatchQueue.main.async {
videoFinished = true
closeWriter()
}
break;
}
}
}
}
How can I change this to be able to set the quality? I am looking for compression of about 0.6
I am now playing around with the following code, the issue is that it keeps printing the error (does not seem to work):
func convertVideoToLowQuailty(withInputURL inputURL: URL?, outputURL: URL?, handler: #escaping (AVAssetExportSession?) -> Void) {
do {
if let outputURL = outputURL {
try FileManager.default.removeItem(at: outputURL)
}
} catch {
}
var asset: AVURLAsset? = nil
if let inputURL = inputURL {
asset = AVURLAsset(url: inputURL, options: nil)
}
var exportSession: AVAssetExportSession? = nil
if let asset = asset {
exportSession = AVAssetExportSession(asset: asset, presetName:AVAssetExportPresetMediumQuality)
}
exportSession?.outputURL = outputURL
exportSession?.outputFileType = .mov
exportSession?.exportAsynchronously(completionHandler: {
handler(exportSession)
})
}
func compressVideo(videoURL: URL) -> URL {
var outputURL = URL(fileURLWithPath: "/Users/alexramirezblonski/Desktop/output.mov")
convertVideoToLowQuailty(withInputURL: videoURL, outputURL: outputURL, handler: { exportSession in
print("fdshljfhdlasjkfdhsfsdljk")
if exportSession?.status == .completed {
print("completed\n", exportSession!.outputURL!)
outputURL = exportSession!.outputURL!
} else {
print("error\n")
outputURL = exportSession!.outputURL!//this needs to be fixed and may cause errors
}
})
return outputURL
}
I have looked at your code. Actually, you are compressing video in medium quality which will be around the same as the original video which you have. So, you have to change presetName in export session initialization as follow:
exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetLowQuality)
You can pass AVAssetExportPresetMediumQuality, So it's could be compressed as you expect.
There is the following list of format available to compress video.
1. Available from iOS 11.0
AVAssetExportPresetHEVCHighestQuality
AVAssetExportPresetHEVC1920x1080
AVAssetExportPresetHEVC3840x2160
2. Available from iOS 4.0
AVAssetExportPresetLowQuality
AVAssetExportPresetMediumQuality
AVAssetExportPresetHighestQuality
AVAssetExportPreset640x480
AVAssetExportPreset960x540
AVAssetExportPreset1280x720
AVAssetExportPreset1920x1080
AVAssetExportPreset3840x2160
AVAssetExportPresetAppleM4A
You can use above all format to compress your video based on your requirements.
I hope this will help you.
If you want more customizable compression filters using AVAssetWriter, consider this library that I wrote. You can compress the video with general quality settings or with more detail filters like bitrate, fps, scale, and more.
FYVideoCompressor().compressVideo(yourVideoPath, quality: .lowQuality) { result in
switch result {
case .success(let compressedVideoURL):
case .failure(let error):
}
}
or with more custom configuration:
let config = FYVideoCompressor.CompressionConfig(videoBitrate: 1000_000,
videomaxKeyFrameInterval: 10,
fps: 24,
audioSampleRate: 44100,
audioBitrate: 128_000,
fileType: .mp4,
scale: CGSize(width: 640, height: 480))
FYVideoCompressor().compressVideo(yourVideoPath, config: config) { result in
switch result {
case .success(let compressedVideoURL):
case .failure(let error):
}
}
More: Batch compression is now supported.
I am compressing videos with AVAssetWriter. If i set the video compression file to Quicktime movie it works fine, however i would like to export it to MPEG4, but it gives me this error while running:
In order to perform passthrough to file type public.mpeg-4, please provide a format hint in the AVAssetWriterInput initializer'
Here is the specific code where i declare the file type:
let videoInputQueue = DispatchQueue(label: "videoQueue")
let audioInputQueue = DispatchQueue(label: "audioQueue")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
let date = Date()
let documentsPath = NSTemporaryDirectory()
let outputPath = "\(documentsPath)/\(formatter.string(from: date)).mp4"
let newOutputUrl = URL(fileURLWithPath: outputPath)
do{
assetWriter = try AVAssetWriter(outputURL: newOutputUrl, fileType: AVFileTypeMPEG4)
}catch{
assetWriter = nil
}
guard let writer = assetWriter else{
fatalError("assetWriter was nil")
}
writer.shouldOptimizeForNetworkUse = true
writer.add(videoInput)
writer.add(audioInput)
Here is the full code for my compression:
func compressFile(urlToCompress: URL, completion:#escaping (URL)->Void){
//video file to make the asset
var audioFinished = false
var videoFinished = false
let asset = AVAsset(url: urlToCompress)
//create asset reader
do{
assetReader = try AVAssetReader(asset: asset)
} catch{
assetReader = nil
}
guard let reader = assetReader else{
fatalError("Could not initalize asset reader probably failed its try catch")
}
let videoTrack = asset.tracks(withMediaType: AVMediaTypeVideo).first!
let audioTrack = asset.tracks(withMediaType: AVMediaTypeAudio).first!
let videoReaderSettings: [String:Any] = [kCVPixelBufferPixelFormatTypeKey as String!:kCVPixelFormatType_32ARGB ]
// ADJUST BIT RATE OF VIDEO HERE
let videoSettings:[String:Any] = [
AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey:self.bitrate],
AVVideoCodecKey: AVVideoCodecH264,
AVVideoHeightKey: videoTrack.naturalSize.height,
AVVideoWidthKey: videoTrack.naturalSize.width
]
let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
if reader.canAdd(assetReaderVideoOutput){
reader.add(assetReaderVideoOutput)
}else{
fatalError("Couldn't add video output reader")
}
if reader.canAdd(assetReaderAudioOutput){
reader.add(assetReaderAudioOutput)
}else{
fatalError("Couldn't add audio output reader")
}
let audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: nil)
let videoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)
videoInput.transform = videoTrack.preferredTransform
//we need to add samples to the video input
let videoInputQueue = DispatchQueue(label: "videoQueue")
let audioInputQueue = DispatchQueue(label: "audioQueue")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
let date = Date()
let documentsPath = NSTemporaryDirectory()
let outputPath = "\(documentsPath)/\(formatter.string(from: date)).mp4"
let newOutputUrl = URL(fileURLWithPath: outputPath)
do{
assetWriter = try AVAssetWriter(outputURL: newOutputUrl, fileType: AVFileTypeMPEG4)
}catch{
assetWriter = nil
}
guard let writer = assetWriter else{
fatalError("assetWriter was nil")
}
writer.shouldOptimizeForNetworkUse = true
writer.add(videoInput)
writer.add(audioInput)
writer.startWriting()
reader.startReading()
writer.startSession(atSourceTime: kCMTimeZero)
let closeWriter:()->Void = {
if (audioFinished && videoFinished){
self.assetWriter?.finishWriting(completionHandler: {
self.checkFileSize(sizeUrl: (self.assetWriter?.outputURL)!, message: "The file size of the compressed file is: ")
completion((self.assetWriter?.outputURL)!)
print("Completed 1")
})
self.assetReader?.cancelReading()
}
}
audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
while(audioInput.isReadyForMoreMediaData){
let sample = assetReaderAudioOutput.copyNextSampleBuffer()
if (sample != nil){
audioInput.append(sample!)
}else{
audioInput.markAsFinished()
DispatchQueue.main.async {
audioFinished = true
closeWriter()
print("Completed 2")
}
break;
}
}
}
videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
//request data here
while(videoInput.isReadyForMoreMediaData){
let sample = assetReaderVideoOutput.copyNextSampleBuffer()
if (sample != nil){
videoInput.append(sample!)
}else{
videoInput.markAsFinished()
DispatchQueue.main.async {
videoFinished = true
print("Completed 3")
closeWriter()
}
break;
}
}
}
}
By creating your audio AVAssetWriterInput with nil outputSettings you indicate that you want to pass through your audio data. The assetWriterInputWithMediaType:outputSettings: header file comment says:
AVAssetWriter only supports passing through a restricted set of media types and subtypes. In order to pass through media data to files other than AVFileTypeQuickTimeMovie, a non-NULL format hint must be provided using +assetWriterInputWithMediaType:outputSettings:sourceFormatHint: instead of this method.
A format description is needed and luckily you can get one from the sample buffers you encounter:
let formatDesc = CMSampleBufferGetFormatDescription(anAudioSampleBuffer)!
let audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: nil, sourceFormatHint: formatDesc)
So if it's that easy to do, why doesn't AVAssetWriter do it for us? I guess because it would awkwardly push AVAssetWriter's usual initialization until some point after you've appended a few CMSampleBuffers, or (maybe?) because not all CMSampleBuffers have a format description.
I ran into this problem when trying to pass a .mp4 into an AVAssetWriter.
Specifically:
AVAssetReaderTrackOutput(track:,outputSettings:) outputSettings parameter to nil
and
AVAssetWriterInput(mediaType:,outputSettings:) outputSettings parameter to nil
Using this code from samkirkiles or for more a detailed look.
The problems are on lines 38 and 53:
// outputSettings: nil is the problem
let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil)
Using a .mov that line works fine but using a .mp4 the nil value in the outputSettings parameter causes a crash: outputSettings: nil
To fix it add a dictionary with some audioSettings into the outputSettings parameter:
This is for line 38:
let audioOutputSettingsDict: [String : Any] = [
AVFormatIDKey: kAudioFormatLinearPCM,
AVSampleRateKey: 44100
]
let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioOutputSettingsDict)
This is for line 53:
let audioInputSettingsDict: [String:Any] = [AVFormatIDKey : kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey : 2,
AVSampleRateKey : 44100.0,
AVEncoderBitRateKey: 128000 // he uses 250000 in his code via self.bitRate
]
let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioInputSettingsDict)
I am working on a task in which I have to trim the recorded video from particular start point to particular end point as entered or selected by user.
How am I supposed to do that. As I used UIVideoEditorController before but I don't want to use the default view and I want to trim the video directly.
let FinalUrlTosave = NSURL(string: "\(newURL)")
exportSession!.outputURL=FinalUrlTosave
exportSession!.shouldOptimizeForNetworkUse = true
// exportSession.outputFileType = AVFileTypeQuickTimeMovie
exportSession!.outputFileType = AVFileTypeQuickTimeMovie;
let start:CMTime
let duration:CMTime
var st = starttime.doubleValue
var ed = endTime.doubleValue
start = CMTimeMakeWithSeconds(st, 600)
duration = CMTimeMakeWithSeconds(ed, 600)
// let timeRangeForCurrentSlice = CMTimeRangeMake(start, duration)
let range = CMTimeRangeMake(start, duration);
exportSession!.timeRange = range
exportSession!.exportAsynchronouslyWithCompletionHandler({
switch exportSession!.status{
case AVAssetExportSessionStatus.Failed:
print("failed \(exportSession!.error)")
case AVAssetExportSessionStatus.Cancelled:
print("cancelled \(exportSession!.error)")
default:
print("complete....complete")
// self.SaveVideoToPhotoLibrary(destinationURL1!)
}
})
I am trying to achieve my goal using this but not succeeding.
Error message:
failed Optional(Error Domain=NSURLErrorDomain Code=-1100 "The
requested URL was not found on this server."
UserInfo={NSErrorFailingURLStringKey=file:///var/mobile/Containers/Data/Application/E68D3BFD-6923-4EA6-9FB3-C020CE4AA9D4/Documents/moment/jGq_9AUFa47s2ZiiPP4x.mp4,
NSErrorFailingURLKey=file:///var/mobile/Containers/Data/Application/E68D3BFD-6923-4EA6-9FB3-C020CE4AA9D4/Documents/moment/jGq_9AUFa47s2ZiiPP4x.mp4,
NSLocalizedDescription=The requested URL was not found on this
server., NSUnderlyingError=0x1553c220 {Error Domain=N
Error occured second time:
failed Optional(Error Domain=NSURLErrorDomain Code=-3000 "Cannot
create file" UserInfo={NSUnderlyingError=0x14e00000 {Error
Domain=NSOSStatusErrorDomain Code=-12124 "(null)"},
NSLocalizedDescription=Cannot create file})
I found my solution using this method and it works like a charm....
func cropVideo(sourceURL1: NSURL, statTime:Float, endTime:Float)
{
let manager = NSFileManager.defaultManager()
guard let documentDirectory = try? manager.URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) else {return}
guard let mediaType = "mp4" as? String else {return}
guard let url = sourceURL1 as? NSURL else {return}
if mediaType == kUTTypeMovie as String || mediaType == "mp4" as String {
let asset = AVAsset(URL: url)
let length = Float(asset.duration.value) / Float(asset.duration.timescale)
print("video length: \(length) seconds")
let start = statTime
let end = endTime
var outputURL = documentDirectory.URLByAppendingPathComponent("output")
do {
try manager.createDirectoryAtURL(outputURL, withIntermediateDirectories: true, attributes: nil)
let name = Moment.newName()
outputURL = outputURL.URLByAppendingPathComponent("\(name).mp4")
}catch let error {
print(error)
}
//Remove existing file
_ = try? manager.removeItemAtURL(outputURL)
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {return}
exportSession.outputURL = outputURL
exportSession.outputFileType = AVFileTypeMPEG4
let startTime = CMTime(seconds: Double(start ?? 0), preferredTimescale: 1000)
let endTime = CMTime(seconds: Double(end ?? length), preferredTimescale: 1000)
let timeRange = CMTimeRange(start: startTime, end: endTime)
exportSession.timeRange = timeRange
exportSession.exportAsynchronouslyWithCompletionHandler{
switch exportSession.status {
case .Completed:
print("exported at \(outputURL)")
self.saveVideoTimeline(outputURL)
case .Failed:
print("failed \(exportSession.error)")
case .Cancelled:
print("cancelled \(exportSession.error)")
default: break
}
}
}
}
Swift 5
func cropVideo(sourceURL1: URL, statTime:Float, endTime:Float)
{
let manager = FileManager.default
guard let documentDirectory = try? manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {return}
let mediaType = "mp4"
if mediaType == kUTTypeMovie as String || mediaType == "mp4" as String {
let asset = AVAsset(url: sourceURL1 as URL)
let length = Float(asset.duration.value) / Float(asset.duration.timescale)
print("video length: \(length) seconds")
let start = statTime
let end = endTime
var outputURL = documentDirectory.appendingPathComponent("output")
do {
try manager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)
outputURL = outputURL.appendingPathComponent("\(UUID().uuidString).\(mediaType)")
}catch let error {
print(error)
}
//Remove existing file
_ = try? manager.removeItem(at: outputURL)
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {return}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
let startTime = CMTime(seconds: Double(start ), preferredTimescale: 1000)
let endTime = CMTime(seconds: Double(end ), preferredTimescale: 1000)
let timeRange = CMTimeRange(start: startTime, end: endTime)
exportSession.timeRange = timeRange
exportSession.exportAsynchronously{
switch exportSession.status {
case .completed:
print("exported at \(outputURL)")
case .failed:
print("failed \(exportSession.error)")
case .cancelled:
print("cancelled \(exportSession.error)")
default: break
}
}
}
}
A swift4 version for this.
static func cropVideo(sourceURL: URL, startTime: Double, endTime: Double, completion: ((_ outputUrl: URL) -> Void)? = nil)
{
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let asset = AVAsset(url: sourceURL)
let length = Float(asset.duration.value) / Float(asset.duration.timescale)
print("video length: \(length) seconds")
var outputURL = documentDirectory.appendingPathComponent("output")
do {
try fileManager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)
outputURL = outputURL.appendingPathComponent("\(sourceURL.lastPathComponent).mp4")
}catch let error {
print(error)
}
//Remove existing file
try? fileManager.removeItem(at: outputURL)
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else { return }
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
let timeRange = CMTimeRange(start: CMTime(seconds: startTime, preferredTimescale: 1000),
end: CMTime(seconds: endTime, preferredTimescale: 1000))
exportSession.timeRange = timeRange
exportSession.exportAsynchronously {
switch exportSession.status {
case .completed:
print("exported at \(outputURL)")
completion?(outputURL)
case .failed:
print("failed \(exportSession.error.debugDescription)")
case .cancelled:
print("cancelled \(exportSession.error.debugDescription)")
default: break
}
}
}
This one does the job and it fixes the rotation problem.
extension AVAsset {
func assetByTrimming(startTime: CMTime, endTime: CMTime) throws -> AVAsset {
let duration = CMTimeSubtract(endTime, startTime)
let timeRange = CMTimeRange(start: startTime, duration: duration)
let composition = AVMutableComposition()
do {
for track in tracks {
let compositionTrack = composition.addMutableTrack(withMediaType: track.mediaType, preferredTrackID: track.trackID)
compositionTrack?.preferredTransform = track.preferredTransform
try compositionTrack?.insertTimeRange(timeRange, of: track, at: CMTime.zero)
}
} catch let error {
throw TrimError("error during composition", underlyingError: error)
}
return composition
}
struct TrimError: Error {
let description: String
let underlyingError: Error?
init(_ description: String, underlyingError: Error? = nil) {
self.description = "TrimVideo: " + description
self.underlyingError = underlyingError
}
}
func cropVideo1(_ sourceURL1: URL, statTime:Float, endTime:Float){
let videoAsset: AVAsset = AVAsset(url: sourceURL1) as AVAsset
let composition = AVMutableComposition()
composition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID())
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = CGSize(width: 1280, height: 768)
videoComposition.frameDuration = CMTimeMake(8, 15)
let instruction = AVMutableVideoCompositionInstruction()
let length = Float(videoAsset.duration.value)
print(length)
instruction.timeRange = CMTimeRangeMake(kCMTimeZero, CMTimeMakeWithSeconds(60, 30))
let start = statTime
let end = endTime
let exportSession = AVAssetExportSession(asset: videoAsset, presetName: AVAssetExportPresetHighestQuality)!
exportSession.outputFileType = AVFileTypeMPEG4
let startTime = CMTime(seconds: Double(start ), preferredTimescale: 1000)
let endTime = CMTime(seconds: Double(end ), preferredTimescale: 1000)
let timeRange = CMTimeRange(start: startTime, end: endTime)
exportSession.timeRange = timeRange
let formatter = DateFormatter()
formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
let date = Date()
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let outputPath = "\(documentsPath)/\(formatter.string(from: date)).mp4"
let outputURL = URL(fileURLWithPath: outputPath)
exportSession.outputURL = outputURL
exportSession.outputFileType = AVFileTypeQuickTimeMovie
print("sucess")
exportSession.exportAsynchronously(completionHandler: { () -> Void in
DispatchQueue.main.async(execute: {
self.exportDidFinish(exportSession)
print("sucess")
})
})
}
func exportDidFinish(_ session: AVAssetExportSession) {
if session.status == AVAssetExportSessionStatus.completed {
let outputURL = session.outputURL
let library = ALAssetsLibrary()
if library.videoAtPathIs(compatibleWithSavedPhotosAlbum: outputURL) {
library.writeVideoAtPath(toSavedPhotosAlbum: outputURL) { alAssetURL, error in
if error != nil {
DispatchQueue.main.async(execute: {
print("Failed to save video")
})
} else {
DispatchQueue.main.async(execute: {
Print("Sucessfully saved Video")
})
}
self.activityIndicator.stopAnimating()
}
}
}
}