Async API for video export

// FREEBIE
pull/1/head
Michael Kirk 8 years ago
parent 21fd7b040e
commit 538b3e5fd5

@ -5,6 +5,7 @@
import Foundation import Foundation
import MobileCoreServices import MobileCoreServices
import SignalServiceKit import SignalServiceKit
import PromiseKit
import AVFoundation import AVFoundation
enum SignalAttachmentError: Error { enum SignalAttachmentError: Error {
@ -781,30 +782,32 @@ public class SignalAttachment: NSObject {
return attachment return attachment
} }
if isInputVideoValidOutputVideo(dataSource: dataSource, dataUTI: dataUTI) { if !isInputVideoValidOutputVideo(dataSource: dataSource, dataUTI: dataUTI) {
return newAttachment(dataSource: dataSource, // Most people won't hit this because we convert video when picked from the media picker
dataUTI: dataUTI, // But the current API allos sending videos that some Signal clients will not
validUTISet: videoUTISet, // be able to view. (e.g. when picked from document picker)
maxFileSize: kMaxFileSizeVideo) owsFail("building video with invalid output, migrate to async API using compressVideoAsMp4")
} else {
// convert to mp4
return compressVideoAsMp4(dataSource: dataSource, dataUTI: dataUTI)
} }
return newAttachment(dataSource: dataSource,
dataUTI: dataUTI,
validUTISet: videoUTISet,
maxFileSize: kMaxFileSizeVideo)
} }
class var videoTempPath: URL { private class var videoTempPath: URL {
let videoDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("video") let videoDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("video")
OWSFileSystem.ensureDirectoryExists(videoDir.path) OWSFileSystem.ensureDirectoryExists(videoDir.path)
return videoDir return videoDir
} }
class func compressVideoAsMp4(dataSource: DataSource, dataUTI: String) -> SignalAttachment { public class func compressVideoAsMp4(dataSource: DataSource, dataUTI: String) -> (Promise<SignalAttachment>, AVAssetExportSession?) {
Logger.debug("\(self.TAG) in \(#function)") Logger.debug("\(self.TAG) in \(#function)")
guard let url = dataSource.dataUrl() else { guard let url = dataSource.dataUrl() else {
let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: dataUTI) let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .missingData attachment.error = .missingData
return attachment return (Promise(value: attachment), nil)
} }
let asset = AVAsset(url: url) let asset = AVAsset(url: url)
@ -812,7 +815,7 @@ public class SignalAttachment: NSObject {
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality) else { guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality) else {
let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: dataUTI) let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .couldNotConvertToMpeg4 attachment.error = .couldNotConvertToMpeg4
return attachment return (Promise(value: attachment), nil)
} }
exportSession.shouldOptimizeForNetworkUse = true exportSession.shouldOptimizeForNetworkUse = true
@ -821,32 +824,47 @@ public class SignalAttachment: NSObject {
let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4") let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
exportSession.outputURL = exportURL exportSession.outputURL = exportURL
let (promise, fulfill, _) = Promise<SignalAttachment>.pending()
Logger.debug("\(self.TAG) starting video export") Logger.debug("\(self.TAG) starting video export")
let semaphore = DispatchSemaphore(value: 0)
exportSession.exportAsynchronously { exportSession.exportAsynchronously {
Logger.debug("\(self.TAG) Completed video export") Logger.debug("\(self.TAG) Completed video export")
semaphore.signal() let baseFilename = dataSource.sourceFilename
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
guard let dataSource = DataSourcePath.dataSource(with: exportURL) else {
owsFail("Failed to build data source for exported video URL")
let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .couldNotConvertToMpeg4
fulfill(attachment)
return
}
dataSource.sourceFilename = mp4Filename
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
fulfill(attachment)
} }
// FIXME make the API async, return progress. return (promise, exportSession)
Logger.debug("\(self.TAG) Waiting for video export") }
semaphore.wait()
Logger.debug("\(self.TAG) Done waiting for video export")
let baseFilename = dataSource.sourceFilename public class func isInvalidVideo(dataSource: DataSource, dataUTI: String) -> Bool {
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4") guard videoUTISet.contains(dataUTI) else {
// not a video
return false
}
guard let dataSource = DataSourcePath.dataSource(with: exportURL) else { guard isInputVideoValidOutputVideo(dataSource: dataSource, dataUTI: dataUTI) else {
owsFail("Failed to build data source for exported video URL") // found a video which needs to be converted
let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: dataUTI) return true
attachment.error = .couldNotConvertToMpeg4
return attachment
} }
dataSource.sourceFilename = mp4Filename
return SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String) // It is a video, but it's not invalid
return false
} }
class func isInputVideoValidOutputVideo(dataSource: DataSource?, dataUTI: String) -> Bool { private class func isInputVideoValidOutputVideo(dataSource: DataSource?, dataUTI: String) -> Bool {
guard let dataSource = dataSource else { guard let dataSource = dataSource else {
return false return false
} }

@ -448,7 +448,7 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
// TODO accept other data types // TODO accept other data types
// TODO whitelist attachment types // TODO whitelist attachment types
// TODO coerce when necessary and possible // TODO coerce when necessary and possible
return promise.then { (url: URL) -> SignalAttachment in return promise.then { (url: URL) -> Promise<SignalAttachment> in
guard let dataSource = DataSourcePath.dataSource(with: url) else { guard let dataSource = DataSourcePath.dataSource(with: url) else {
throw ShareViewControllerError.assertionError(description: "Unable to read attachment data") throw ShareViewControllerError.assertionError(description: "Unable to read attachment data")
} }
@ -464,9 +464,14 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
} }
} }
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality:.medium) guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else {
let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium)
// TODO show progress with exportSession
return promise
}
return attachment let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType)
return Promise(value: attachment)
} }
} }
} }

Loading…
Cancel
Save