Extended/TIPXMP4Codec.m (435 lines of code) (raw):
//
// TIPXMP4Codec.m
// TwitterImagePipeline
//
// Created on 3/16/17.
// Copyright © 2020 Twitter. All rights reserved.
//
#import <TwitterImagePipeline/TwitterImagePipeline.h>
#import "TIPXMP4Codec.h"
#import "TIPXUtils.h"
@import AVFoundation;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Constants
NSString * const TIPXImageTypeMP4 = @"public.mp4";
typedef struct _TIPXMP4Signature {
const size_t offset;
const size_t length;
const char *signature;
} TIPXMP4Signature;
/* ftypMSNV.).FMSNVmp42 */
static const char kComplexSignature1[] = { 0x66, 0x74, 0x79, 0x70, 0x4D, 0x53, 0x4E, 0x56, 0x01, 0x29, 0x00, 0x46, 0x4D, 0x53, 0x4E, 0x56, 0x6D, 0x70, 0x34, 0x32 };
static const TIPXMP4Signature kSignatures[] = {
{ .offset = 4, .length = sizeof(kComplexSignature1), .signature = kComplexSignature1 },
{ .offset = 4, .length = 8, .signature = "ftypisom" },
{ .offset = 4, .length = 8, .signature = "ftyp3gp5" },
{ .offset = 4, .length = 8, .signature = "ftypMSNV" },
{ .offset = 4, .length = 8, .signature = "ftypmp42" },
{ .offset = 4, .length = 6, .signature = "ftypqt" },
};
static const size_t kSignatureDataRequiredToCheck = sizeof(kComplexSignature1) + 4;
static const CGFloat kAdjustmentEpsilon = (CGFloat)0.005;
#pragma mark - Declarations
static CGImageRef __nullable TIPX_CGImageCreateFromCMSampleBuffer(CMSampleBufferRef __nullable sample) CF_RETURNS_RETAINED;
static BOOL TIPX_imageNeedsScaling(CGImageRef imageRef, CGSize naturalSize);
static UIImage *TIPX_scaledImage(CGImageRef imageRef, CGSize naturalSize, CIContext *context);
@interface TIPXMP4DecoderConfigInternal : NSObject <TIPXMP4DecoderConfig>
- (instancetype)initWithMaxDecodableFramesCount:(NSUInteger)max;
@end
@interface TIPXMP4DecoderContext : NSObject <TIPImageDecoderContext>
- (instancetype)initWithBuffer:(nonnull NSMutableData *)buffer config:(nullable id<TIPXMP4DecoderConfig>)config;
- (TIPImageDecoderAppendResult)appendData:(nonnull NSData *)data TIPX_OBJC_DIRECT;
- (nullable TIPImageContainer *)renderImageWithRenderMode:(TIPImageDecoderRenderMode)renderMode
targetDimensions:(CGSize)targetDimensions
targetContentMode:(UIViewContentMode)targetContentMode TIPX_OBJC_DIRECT;
- (TIPImageDecoderAppendResult)finalizeDecoding TIPX_OBJC_DIRECT;
@end
@interface TIPXMP4Decoder : NSObject <TIPImageDecoder>
@property (nonatomic, readonly, nullable) id<TIPXMP4DecoderConfig> defaultDecoderConfig;
- (instancetype)initWithDefaultDecoderConfig:(nullable id<TIPXMP4DecoderConfig>)decoderConfig NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@end
#pragma mark - Codec
@implementation TIPXMP4Codec
- (instancetype)init
{
return [self initWithDefaultDecoderConfig:nil];
}
- (instancetype)initWithDefaultDecoderConfig:(nullable id<TIPXMP4DecoderConfig>)decoderConfig
{
if (self = [super init]) {
_tip_decoder = [[TIPXMP4Decoder alloc] initWithDefaultDecoderConfig:decoderConfig];
}
return self;
}
- (nullable id<TIPXMP4DecoderConfig>)defaultDecoderConfig
{
return [(TIPXMP4Decoder *)_tip_decoder defaultDecoderConfig];
}
+ (id<TIPXMP4DecoderConfig>)decoderConfigWithMaxDecodableFramesCount:(NSUInteger)max
{
return [[TIPXMP4DecoderConfigInternal alloc] initWithMaxDecodableFramesCount:max];
}
@end
#pragma mark - Decoder Context
@implementation TIPXMP4DecoderContext
{
id<TIPXMP4DecoderConfig> _config;
NSMutableData *_data;
FILE *_temporaryFile;
NSString *_temporaryFilePath;
AVAsset *_avAsset;
AVAssetTrack *_avTrack;
TIPImageContainer *_cachedContainer;
UIImage *_firstFrame;
NSUInteger _frameCount;
NSUInteger _maxFrameCount;
BOOL _finalized;
}
@synthesize tip_data = _data;
@synthesize tip_dimensions = _dimensions;
@synthesize tip_config = _config;
- (BOOL)tip_isAnimated
{
return YES;
}
- (NSUInteger)tip_frameCount
{
return _frameCount;
}
- (instancetype)initWithBuffer:(NSMutableData *)buffer config:(nullable id<TIPXMP4DecoderConfig>)config
{
if (self = [super init]) {
_data = buffer;
_config = config;
_maxFrameCount = (config) ? [config maxDecodableFramesCount] : 0;
NSFileManager *fm = [NSFileManager defaultManager];
NSString *tmpDir = fm.temporaryDirectory.path;
[fm createDirectoryAtPath:tmpDir withIntermediateDirectories:YES attributes:nil error:NULL];
_temporaryFilePath = [[tmpDir stringByAppendingPathComponent:[NSUUID UUID].UUIDString] stringByAppendingPathExtension:@"mp4"];
_temporaryFile = fopen(_temporaryFilePath.UTF8String, "w");
[self _writeDataToTemporaryFile:_data];
}
return self;
}
- (void)dealloc
{
[self _clear];
}
- (BOOL)_writeDataToTemporaryFile:(NSData *)data TIPX_OBJC_DIRECT
{
if (_temporaryFile) {
const size_t byteCount = data.length;
if (byteCount) {
const size_t byteOut = fwrite(data.bytes, sizeof(char), byteCount, _temporaryFile);
if (byteCount == byteOut) {
return YES;
} else {
fclose(_temporaryFile);
_temporaryFile = NULL;
[[NSFileManager defaultManager] removeItemAtPath:_temporaryFilePath
error:NULL];
}
}
}
return NO;
}
- (TIPImageDecoderAppendResult)appendData:(NSData *)data
{
if (!_finalized) {
if (!_frameCount) {
_frameCount = 1; // seed the frames
}
[_data appendData:data];
[self _writeDataToTemporaryFile:data];
}
return TIPImageDecoderAppendResultDidProgress;
}
- (nullable TIPImageContainer *)_firstFrameImageContainer TIPX_OBJC_DIRECT
{
if (!_firstFrame) {
if (_temporaryFile || _finalized) {
@autoreleasepool {
AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:_temporaryFilePath]];
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
CGImageRef imageRef = [imageGenerator copyCGImageAtTime:CMTimeMake(0, 1)
actualTime:nil
error:nil];
TIPXDeferRelease(imageRef);
if (imageRef) {
AVAssetTrack *track = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
CGSize naturalSize = track.naturalSize;
UIImage *image;
if (track && TIPX_imageNeedsScaling(imageRef, naturalSize)) {
image = TIPX_scaledImage(imageRef, naturalSize, [[CIContext alloc] init]);
}
if (!image) {
image = [UIImage imageWithCGImage:imageRef
scale:[UIScreen mainScreen].scale
orientation:UIImageOrientationUp];
}
if (image) {
_firstFrame = image;
}
}
}
}
}
return _firstFrame ? [[TIPImageContainer alloc] initWithImage:_firstFrame] : nil;
}
- (nullable TIPImageContainer *)renderImageWithRenderMode:(TIPImageDecoderRenderMode)renderMode
targetDimensions:(CGSize)targetDimensions
targetContentMode:(UIViewContentMode)targetContentMode
{
if (_cachedContainer) {
return _cachedContainer;
}
if (renderMode != TIPImageDecoderRenderModeCompleteImage && !_finalized) {
return [self _firstFrameImageContainer];
}
if (!_finalized || !_avTrack || !_avAsset) {
return nil;
}
const NSTimeInterval duration = CMTimeGetSeconds(_avAsset.duration);
if (duration <= 0.0 || _avTrack.nominalFrameRate <= 0.0f) {
// defensive programming: state is not viable for decoding, just treat as a 1 frame image
_cachedContainer = [self _firstFrameImageContainer];
return _cachedContainer;
}
TIPExecuteCGContextBlock(^{
NSError *error = nil;
AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:self->_avAsset error:&error];
if (!reader) {
[[TIPGlobalConfiguration sharedInstance].logger tip_logWithLevel:TIPLogLevelWarning
#if defined(__FILE_NAME__)
file:@(__FILE_NAME__)
#else
file:@(__FILE__)
#endif
function:@(__FUNCTION__)
line:__LINE__
message:error.description];
return;
}
CGSize naturalSize = self->_avTrack.naturalSize;
// TODO: handle targetDimensions & targetContentMode!
NSDictionary *outputSettings = @{
(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)
};
AVAssetReaderTrackOutput *output =
[AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:self->_avTrack
outputSettings:outputSettings];
[reader addOutput:output];
[reader startReading];
const NSUInteger expectedFrameCount = (NSUInteger)(duration / (1. / self->_avTrack.nominalFrameRate));
const NSUInteger maxFrameCount = self->_maxFrameCount ?: NSUIntegerMax;
NSMutableArray<UIImage *> *images = [[NSMutableArray alloc] init];
NSUInteger count = 0;
NSUInteger mod = 1;
while ((expectedFrameCount / mod) > maxFrameCount) {
mod++;
}
CIContext *context;
CMSampleBufferRef sample = NULL;
do {
sample = [output copyNextSampleBuffer];
TIPXDeferRelease(sample);
if (mod > 1 && ((++count % mod) != 1)) {
continue;
}
CGImageRef imageRef = TIPX_CGImageCreateFromCMSampleBuffer(sample);
TIPXDeferRelease(imageRef);
if (imageRef) {
UIImage *image = nil;
if (TIPX_imageNeedsScaling(imageRef, naturalSize)) {
if (!context) {
context = [[CIContext alloc] init];
}
image = TIPX_scaledImage(imageRef, naturalSize, context);
}
if (!image) {
image = [[UIImage alloc] initWithCGImage:imageRef];
}
[images addObject:image];
}
} while (sample != NULL);
self->_frameCount = images.count;
TIPImageContainer *container = nil;
if (self->_frameCount > 1) {
UIImage *animatedImage = [UIImage animatedImageWithImages:images
duration:CMTimeGetSeconds(self->_avAsset.duration)];
container = [[TIPImageContainer alloc] initWithImage:animatedImage];
} else if (self->_frameCount == 1) {
container = [[TIPImageContainer alloc] initWithImage:images.firstObject];
}
self->_cachedContainer = container;
});
[self _clear];
return _cachedContainer;
}
- (TIPImageDecoderAppendResult)finalizeDecoding
{
if (_finalized) {
return TIPImageDecoderAppendResultDidCompleteLoading;
}
@autoreleasepool {
_finalized = YES;
_firstFrame = nil;
if (_temporaryFile) {
fflush(_temporaryFile);
fclose(_temporaryFile);
_temporaryFile = NULL;
} else {
[_data writeToFile:_temporaryFilePath atomically:NO];
}
tipx_defer(^{
if (!self->_avTrack) {
self->_avAsset = nil;
}
if (!self->_avAsset) {
self->_temporaryFilePath = nil;
}
});
_avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:_temporaryFilePath]];
_avTrack = [_avAsset tracksWithMediaType:AVMediaTypeVideo].firstObject;
_frameCount = (NSUInteger)(_avTrack.nominalFrameRate * CMTimeGetSeconds(_avAsset.duration)); // guesstimate
return TIPImageDecoderAppendResultDidCompleteLoading;
}
}
- (void)_clear TIPX_OBJC_DIRECT
{
_avTrack = nil;
_avAsset = nil;
if (_temporaryFile) {
fflush(_temporaryFile);
fclose(_temporaryFile);
_temporaryFile = NULL;
}
if (_temporaryFilePath) {
[[NSFileManager defaultManager] removeItemAtPath:_temporaryFilePath
error:NULL];
_temporaryFilePath = nil;
}
}
@end
#pragma mark - Decoder
@implementation TIPXMP4Decoder
- (instancetype)initWithDefaultDecoderConfig:(nullable id<TIPXMP4DecoderConfig>)decoderConfig
{
if (self = [super init]) {
_defaultDecoderConfig = decoderConfig;
}
return self;
}
- (TIPImageDecoderDetectionResult)tip_detectDecodableData:(NSData *)data
isCompleteData:(BOOL)complete
earlyGuessImageType:(nullable NSString *)imageType
{
if (data.length < kSignatureDataRequiredToCheck) {
return TIPImageDecoderDetectionResultNeedMoreData;
}
for (size_t i = 0; i < (sizeof(kSignatures) / sizeof(kSignatures[0])); i++) {
const TIPXMP4Signature sig = kSignatures[i];
if (0 == memcmp(data.bytes + sig.offset, sig.signature, sig.length)) {
return TIPImageDecoderDetectionResultMatch;
}
}
return TIPImageDecoderDetectionResultNoMatch;
}
- (TIPXMP4DecoderContext *)tip_initiateDecoding:(nullable id)config
expectedDataLength:(NSUInteger)expectedDataLength
buffer:(nullable NSMutableData *)buffer
{
id<TIPXMP4DecoderConfig> decoderConfig = nil;
if ([config respondsToSelector:@selector(maxDecodableFramesCount)]) {
decoderConfig = config;
}
if (!decoderConfig) {
decoderConfig = self.defaultDecoderConfig;
}
return [[TIPXMP4DecoderContext alloc] initWithBuffer:buffer ?: [[NSMutableData alloc] initWithCapacity:expectedDataLength]
config:decoderConfig];
}
- (TIPImageDecoderAppendResult)tip_append:(TIPXMP4DecoderContext *)context
data:(NSData *)data
{
return [context appendData:data];
}
- (nullable TIPImageContainer *)tip_renderImage:(TIPXMP4DecoderContext *)context
renderMode:(TIPImageDecoderRenderMode)renderMode
targetDimensions:(CGSize)targetDimensions
targetContentMode:(UIViewContentMode)targetContentMode
{
return [context renderImageWithRenderMode:renderMode
targetDimensions:targetDimensions
targetContentMode:targetContentMode];
}
- (TIPImageDecoderAppendResult)tip_finalizeDecoding:(TIPXMP4DecoderContext *)context
{
return [context finalizeDecoding];
}
@end
@implementation TIPXMP4DecoderConfigInternal
@synthesize maxDecodableFramesCount = _maxDecodableFramesCount;
- (instancetype)initWithMaxDecodableFramesCount:(NSUInteger)max
{
if (self = [super init]) {
_maxDecodableFramesCount = max;
}
return self;
}
@end
static CGImageRef __nullable TIPX_CGImageCreateFromCMSampleBuffer(CMSampleBufferRef __nullable sampleBuffer)
{
CVImageBufferRef imageBuffer = sampleBuffer ? CMSampleBufferGetImageBuffer(sampleBuffer) : NULL;
if (!imageBuffer) {
return NULL;
}
// Lock the image buffer
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
tipx_defer(^{
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
});
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0); // Get information of the image
const size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
const size_t width = CVPixelBufferGetWidth(imageBuffer);
const size_t height = CVPixelBufferGetHeight(imageBuffer);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
tipx_defer(^{
CGColorSpaceRelease(colorSpace);
});
CGContextRef newContext = CGBitmapContextCreate(baseAddress,
width,
height,
8,
bytesPerRow,
colorSpace,
kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst);
tipx_defer(^{
CGContextRelease(newContext);
});
return CGBitmapContextCreateImage(newContext);
}
static BOOL TIPX_imageNeedsScaling(CGImageRef imageRef, CGSize naturalSize)
{
if (imageRef && CGImageGetWidth(imageRef) > 0 && CGImageGetHeight(imageRef) > 0) {
const CGFloat widthScale = naturalSize.width / CGImageGetWidth(imageRef);
const CGFloat heightScale = naturalSize.height / CGImageGetHeight(imageRef);
if (ABS(widthScale - 1) > kAdjustmentEpsilon || ABS(heightScale - 1) > kAdjustmentEpsilon) {
return YES;
}
}
return NO;
}
static UIImage *TIPX_scaledImage(CGImageRef imageRef, CGSize naturalSize, CIContext *context)
{
CGImageRef finalImageRef = NULL;
@autoreleasepool {
if (imageRef) {
const CGFloat widthScale = naturalSize.width / CGImageGetWidth(imageRef);
const CGFloat heightScale = naturalSize.height / CGImageGetHeight(imageRef);
CIImage *ciimage = [CIImage imageWithCGImage:imageRef];
ciimage = [ciimage imageByApplyingTransform:CGAffineTransformMakeScale(widthScale, heightScale)];
CGImageRef scaledImageRef = [context createCGImage:ciimage fromRect:ciimage.extent];
if (scaledImageRef) {
finalImageRef = scaledImageRef;
}
}
}
if (finalImageRef) {
TIPXDeferRelease(finalImageRef);
return [UIImage imageWithCGImage:finalImageRef];
}
return nil;
}
NS_ASSUME_NONNULL_END