1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-03 09:57:46 +01:00
Telegram/Telegraph/TGMediaVideoConverter.m
2016-02-25 01:03:51 +01:00

274 lines
8.8 KiB
Objective-C

#import "TGMediaVideoConverter.h"
#import <CommonCrypto/CommonDigest.h>
#import <sys/stat.h>
#import "MP4Atom.h"
#import "TGImageUtils.h"
#import "TGPhotoEditorUtils.h"
#import "TGVideoEditAdjustments.h"
@interface TGMediaVideoConversionPresetSettings : NSObject
@property (nonatomic, readonly) CGSize maximumResultSize;
@property (nonatomic, readonly) NSDictionary *videoSettings;
@property (nonatomic, readonly) bool hasAudio;
@property (nonatomic, readonly) NSDictionary *audioInputSettings;
@property (nonatomic, readonly) NSDictionary *audioOutputSettings;
+ (TGMediaVideoConversionPresetSettings *)settingsForPreset:(TGMediaVideoConversionPreset)preset;
@end
@implementation TGMediaVideoConverter
#pragma mark - Miscellaneous
+ (SQueue *)converterQueue
{
static dispatch_once_t onceToken;
static SQueue *queue;
dispatch_once(&onceToken, ^
{
queue = [[SQueue alloc] init];
});
return queue;
}
+ (CGRect)_normalizeCropRect:(CGRect)cropRect
{
return CGRectIntegral(cropRect);
}
+ (CGSize)_renderSizeWithCropSize:(CGSize)cropSize
{
CGFloat blockSize = 16.0f;
CGFloat renderWidth = CGFloor(cropSize.width / blockSize) * blockSize;
CGFloat renderHeight = CGFloor(cropSize.height * renderWidth / cropSize.width);
if (fmodf((float)renderHeight, (float)blockSize) != 0)
renderHeight = CGFloor(cropSize.height / blockSize) * blockSize;
return CGSizeMake(renderWidth, renderHeight);
}
+ (MP4Atom *)_findMdat:(MP4Atom *)atom
{
if (atom == nil)
return nil;
if (atom.type == (OSType)'mdat')
return atom;
while (true)
{
MP4Atom *child = [atom nextChild];
if (child == nil)
break;
MP4Atom *result = [self _findMdat:child];
if (result != nil)
return result;
}
return nil;
}
#pragma mark -
+ (CGSize)_outputDimensionsForVideoTrack:(AVAssetTrack *)track presetSettings:(TGMediaVideoConversionPresetSettings *)settings adjustments:(TGMediaVideoEditAdjustments *)adjustments
{
CGSize normalizedDimensions = CGRectApplyAffineTransform((CGRect){ CGPointZero, track.naturalSize }, track.preferredTransform).size;
CGRect cropRect = [adjustments cropAppliedForAvatar:false] ? adjustments.cropRect : (CGRect){ CGPointZero, normalizedDimensions };
CGSize outputDimensions = TGFitSize(cropRect.size, settings.maximumResultSize);
if ([adjustments cropAppliedForAvatar:false])
outputDimensions = [self _renderSizeWithCropSize:outputDimensions];
if (TGOrientationIsSideward(adjustments.cropOrientation, NULL))
outputDimensions = CGSizeMake(outputDimensions.height, outputDimensions.width);
return outputDimensions;
}
#pragma mark - Signals
+ (SSignal *)convertSignalForAVAsset:(AVAsset *)avAsset preset:(TGMediaVideoConversionPreset)preset adjustments:(TGMediaVideoEditAdjustments *)adjustments
{
NSURL *fileUrl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"%x.tmp", (int)arc4random()]]];
NSError *error;
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:fileUrl fileType:AVFileTypeMPEG4 error:&error];
if (assetWriter == nil || error != nil)
return [SSignal fail:error];
AVAssetTrack *videoTrack = [avAsset tracksWithMediaType:AVMediaTypeVideo].firstObject;
CGSize outputVideoDimensions = [self _outputDimensionsForVideoTrack:videoTrack presetSettings:nil adjustments:adjustments];
NSDictionary *videoSettings = nil;
AVAssetWriterInput *videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
videoWriterInput.expectsMediaDataInRealTime = true;
if (![assetWriter canAddInput:videoWriterInput])
return [SSignal fail:nil];
[assetWriter addInput:videoWriterInput];
AVMutableComposition *composition = [[AVMutableComposition alloc] init];
AVMutableVideoComposition *videoComposition = nil;
AVAssetTrack *videoTrackToRead = nil;
AVAssetTrack *audioTrack = [avAsset tracksWithMediaType:AVMediaTypeAudio].firstObject;
// [assetWriter startWriting];
// [assetWriter startSessionAtSourceTime:kCMTimeZero];
// [reader startReading];
//
// return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
// {
// TGMediaVideoConverter *converter = [[TGMediaVideoConverter alloc] initWithAVAsset:avAsset preset:preset adjustments:adjustments];
//
// return [[SBlockDisposable alloc] initWithBlock:^
// {
// [converter cancel];
// }];
// }];
}
+ (SSignal *)hashSignalForAVAsset:(AVAsset *)avAsset adjustments:(TGMediaVideoEditAdjustments *)adjustments
{
if ([adjustments trimApplied] || [adjustments cropAppliedForAvatar:false] || [adjustments rotationApplied])
return [SSignal single:nil];
NSURL *fileUrl = nil;
NSData *timingData = nil;
if ([avAsset isKindOfClass:[AVURLAsset class]])
{
fileUrl = ((AVURLAsset *)avAsset).URL;
}
else
{
AVComposition *composition = (AVComposition *)avAsset;
AVCompositionTrack *videoTrack = nil;
for (AVCompositionTrack *track in composition.tracks)
{
if ([track.mediaType isEqualToString:AVMediaTypeVideo])
{
videoTrack = track;
break;
}
}
if (videoTrack != nil)
{
AVCompositionTrackSegment *firstSegment = videoTrack.segments.firstObject;
NSMutableData *timingData = [[NSMutableData alloc] init];
for (AVCompositionTrackSegment *segment in videoTrack.segments)
{
CMTimeRange targetRange = segment.timeMapping.target;
CMTimeValue startTime = targetRange.start.value / targetRange.start.timescale;
CMTimeValue duration = targetRange.duration.value / targetRange.duration.timescale;
[timingData appendBytes:&startTime length:sizeof(startTime)];
[timingData appendBytes:&duration length:sizeof(duration)];
}
fileUrl = firstSegment.sourceURL;
}
}
return [SSignal defer:^SSignal *
{
NSError *error;
NSData *fileData = [NSData dataWithContentsOfURL:fileUrl options:NSDataReadingMappedIfSafe error:&error];
if (error == nil)
return [SSignal single:[self _hashForVideoWithFileData:fileData timingData:timingData]];
else
return [SSignal fail:error];
}];
}
+ (NSString *)_hashForVideoWithFileData:(NSData *)fileData timingData:(NSData *)timingData
{
const NSUInteger bufSize = 1024;
const NSUInteger numberOfBuffersToRead = 32;
uint8_t buf[bufSize];
NSUInteger size = fileData.length;
CC_MD5_CTX md5;
CC_MD5_Init(&md5);
CC_MD5_Update(&md5, &size, sizeof(size));
const char *SDString = "SD";
CC_MD5_Update(&md5, SDString, (CC_LONG)strlen(SDString));
if (timingData != nil)
CC_MD5_Update(&md5, timingData.bytes, (CC_LONG)timingData.length);
for (NSUInteger i = 0; (i < size) && (i < bufSize * numberOfBuffersToRead); i += bufSize)
{
[fileData getBytes:buf range:NSMakeRange(i, bufSize)];
CC_MD5_Update(&md5, buf, bufSize);
}
for (NSUInteger i = size - MIN(size, bufSize * numberOfBuffersToRead); i < size; i += bufSize)
{
[fileData getBytes:buf range:NSMakeRange(i, bufSize)];
CC_MD5_Update(&md5, buf, bufSize);
}
unsigned char md5Buffer[16];
CC_MD5_Final(md5Buffer, &md5);
NSString *hash = [[NSString alloc] initWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", md5Buffer[0], md5Buffer[1], md5Buffer[2], md5Buffer[3], md5Buffer[4], md5Buffer[5], md5Buffer[6], md5Buffer[7], md5Buffer[8], md5Buffer[9], md5Buffer[10], md5Buffer[11], md5Buffer[12], md5Buffer[13], md5Buffer[14], md5Buffer[15]];
return hash;
}
@end
@implementation TGMediaVideoConversionResult
@end
const CGSize TGMediaVideoConversionAnimationResultSize = { 320.0f, 320.0f };
const CGSize TGMediaVideoConversionCompressedResultSize = { 640.0f, 640.0f };
@implementation TGMediaVideoConversionPresetSettings
+ (TGMediaVideoConversionPresetSettings *)settingsForPreset:(TGMediaVideoConversionPreset)preset
{
switch (preset)
{
case TGMediaVideoConversionPresetPassthrough:
{
}
break;
case TGMediaVideoConversionPresetCompressed:
{
}
break;
case TGMediaVideoConversionPresetAnimation:
{
}
break;
default:
break;
}
}
@end