diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 28ae423b7..48e6936f3 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -62,6 +62,7 @@ #import #import #import +#import #import #import #import @@ -2722,50 +2723,33 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { presentFromViewController:self canCancel:YES backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { - AVAsset *video = [AVAsset assetWithURL:movieURL]; - AVAssetExportSession *exportSession = - [AVAssetExportSession exportSessionWithAsset:video - presetName:AVAssetExportPresetMediumQuality]; - exportSession.shouldOptimizeForNetworkUse = YES; - exportSession.outputFileType = AVFileTypeMPEG4; - NSURL *compressedVideoUrl = [[self videoTempFolder] - URLByAppendingPathComponent:[[[NSUUID UUID] UUIDString] - stringByAppendingPathExtension:@"mp4"]]; - exportSession.outputURL = compressedVideoUrl; - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - dispatch_async(dispatch_get_main_queue(), ^{ - OWSAssert([NSThread isMainThread]); - - if (modalActivityIndicator.wasCancelled) { - return; + DataSource *dataSource = [DataSourcePath dataSourceWithURL:movieURL]; + dataSource.sourceFilename = filename; + VideoCompressionResult *compressionResult = + [SignalAttachment compressVideoAsMp4WithDataSource:dataSource + dataUTI:(NSString *)kUTTypeMPEG4]; + [compressionResult.attachmentPromise retainUntilComplete]; + + compressionResult.attachmentPromise.then(^(SignalAttachment *attachment) { + OWSAssert([NSThread isMainThread]); + OWSAssert([attachment isKindOfClass:[SignalAttachment class]]); + + if (modalActivityIndicator.wasCancelled) { + return; + } + + [modalActivityIndicator dismissWithCompletion:^{ + if (!attachment || [attachment hasError]) { + DDLogError(@"%@ %s Invalid attachment: %@.", + self.logTag, + __PRETTY_FUNCTION__, + attachment ? [attachment errorName] : @"Missing data"); + [self showErrorAlertForAttachment:attachment]; + } else { + [self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog]; } - - [modalActivityIndicator dismissWithCompletion:^{ - - NSString *baseFilename = filename.stringByDeletingPathExtension; - NSString *mp4Filename = [baseFilename stringByAppendingPathExtension:@"mp4"]; - DataSource *_Nullable dataSource = - [DataSourcePath dataSourceWithURL:compressedVideoUrl]; - [dataSource setSourceFilename:mp4Filename]; - - // Remove temporary file when complete. - [dataSource setShouldDeleteOnDeallocation]; - SignalAttachment *attachment = - [SignalAttachment attachmentWithDataSource:dataSource - dataUTI:(NSString *)kUTTypeMPEG4]; - if (!attachment || [attachment hasError]) { - DDLogError(@"%@ %s Invalid attachment: %@.", - self.logTag, - __PRETTY_FUNCTION__, - attachment ? [attachment errorName] : @"Missing data"); - [self showErrorAlertForAttachment:attachment]; - } else { - [self tryToSendAttachmentIfApproved:attachment - skipApprovalDialog:skipApprovalDialog]; - } - }]; - }); - }]; + }]; + }); }]; } diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index d1a590cba..c9758defc 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -866,6 +866,27 @@ public class SignalAttachment: NSObject { return (promise, exportSession) } + @objc + public class VideoCompressionResult: NSObject { + @objc + public let attachmentPromise: AnyPromise + + @objc + public let exportSession: AVAssetExportSession? + + fileprivate init(attachmentPromise: Promise, exportSession: AVAssetExportSession?) { + self.attachmentPromise = AnyPromise(attachmentPromise) + self.exportSession = exportSession + super.init() + } + } + + @objc + public class func compressVideoAsMp4(dataSource: DataSource, dataUTI: String) -> VideoCompressionResult { + let (attachmentPromise, exportSession) = compressVideoAsMp4(dataSource: dataSource, dataUTI: dataUTI) + return VideoCompressionResult(attachmentPromise: attachmentPromise, exportSession: exportSession) + } + public class func isInvalidVideo(dataSource: DataSource, dataUTI: String) -> Bool { guard videoUTISet.contains(dataUTI) else { // not a video diff --git a/SignalMessaging/categories/Promise+retainUntilComplete.swift b/SignalMessaging/categories/Promise+retainUntilComplete.swift index d3a74a61a..8f45c95c6 100644 --- a/SignalMessaging/categories/Promise+retainUntilComplete.swift +++ b/SignalMessaging/categories/Promise+retainUntilComplete.swift @@ -4,6 +4,21 @@ import PromiseKit +public extension AnyPromise { + /** + * Sometimes there isn't a straight forward candidate to retain a promise, in that case we tell the + * promise to self retain, until it completes to avoid the risk it's GC'd before completion. + */ + func retainUntilComplete() { + // Unfortunately, there is (currently) no way to surpress the + // compiler warning: "Variable 'retainCycle' was written to, but never read" + var retainCycle: AnyPromise? = self + self.always { + retainCycle = nil + } + } +} + public extension Promise { /** * Sometimes there isn't a straight forward candidate to retain a promise, in that case we tell the