diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index 868eda051..d1a590cba 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -792,7 +792,7 @@ public class SignalAttachment: NSObject { // Most people won't hit this because we convert video when picked from the media picker // But the current API allows sending videos that some Signal clients will not // be able to view. (e.g. when picked from document picker) -// owsFail("building video with invalid output, migrate to async API using compressVideoAsMp4") + owsFail("building video with invalid output, migrate to async API using compressVideoAsMp4") } return newAttachment(dataSource: dataSource, @@ -801,6 +801,17 @@ public class SignalAttachment: NSObject { maxFileSize: kMaxFileSizeVideo) } + public class func copyToVideoTempDir(url fromUrl: URL) throws -> URL { + let baseDir = SignalAttachment.videoTempPath.appendingPathComponent(UUID().uuidString, isDirectory: true) + OWSFileSystem.ensureDirectoryExists(baseDir.path) + let toUrl = baseDir.appendingPathComponent(fromUrl.lastPathComponent) + + Logger.debug("\(self.logTag) moving \(fromUrl) -> \(toUrl)") + try FileManager.default.copyItem(at: fromUrl, to: toUrl) + + return toUrl + } + private class var videoTempPath: URL { let videoDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("video") OWSFileSystem.ensureDirectoryExists(videoDir.path) diff --git a/SignalServiceKit/src/Util/MIMETypeUtil.m b/SignalServiceKit/src/Util/MIMETypeUtil.m index e779d2dbb..ff9f7e0de 100644 --- a/SignalServiceKit/src/Util/MIMETypeUtil.m +++ b/SignalServiceKit/src/Util/MIMETypeUtil.m @@ -211,19 +211,20 @@ NSString *const kSyncMessageFileExtension = @"bin"; } + (BOOL)isSupportedVideoFile:(NSString *)filePath { - return [[self supportedVideoExtensionTypesToMIMETypes] objectForKey:[filePath pathExtension]] != nil; + return [[self supportedVideoExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; } + (BOOL)isSupportedAudioFile:(NSString *)filePath { - return [[self supportedAudioExtensionTypesToMIMETypes] objectForKey:[filePath pathExtension]] != nil; + return [[self supportedAudioExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; } + (BOOL)isSupportedImageFile:(NSString *)filePath { - return [[self supportedImageExtensionTypesToMIMETypes] objectForKey:[filePath pathExtension]] != nil; + return [[self supportedImageExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; } + (BOOL)isSupportedAnimatedFile:(NSString *)filePath { - return [[self supportedAnimatedExtensionTypesToMIMETypes] objectForKey:[filePath pathExtension]] != nil; + return + [[self supportedAnimatedExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; } + (nullable NSString *)getSupportedExtensionFromVideoMIMEType:(NSString *)supportedMIMEType diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index ada9bb84d..47ea8c284 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -450,7 +450,30 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE // TODO accept other data types // TODO whitelist attachment types // TODO coerce when necessary and possible - return promise.then { (url: URL) -> Promise in + return promise.then { (itemUrl: URL) -> Promise in + + let url: URL = try { + // iOS converts at least some video formats (e.g. com.apple.quicktime-movie) into mp4s as part of the + // NSItemProvider `loadItem` API. + // However, for some reason, AVFoundation operations such as generating a preview image and playing + // the url in the AVMoviePlayer fail on these converted formats until unless we first copy the media + // into our container. (These operations succeed when resending mp4s received and sent in Signal) + // + // I don't understand why this is, and I haven't found any relevant documentation in the NSItemProvider + // or AVFoundation docs. + // + // I *did* verify that the size and sah256 sum of the original url matches that of the copied url. + // Perhaps the AVFoundation API's require some extra file system permssion we don't have in the + // passed through URL. + if MIMETypeUtil.isSupportedVideoFile(itemUrl.path) { + return try SignalAttachment.copyToVideoTempDir(url: itemUrl) + } else { + return itemUrl + } + }() + + Logger.debug("\(self.logTag) building DataSource with url: \(url)") + guard let dataSource = DataSourcePath.dataSource(with: url) else { throw ShareViewControllerError.assertionError(description: "Unable to read attachment data") } @@ -467,7 +490,8 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE } guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { - // TODO show progress with exportSession + // This can happen, e.g. when sharing a quicktime-video from iCloud drive. + let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) // Can we move this process to the end of the share flow rather than up front? @@ -488,8 +512,6 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE return promise } - // DO NOT COMMIT -// specificUTIType = "com.apple.quicktime-movie" let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) return Promise(value: attachment) }