Ich versuche, Videos zu komprimieren, die mit der Benutzerkamera von UIImagePickerController aufgenommen wurden (kein vorhandenes Video, sondern eines im laufenden Betrieb), um sie auf meinen Server hochzuladen. 45 mb auf neueren Qualitätskameras.
Hier ist der Code, um eine Komprimierung in Swift für iOS 8 durchzuführen, und er lässt sich wunderbar komprimieren. Ich gehe einfach von 35 MB auf 2,1 MB herunter.
func convertVideo(inputUrl: NSURL, outputURL: NSURL)
{
//setup video writer
var videoAsset = AVURLAsset(URL: inputUrl, options: nil) as AVAsset
var videoTrack = videoAsset.tracksWithMediaType(AVMediaTypeVideo)[0] as AVAssetTrack
var videoSize = videoTrack.naturalSize
var videoWriterCompressionSettings = Dictionary(dictionaryLiteral:(AVVideoAverageBitRateKey,NSNumber(integer:960000)))
var videoWriterSettings = Dictionary(dictionaryLiteral:(AVVideoCodecKey,AVVideoCodecH264),
(AVVideoCompressionPropertiesKey,videoWriterCompressionSettings),
(AVVideoWidthKey,videoSize.width),
(AVVideoHeightKey,videoSize.height))
var videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoWriterSettings)
videoWriterInput.expectsMediaDataInRealTime = true
videoWriterInput.transform = videoTrack.preferredTransform
var videoWriter = AVAssetWriter(URL: outputURL, fileType: AVFileTypeQuickTimeMovie, error: nil)
videoWriter.addInput(videoWriterInput)
var videoReaderSettings: [String:AnyObject] = [kCVPixelBufferPixelFormatTypeKey:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]
var videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
var videoReader = AVAssetReader(asset: videoAsset, error: nil)
videoReader.addOutput(videoReaderOutput)
//setup audio writer
var audioWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: nil)
audioWriterInput.expectsMediaDataInRealTime = false
videoWriter.addInput(audioWriterInput)
//setup audio reader
var audioTrack = videoAsset.tracksWithMediaType(AVMediaTypeAudio)[0] as AVAssetTrack
var audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil) as AVAssetReaderOutput
var audioReader = AVAssetReader(asset: videoAsset, error: nil)
audioReader.addOutput(audioReaderOutput)
videoWriter.startWriting()
//start writing from video reader
videoReader.startReading()
videoWriter.startSessionAtSourceTime(kCMTimeZero)
//dispatch_queue_t processingQueue = dispatch_queue_create("processingQueue", nil)
var queue = dispatch_queue_create("processingQueue", nil)
videoWriterInput.requestMediaDataWhenReadyOnQueue(queue, usingBlock: { () -> Void in
println("Export starting")
while videoWriterInput.readyForMoreMediaData
{
var sampleBuffer:CMSampleBufferRef!
sampleBuffer = videoReaderOutput.copyNextSampleBuffer()
if (videoReader.status == AVAssetReaderStatus.Reading && sampleBuffer != nil)
{
videoWriterInput.appendSampleBuffer(sampleBuffer)
}
else
{
videoWriterInput.markAsFinished()
if videoReader.status == AVAssetReaderStatus.Completed
{
if audioReader.status == AVAssetReaderStatus.Reading || audioReader.status == AVAssetReaderStatus.Completed
{
}
else {
audioReader.startReading()
videoWriter.startSessionAtSourceTime(kCMTimeZero)
var queue2 = dispatch_queue_create("processingQueue2", nil)
audioWriterInput.requestMediaDataWhenReadyOnQueue(queue2, usingBlock: { () -> Void in
while audioWriterInput.readyForMoreMediaData
{
var sampleBuffer:CMSampleBufferRef!
sampleBuffer = audioReaderOutput.copyNextSampleBuffer()
println(sampleBuffer == nil)
if (audioReader.status == AVAssetReaderStatus.Reading && sampleBuffer != nil)
{
audioWriterInput.appendSampleBuffer(sampleBuffer)
}
else
{
audioWriterInput.markAsFinished()
if (audioReader.status == AVAssetReaderStatus.Completed)
{
videoWriter.finishWritingWithCompletionHandler({ () -> Void in
println("Finished writing video asset.")
self.videoUrl = outputURL
var data = NSData(contentsOfURL: outputURL)!
println("Byte Size After Compression: \(data.length / 1048576) mb")
println(videoAsset.playable)
//Networking().uploadVideo(data, fileName: "Test2")
self.dismissViewControllerAnimated(true, completion: nil)
})
break
}
}
}
})
break
}
}
}// Second if
}//first while
})// first block
// return
}
Hier ist der Code für meinen UIImagePickerController, der die Methode compress aufruft
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject])
{
// Extract the media type from selection
let type = info[UIImagePickerControllerMediaType] as String
if (type == kUTTypeMovie)
{
self.videoUrl = info[UIImagePickerControllerMediaURL] as? NSURL
var uploadUrl = NSURL.fileURLWithPath(NSTemporaryDirectory().stringByAppendingPathComponent("captured").stringByAppendingString(".mov"))
var data = NSData(contentsOfURL: self.videoUrl!)!
println("Size Before Compression: \(data.length / 1048576) mb")
self.convertVideo(self.videoUrl!, outputURL: uploadUrl!)
// Get the video from the info and set it appropriately.
/*self.dismissViewControllerAnimated(true, completion: { () -> Void in
//self.next.enabled = true
})*/
}
}
Wie ich oben erwähnte, funktioniert dies bis zur Reduzierung der Dateigröße, aber wenn ich die Datei zurück bekomme (sie ist immer noch vom Typ .mov), kann Quicktime sie nicht abspielen. Quicktime versucht es zunächst zu konvertieren, schlägt jedoch nach der Hälfte der Zeit (1-2 Sekunden nach dem Öffnen der Datei) fehl. Ich habe die Videodatei sogar in AVPlayerController getestet, aber es gibt keine Informationen über den Film, sondern nur eine Wiedergabetaste ohne ameisenladen und ohne länge nur "-" wo die zeit normalerweise im player steht. IE eine beschädigte Datei, die nicht abgespielt werden kann.
Ich bin mir sicher, dass es etwas mit den Einstellungen zu tun hat, mit denen das Asset herausgeschrieben wird, ob es sich um das Video- oder das Audio-Schreiben handelt. Ich bin mir überhaupt nicht sicher. Es könnte sogar das Lesen des Assets sein, das dazu führt, dass es beschädigt wird. Ich habe versucht, die Variablen zu ändern und verschiedene Schlüssel zum Lesen und Schreiben zu setzen, aber ich habe nicht die richtige Kombination gefunden, und das ist scheiße, dass ich komprimieren kann, aber eine beschädigte Datei daraus bekomme. Ich bin mir überhaupt nicht sicher und jede Hilfe wäre dankbar. Pleeeeeeeeease.
Diese Antwort wurde komplett umgeschrieben und mit Anmerkungen versehen, um Swift 4.0 zu unterstützen. Denken Sie daran, dass Sie durch Ändern der Werte für AVFileType
und presetName
die endgültige Ausgabe in Bezug auf Größe und Qualität optimieren können.
import AVFoundation
extension ViewController: AVCaptureFileOutputRecordingDelegate {
// Delegate function has been updated
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
// This code just exists for getting the before size. You can remove it from production code
do {
let data = try Data(contentsOf: outputFileURL)
print("File size before compression: \(Double(data.count / 1048576)) mb")
} catch {
print("Error: \(error)")
}
// This line creates a generic filename based on UUID, but you may want to use your own
// The extension must match with the AVFileType enum
let path = NSTemporaryDirectory() + UUID().uuidString + ".m4v"
let outputURL = URL.init(fileURLWithPath: path)
let urlAsset = AVURLAsset(url: outputURL)
// You can change the presetName value to obtain different results
if let exportSession = AVAssetExportSession(asset: urlAsset,
presetName: AVAssetExportPresetMediumQuality) {
exportSession.outputURL = outputURL
// Changing the AVFileType enum gives you different options with
// varying size and quality. Just ensure that the file extension
// aligns with your choice
exportSession.outputFileType = AVFileType.mov
exportSession.exportAsynchronously {
switch exportSession.status {
case .unknown: break
case .waiting: break
case .exporting: break
case .completed:
// This code only exists to provide the file size after compression. Should remove this from production code
do {
let data = try Data(contentsOf: outputFileURL)
print("File size after compression: \(Double(data.count / 1048576)) mb")
} catch {
print("Error: \(error)")
}
case .failed: break
case .cancelled: break
}
}
}
}
}
Nachfolgend finden Sie die ursprüngliche Antwort für Swift 3.0:
extension ViewController: AVCaptureFileOutputRecordingDelegate {
func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
guard let data = NSData(contentsOf: outputFileURL as URL) else {
return
}
print("File size before compression: \(Double(data.length / 1048576)) mb")
let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + NSUUID().uuidString + ".m4v")
compressVideo(inputURL: outputFileURL as URL, outputURL: compressedURL) { (exportSession) in
guard let session = exportSession else {
return
}
switch session.status {
case .unknown:
break
case .waiting:
break
case .exporting:
break
case .completed:
guard let compressedData = NSData(contentsOf: compressedURL) else {
return
}
print("File size after compression: \(Double(compressedData.length / 1048576)) mb")
case .failed:
break
case .cancelled:
break
}
}
}
func compressVideo(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 = AVFileTypeQuickTimeMovie
exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronously { () -> Void in
handler(exportSession)
}
}
}
Herausgefunden! Ok Es gab also 2 Probleme: 1 Problem war mit dem Funktionsaufruf videoWriter.finishWritingWithCompletionHandler. Wenn dieser Beendigungsblock ausgeführt wird, bedeutet dies NICHT, dass der Video Writer das Schreiben in die Ausgabe-URL beendet hat. Ich musste also überprüfen, ob der Status abgeschlossen war, bevor ich die eigentliche Videodatei hochlud. Es ist eine Art Hack, aber das habe ich getan
videoWriter.finishWritingWithCompletionHandler({() -> Void in
while true
{
if videoWriter.status == .Completed
{
var data = NSData(contentsOfURL: outputURL)!
println("Finished: Byte Size After Compression: \(data.length / 1048576) mb")
Networking().uploadVideo(data, fileName: "Video")
self.dismissViewControllerAnimated(true, completion: nil)
break
}
}
})
Das zweite Problem, das ich hatte, war ein Fehlgeschlagener Status, und das lag daran, dass ich in das gleiche temporäre Verzeichnis geschrieben habe, wie es im Code für die UIImagePickerController-Methode didFinishSelectingMediaWithInfo in meiner Frage angegeben ist. Also habe ich nur das aktuelle Datum als Verzeichnisnamen verwendet, damit es eindeutig ist.
var uploadUrl = NSURL.fileURLWithPath(NSTemporaryDirectory().stringByAppendingPathComponent("\(NSDate())").stringByAppendingString(".mov"))
[EDIT]: BESSERE LÖSUNG
Ok, also nach vielen Experimenten und Monaten später habe ich eine verdammt gute und viel einfachere Lösung gefunden, um ein Video von 45 MB auf 1,42 MB mit ziemlich guter Qualität runter zu bringen.
Unten sehen Sie die aufzurufende Funktion anstelle der ursprünglichen Funktion convertVideo. Beachten Sie, dass ich meinen eigenen Completion-Handler-Parameter schreiben musste, der aufgerufen wird, nachdem der asynchrone Export abgeschlossen wurde. Ich habe es einfach Handler genannt.
func compressVideo(inputURL: NSURL, outputURL: NSURL, handler:(session: AVAssetExportSession)-> Void)
{
var urlAsset = AVURLAsset(URL: inputURL, options: nil)
var exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality)
exportSession.outputURL = outputURL
exportSession.outputFileType = AVFileTypeQuickTimeMovie
exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronouslyWithCompletionHandler { () -> Void in
handler(session: exportSession)
}
}
Und hier ist der Code in der Funktion uiimagepickercontrollerDidFinisPickingMediaWithInfo.
self.compressVideo(inputURL!, outputURL: uploadUrl!, handler: { (handler) -> Void in
if handler.status == AVAssetExportSessionStatus.Completed
{
var data = NSData(contentsOfURL: uploadUrl!)
println("File size after compression: \(Double(data!.length / 1048576)) mb")
self.picker.dismissViewControllerAnimated(true, completion: nil)
}
else if handler.status == AVAssetExportSessionStatus.Failed
{
let alert = UIAlertView(title: "Uh oh", message: " There was a problem compressing the video maybe you can try again later. Error: \(handler.error.localizedDescription)", delegate: nil, cancelButtonTitle: "Okay")
alert.show()
})
}
})
Ihre Konvertierungsmethode ist asynchron, hat jedoch keinen Abschlussblock. Wie kann Ihr Code wissen, wann die Datei fertig ist? Möglicherweise verwenden Sie die Datei, bevor sie vollständig geschrieben wurde.
Die Konvertierung selbst sieht ebenfalls seltsam aus - Audio und Video werden normalerweise parallel und nicht in Reihe geschrieben.
Ihre wundersame Komprimierungsrate weist möglicherweise darauf hin, dass Sie weniger Frames geschrieben haben, als Sie tatsächlich denken.
Hier ist der Code, der mit Swift 4.0 kompatibel ist Videogröße komprimieren, bevor eine E-Mail in Swift angehängt wird Sie können auch den Fortschritt der Videokomprimierung verfolgen