MediaConverter.m 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2016-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "MediaConverter.h"
  21. #import "UIImage+Resize.h"
  22. #import "UserSettings.h"
  23. #import "SDAVAssetExportSession.h"
  24. #import "UIDefines.h"
  25. #import "ValidationLogger.h"
  26. #import <ThreemaFramework/ThreemaFramework-Swift.h>
  27. #ifdef DEBUG
  28. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  29. #else
  30. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  31. #endif
  32. @implementation MediaConverter
  33. + (CGSize)getWebThumbnailSizeForImageData:(NSData *)data {
  34. UIImage *image = [self scaleImageData:data toMaxSize:kWebClientMediaThumbnailSize];
  35. return CGSizeMake(image.size.width, image.size.height);
  36. }
  37. + (UIImage*)getThumbnailForVideo:(AVAsset *)asset {
  38. AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:asset];
  39. gen.appliesPreferredTrackTransform = YES;
  40. CMTime time = CMTimeMakeWithSeconds(0.0, 600);
  41. NSError *error = nil;
  42. CMTime actualTime;
  43. CGImageRef image = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
  44. UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
  45. CGImageRelease(image);
  46. CGFloat size = (CGFloat) [self thumbnailSizeForCurrentDevice];
  47. return [self scaleImage:thumbnail toMaxSize:size];
  48. }
  49. + (UIImage*)getThumbnailForImage:(UIImage *)orig {
  50. NSUInteger size = [self thumbnailSizeForCurrentDevice];
  51. return [self scaleImage:orig toMaxSize:size];
  52. }
  53. + (NSData *)getWebPreviewData:(NSData *)orig {
  54. UIImage *image = [self scaleImageData:orig toMaxSize:kWebClientMediaPreviewSize];
  55. return UIImageJPEGRepresentation(image, kWebClientMediaQuality);
  56. }
  57. + (NSData *)getWebThumbnailData:(NSData *)orig {
  58. UIImage *image = [self scaleImageData:orig toMaxSize:kWebClientMediaThumbnailSize];
  59. return UIImageJPEGRepresentation(image, kWebClientMediaQuality);
  60. }
  61. + (UIImage*)scaleImage:(UIImage*)orig toMaxSize:(CGFloat)maxSize {
  62. // Check if we need to scale this image at all
  63. if (orig.size.width <= maxSize && orig.size.height <= maxSize) {
  64. // to rotate the image to the correct orientation
  65. return [orig resizedImage:CGSizeMake(orig.size.width, orig.size.height) interpolationQuality:kCGInterpolationLow];
  66. }
  67. UIImage *scaled = [orig resizedImageWithContentMode:UIViewContentModeScaleAspectFit bounds:CGSizeMake(maxSize, maxSize) interpolationQuality:kCGInterpolationLow];
  68. return scaled;
  69. }
  70. + (UIImage*)scaleImageData:(NSData *)imageData toMaxSize:(CGFloat)maxSize {
  71. CGImageSourceRef src = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, (__bridge CFDictionaryRef) @{
  72. (id) kCGImageSourceShouldCache : @NO});
  73. NSMutableDictionary *imageRefOptions = [MediaConverter imageRefOptionsForSize:maxSize];
  74. CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(src, 0, (__bridge CFDictionaryRef) imageRefOptions);
  75. UIImage *scaled = [UIImage imageWithCGImage:scaledImageRef];
  76. CGImageRelease(scaledImageRef);
  77. CFRelease(src);
  78. if (scaled == nil) {
  79. scaled = [UIImage imageWithData:imageData];
  80. }
  81. return scaled;
  82. }
  83. + (NSData*)scaleImageDataToData:(nonnull NSData *)imageData toMaxSize:(CGFloat)maxSize useJPEG:(BOOL)useJPEG {
  84. CGImageSourceRef src = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, (__bridge CFDictionaryRef) @{
  85. (id) kCGImageSourceShouldCache : @NO});
  86. NSMutableDictionary *imageRefOptions = [MediaConverter imageRefOptionsForSize:maxSize];
  87. CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(src, 0, (__bridge CFDictionaryRef) imageRefOptions);
  88. UIImage *scaled = [UIImage imageWithCGImage:scaledImageRef];
  89. CGImageRelease(scaledImageRef);
  90. CFRelease(src);
  91. if (scaled == nil) {
  92. scaled = [UIImage imageWithData:imageData];
  93. }
  94. NSData *scaledData = useJPEG ? UIImageJPEGRepresentation(scaled, kJPEGCompressionQuality) : UIImagePNGRepresentation(scaled);
  95. return scaledData;
  96. }
  97. + (UIImage*)scaleImageUrl:(NSURL *)imageUrl toMaxSize:(CGFloat)maxSize {
  98. CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef)imageUrl, (__bridge CFDictionaryRef) @{
  99. (id) kCGImageSourceShouldCache : @NO});
  100. NSMutableDictionary *imageRefOptions = [MediaConverter imageRefOptionsForSize:maxSize];
  101. CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(src, 0, (__bridge CFDictionaryRef) imageRefOptions);
  102. UIImage *scaled = [UIImage imageWithCGImage:scaledImageRef];
  103. CGImageRelease(scaledImageRef);
  104. CFRelease(src);
  105. return scaled;
  106. }
  107. + (NSMutableDictionary *)imageRefOptionsForSize:(CGFloat)maxSize {
  108. NSMutableDictionary *imageRefOptions = [[NSMutableDictionary alloc] initWithDictionary:@{
  109. (id) kCGImageSourceCreateThumbnailWithTransform : @YES,
  110. (id) kCGImageSourceCreateThumbnailFromImageAlways : @YES,
  111. }];
  112. // If maxSize is 0, we don't need to scale this image
  113. if (maxSize > 0) {
  114. [imageRefOptions setValue:@(maxSize) forKey:(id)kCGImageSourceThumbnailMaxPixelSize];
  115. }
  116. return imageRefOptions;
  117. }
  118. + (NSUInteger)thumbnailSizeForCurrentDevice {
  119. /* maximum thumbnail size: 50% of the screen height of the current device multiplied by the scale,
  120. and rounded to the nearest multiple of 32 */
  121. int screenHeightPixels = (int)(MAX([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) * [UIScreen mainScreen].scale);
  122. int thumbnailSize = screenHeightPixels / 2;
  123. thumbnailSize -= (thumbnailSize % 32);
  124. return thumbnailSize;
  125. }
  126. + (NSArray*)videoQualities {
  127. return @[@"low", @"high"];
  128. }
  129. + (NSArray*)videoQualityMaxDurations {
  130. int highMaxDuration = (int) [VideoConversionHelper getMaxdurationForVideoBitrate:kVideoBitrateHigh audioBitrate:kAudioBitrateHigh];
  131. int lowMaxDuration = (int) [VideoConversionHelper getMaxdurationForVideoBitrate:kVideoBitrateLow audioBitrate:kAudioBitrateLow];
  132. return @[[NSNumber numberWithInt:lowMaxDuration], [NSNumber numberWithInt:highMaxDuration]];
  133. }
  134. + (NSTimeInterval)videoMaxDurationAtCurrentQuality {
  135. long long lowMaxDuration = [VideoConversionHelper getMaxdurationForVideoBitrate:kVideoBitrateLow audioBitrate:kAudioBitrateLow];
  136. return lowMaxDuration;
  137. }
  138. + (BOOL)isVideoDurationValidAtUrl:(NSURL *)url {
  139. if (url == nil) {
  140. return false;
  141. }
  142. return [VideoConversionHelper videoHasAllowedSizeAt:url];
  143. }
  144. + (SDAVAssetExportSession*)convertVideoAsset:(AVAsset*)asset onCompletion:(void(^)(NSURL *url))onCompletion onError:(void(^)(NSError *error))onError {
  145. /* convert video to MPEG4 for compatibility with Android */
  146. NSURL *outputURL = [MediaConverter getAssetOutputURL];
  147. SDAVAssetExportSession *exportSession = [MediaConverter getAVAssetExportSessionFrom:asset outputURL:outputURL];
  148. [MediaConverter convertVideoAsset:asset withExportSession:exportSession onCompletion:onCompletion onError:onError];
  149. return exportSession;
  150. }
  151. + (void)convertVideoAsset:(AVAsset*)asset withExportSession:(SDAVAssetExportSession *)exportSession onCompletion:(void(^)(NSURL *url))onCompletion onError:(void(^)(NSError *error))onError {
  152. /* convert video to MPEG4 for compatibility with Android */
  153. [exportSession exportAsynchronouslyWithCompletionHandler:^(void) {
  154. DDLogVerbose(@"Export Complete %ld %@ %@", (long)exportSession.status, exportSession.error, exportSession.outputURL);
  155. if (exportSession.status == AVAssetExportSessionStatusCompleted) {
  156. onCompletion(exportSession.outputURL);
  157. } else {
  158. [[NSFileManager defaultManager] removeItemAtURL:exportSession.outputURL error:nil];
  159. onError(exportSession.error);
  160. }
  161. }];
  162. }
  163. + (NSURL *)getAssetOutputURL {
  164. NSString *filename = [NSString stringWithFormat:@"%f-%u.%@", [[NSDate date] timeIntervalSinceReferenceDate], arc4random(), MEDIA_EXTENSION_VIDEO];
  165. NSString *tmpfile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
  166. NSURL *outputURL = [NSURL fileURLWithPath:tmpfile];
  167. return outputURL;
  168. }
  169. + (SDAVAssetExportSession *)getAVAssetExportSessionFrom:(AVAsset *) asset outputURL:(NSURL *)outputURL {
  170. if (asset == nil) {
  171. return nil;
  172. }
  173. if (outputURL == nil) {
  174. return nil;
  175. }
  176. return [VideoConversionHelper getAVAssetExportSessionFrom:asset outputURL:outputURL];
  177. }
  178. @end