Функция камер iPhone 6s, которая записывает трехсекундное видео (MOV) вместе со статической фотографией в формате JPEG. Когда снимок сделан, предыдущие 1,5 секунды непрерывно записываемого видео сохраняются вместе с последующими 1,5 секундами.

Как мы понимаем. Apple сохраняет JPEG и MOV в своей папке мультимедиа. Таким образом, файл JPEG должен содержать какую-то информацию о соответствующем файле MOV и наоборот.

Ниже приведены элементы, необходимые для создания живого изображения.

Изображение (JPEG)

Метаданные:

{ “{MakerApple}” : { “17” : “<Identifier>” } }

Видео (MOV)

  • Кодирование H.264
  • Цветовая кодировка YUV420P
  • Метаданные верхнего уровня (метаданные)
{ "com.apple.quicktime.content.identifier" : "<Identifier>" }
  • Дорожка метаданных
{ "MetadataIdentifier" : "mdta/com.apple.quicktime.still-image-time" , "MetadataDataType" : "com.apple.metadata.datatype.int8" }
  • Метаданные в дорожке метаданных
{ "com.apple.quicktime.still-image-time" : 0 }

* картинки и видео <Identifier>должны быть одинаковыми.

Добавление метаданных с помощью кода:

Нам нужно добавить изображения и видео с соответствующими метаданными.

Предположения:

  1. Версия iOS выше 9.1. Ниже iOS 9.1 нет поддержки Live Photo.
  2. Добавьте следующие 3 фреймворка.
1. Photos.framework 
2. CoreMedia.framework
3. MobileCoreServices.framework
Add corresponding headers to your file:
#import <Photos/Photos.h> 
#import <CoreMedia/CMMetadata.h> 
#import <MobileCoreServices/MobileCoreServices.h>

Добавление метаданных изображений:

Добавить метаданные к изображению очень просто

- (void)addMetadataToPhoto:(NSURL *)photoURL outputPhotoFile:(NSString *)outputFile identifier:(NSString *)identifier { 
    NSMutableData *data = [NSData dataWithContentsOfURL:photoURL].mutableCopy; 
    UIImage *image = [UIImage imageWithData:data]; 
    CGImageRef imageRef = image.CGImage; 
    NSDictionary *imageMetadata = @{(NSString *)kCGImagePropertyMakerAppleDictionary : @{@"17" : <identifier>}}; 
    CGImageDestinationRef dest = CGImageDestinationCreateWithData((CFMutableDataRef)data, kUTTypeJPEG, 1, nil); 
    CGImageDestinationAddImage(dest , imageRef, (CFDictionaryRef)imageMetadata); 
    CGImageDestinationFinalize(dest); 
    [data writeToFile:outputFile atomically:YES]; 
}

JPEG; здесь должна быть запись метаданных для kCGImagePropertyMakerAppleDictionary с [17 : assetIdentifier] (17 - это ключ идентификатора активов Apple Maker Note).

Добавление метаданных видео:

Для этого нам нужны AVAssetReader и AVAssetWriter для чтения и записи.

AVAssetReader - видеообъект. Считывать данные не несет ответственности, а относитесь к ним как к менеджеру. AVAssetReaderOutput используется для чтения данных. И AVAssetReaderOutput нужно было добавить в AVAssetReader, чтобы завершить его функциональность. AVAssetReader может иметь несколько AVAssetReaderOutput.

AVAssetReaderTrackOutputявляется AVAssetReaderOutput’s подклассом, который передается [AVAsset tracks] в дорожке для создания считывателя данных дорожки.

[AVAssetReader startReading] Указывает, что AVAssetReaderTrackOutput может начать чтение данных.
[AVAssetReaderOutput copyNextSampleBuffer] указывает на чтение следующего фрагмента данных. Это могут быть аудиоданные, видеоданные или другие данные.
[AVAssetReader cancelReading] Указывает на прекращение чтения данных.

AVAssetWriter - менеджер записи. AVAssetWriterInput - объект записи данных. А AVAssetWriter может содержать несколько AVAssetWriterInput.

[AVAssetWriter startWriting] указывает, что AVAssetWriterInput может начать запись.

[AVAssetWriter startSessionAtSourceTime: kCMTimeZero] указывает, что данные записываются, начиная с нуля секунды.

AVAssetWriterInput.readyForMoreMediaData, логическое значение, указывающее на готовность ввода принимать дополнительные мультимедийные данные.

Если их несколько AVAssetWriterInput, когда один из них AVAssetWriterInput заполняет буфер, данные не будут обрабатываться, но будут ждать, пока другие данные будут AVAssetWriterInput записаны в течение соответствующего периода времени, прежде чем данные будут обработаны.

Чтение и запись судокода:

  1. Инициализируйте AVAssetReader и AVAssetWriter.
  2. Создайте AVAssetReaderTrackOutput и соответствующий AVAssetWriterInput
  3. Используя AVAssetReader, прочтите метаданные верхнего уровня и измените их, используя AVAssetWriter запись в метаданные верхнего уровня.
  4. AVAssetReader и AVAssetWriter входят в состояние чтения и записи.
  5. после AVAssetReaderOuput чтения данных трека используйте AVAssetWriterInput запись / изменение данных трека.
  6. Прочитав все данные, дайте AVAssetReader прекратить чтение. Сделайте все AVAssetWriterInput завершенными.
  7. Пусть AVAssetWriter он перейдет в полное состояние. Создание видео завершено.

Код:

1. Создайте метаданные верхнего уровня

- (AVMetadataItem *) createContentIdentifierMetadataItem: (NSString *) identifier { 
    AVMutableMetadataItem *item = [AVMutableMetadataItem metadataItem]; 
    item.keySpace = AVMetadataKeySpaceQuickTimeMetadata; 
    item.key = AVMetadataQuickTimeMetadataKeyContentIdentifier; 
    item.value = identifier; 
    return item; 
}

identifier должно быть identifier таким же, как значение изображения.

2: Создайте трек метаданных

- (AVAssetWriterInput *) createStillImageTimeAssetWriterInput { 
    NSArray *spec = @[@{(NSString *)kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier : @"mdta/com.apple.quicktime.still-image-time", 
                        (NSString *)kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType: (NSString *)kCMMetadataBaseDataType_SInt8} ]; 
    CMFormatDescriptionRef desc = NULL; 
    CMMetadataFormatDescriptionCreateWithMetadataSpecifications (kCFAllocatorDefault, kCMMetadataFormatType_Boxed, (__bridge CFArrayRef) spec, & desc); 
    AVAssetWriterInput INPUT * = [AVAssetWriterInput assetWriterInputWithMediaType: AVMediaTypeMetadata OutputSettings: nil sourceFormatHint: desc]; 
    return INPUT; 
}

3. Создание метаданных в треках метаданных

- (AVMetadataItem *) createStillImageTimeMetadataItem { 
    AVMutableMetadataItem *item = [AVMutableMetadataItem metadataItem]; 
    item.keySpace = AVMetadataKeySpaceQuickTimeMetadata; 
    item.key = @"com.apple.quicktime.still-image-time"; 
    item.value = @(-1) ; 
    item.dataType = (NSString *) kCMMetadataBaseDataType_SInt8; 
    return Item; 
}

4: Создание AVAssetReader и AVAssetWriter

Сначала определите метод записи, который добавляет метаданные к видео.

- (void)addMetadataToVideo:(NSURL *)videoURL outputFile:(NSString *)outputFile identifier:(NSString *)identifier;

Затем создайте AVAssetReader и AVAssetWriter одновременно добавьте метаданные верхнего уровня com.apple.quicktime.content.identifier.

NSError *error = nil;
  
// Reader
AVAsset *asset = [AVAsset assetWithURL:videoURL];
AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:asset error:&error];
if (error) {
    NSLog(@"Init reader error: %@", error);
    return;
}
  
// Add content identifier metadata item
NSMutableArray<AVMetadataItem *> *metadata = asset.metadata.mutableCopy;
AVMetadataItem *item = [self createContentIdentifierMetadataItem:identifier];
[metadata addObject:item];
  
// Writer
NSURL *videoFileURL = [NSURL fileURLWithPath:outputFile];
[self deleteFile:outputFile];
AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:videoFileURL fileType:AVFileTypeQuickTimeMovie error:&error]; 
if (error) { 
    NSLog(@"Init writer error: %@", error); 
    return; 
} 
[writer setMetadata:metadata];

5: Создайте AVAssetReaderTrackOutput иAVAssetWriterInput

// Tracks
NSArray<AVAssetTrack *> *tracks = [asset tracks];
for (AVAssetTrack *track in tracks) {
    NSDictionary *readerOutputSettings = nil;
    NSDictionary *writerOuputSettings = nil;
    if ([track.mediaType isEqualToString:AVMediaTypeAudio]) {
        readerOutputSettings = @{AVFormatIDKey : @(kAudioFormatLinearPCM)};
        writerOuputSettings = @{AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                                AVSampleRateKey : @(44100),
                                AVNumberOfChannelsKey : @(2),
                                AVEncoderBitRateKey : @(128000)};
    }
    AVAssetReaderTrackOutput *output = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track outputSettings:readerOutputSettings]; 
    AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:track.mediaType outputSettings:writerOuputSettings]; 
    if ([reader canAddOutput:output] && [writer canAddInput:input]) { 
        [reader addOutput :output]; 
        [writer addInput:input]; 
    } 
}

Для аудиодорожек AVAssetReaderTrackOutputdata kAudioFormatLinearPCM кодируются и декодируются по мере чтения. При AVAssetWriterInputзаписи данных kAudioFormatMPEG4AACкодировать и записывать в формате кодирования. Если вы не хотите перекодировать, вы можете nilpass в outputSettingsparameters, чтобы последняя сгенерированная звуковая дорожка имела формат kAudioFormatLinearPCMencoding.
Для видеодорожки входящее nil представление outputSettings аргумента не перекодируется.

Одна из дорожек должна содержаться в активе целевого AVAssetReader. Значение nil для outputSettings настраивает вывод для продажи сэмплов в их исходном формате, хранящемся на указанной дорожке.

Инициализация не удастся, если настройки вывода не могут быть использованы с указанной дорожкой. AVAssetReaderTrackOutput может производить только несжатый вывод. Для настроек вывода звука это означает, что AVFormatIDKey должен быть kAudioFormatLinearPCM. Для настроек вывода видео это означает, что словарь должен соответствовать правилам вывода несжатого видео, как указано в AVVideoSettings.h. AVAssetReaderTrackOutput не поддерживает ключ AVAudioSettings.h, AVSampleRateConverterAudioQualityKey или следующие ключи AVVideoSettings.h:

AVVideoCleanApertureKey
AVVideoPixelAspectRatioKey
AVVideoScalingModeKey При создании настроек вывода видео выбор формата пикселей повлияет на производительность и качество распаковки. Для оптимальной производительности при распаковке видео запрашиваемый формат пикселей должен быть таким, который декодер поддерживает изначально, чтобы избежать ненужных преобразований. Ниже приведены некоторые рекомендации для: JPEG на iOS, используйте kCVPixelFormatType_420YpCbCr8BiPlanarFullRange


Для ДРУГИХ кодеков ON OSX, kCVPixelFormatType_422YpCbCr8 IS в предпочтительном формате пикселя для видео и IS Расшифровка.
Если вам нужно работать в домене RGB, то kCVPixelFormatType_32BGRA рекомендуется для iOS и kCVPixelFormatType_32ARGB рекомендуется для OSX. Мультимедиа CAN с кодировкой ProRes Содержит до 12 бит / канал. Если ваш Source IS ProRes закодирован и вы хотите сохранить больше в течение последних 8 бит / канал во время декомпрессии, используйте один из следующих форматов пикселей:. kCVPixelFormatType_4444AYpCbCr16, kCVPixelFormatType_422YpCbCr16, kCVPixelFormatType_422YpCbCr10 или kCVPixelFormatType_64ARGB AVAssetReader с большой глубиной не поддерживает форматы пикселей с высокой глубиной масштабирования.

ЕСЛИ вы их используете, то не указывайте kCVPixelBufferWidthKey или kCVPixelBufferHeightKey в ваших OutputSettings Dictionary.
ЕСЛИ вы планируете добавить ЭТИ образцы буферов к AVAssetWriterInput, обратите внимание, что только кодеры ProRes поддерживают ЭТИ форматы пикселей. Мультимедиа CAN, закодированная в ProRes 4444, содержит математически альфа-канал без потерь. Чтобы сохранить Декомпрессию Альфа-канал Во время использования с форматом Пиксельный компонент, ТАК КАК Alpha kCVPixelFormatType_4444AYpCbCr16 или kCVPixelFormatType_64ARGB. Чтобы проверить, содержит ли исходный альфа-канал AN Убедитесь, что описание API формата отслеживает kCMFormatDescriptionExtension_Depth и имеет значение IS ITS 32.

6. Создайте трек метаданных

// Metadata track 
AVAssetWriterInput *input = [self createStillImageTimeAssetWriterInput]; 
AVAssetWriterInputMetadataAdaptor *adaptor = [AVAssetWriterInputMetadataAdaptor assetWriterInputMetadataAdaptorWithAssetWriterInput:input]; 
if ([writer canAddInput:input]) { 
    [writer addInput:input]; 
}

При этом AVAssetWriterInputMetadataAdaptor роль метаданных (метаданных) как калибровочного набора метаданных (синхронизированных групп метаданных) записывается как единый AVAssetWriterInput.

7: начните читать и писать

// Start reading and writing 
[writer startWriting]; 
[writer startSessionAtSourceTime:kCMTimeZero]; 
[reader startReading];

8. Запишите метаданные в трек метаданных

// Write metadata track's metadata 
AVMetadataItem *timedItem = [self createStillImageTimeMetadataItem]; 
CMTimeRange timedRange = CMTimeRangeMake(kCMTimeZero, CMTimeMake(1, 100)); 
AVTimedMetadataGroup *timedMetadataGroup = [[AVTimedMetadataGroup alloc] initWithItems:@[timedItem] timeRange:timedRange]; 
[adaptor appendTimedMetadataGroup:timedMetadataGroup];

9: Асинхронный? Чтение и запись данных трека

// Write other tracks 
self.reader = reader; 
self.writer = writer; 
self.queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
self.group = dispatch_group_create(); 
for (NSInteger i = 0; i < reader.outputs.count ; ++i) { 
    dispatch_group_enter(self.group); 
    [self writeTrack:i]; 
}

Здесь сохраните AVAssetReaderи AVAssetWriterobject и?, dispatch_queue_tand dispatch_group_t? используются в основных операциях чтения и записи - (void)writeTrack:(NSInteger)trackIndex;.
Цель использования dispatch_group - выполнить окончательную отделку после того, как все дорожки были асинхронно прочитаны и записаны.
На этом код - (void)addMetadataToVideo:(NSURL *)videoURL outputFile:(NSString *)outputFile identifier:(NSString *)identifier;method полностью завершен.
Ниже приведен код для асинхронного чтения и записи данных дорожки.

- (void)writeTrack:(NSInteger)trackIndex { 
    AVAssetReaderOutput *output = self.reader.outputs[trackIndex]; 
    AVAssetWriterInput *input = self.writer.inputs[trackIndex];     [input requestMediaDataWhenReadyOnQueue:self.queue usingBlock:^{         while ( input.readyForMoreMediaData) {             AVAssetReaderStatus status = self.reader.status;             CMSampleBufferRef buffer = NULL;             if ((status == AVAssetReaderStatusReading) &&                 (buffer = [output copyNextSampleBuffer])) {                 BOOL success = [input appendSampleBuffer:buffer];                 CFRelease( Buffer);                 if (!success) {

                    NSLog(@"Track %d. Failed to append buffer.", (int)trackIndex); 
                    [input markAsFinished]; 
                    dispatch_group_leave(self.group); 
                    return; 
                } 
            } else { 
                if (status == AVAssetReaderStatusReading) { 
                    NSLog(@ "Track %d complete.", (int)trackIndex); 
                } else if (status == AVAssetReaderStatusCompleted) { 
                    NSLog(@"Reader completed."); 
                } else if (status == AVAssetReaderStatusCancelled) { 
                    NSLog(@"Reader cancelled .); 
                } else if (status == AVAssetReaderStatusFailed) {
                    NSLog(@"Reader failed.") 
                } 
                [input markAsFinished]; 
                dispatch_group_leave(self.group); 
                return; 
            } 
        } 
    }]; 
}

[AVAssetWriterInput requestMediaDataWhenReadyOnQueue:usingBlock:]Блок должен продолжать добавлять данные AVAssetWriterInput, пока значение AVAssetWriterInput.readyForMoreMediaDataproperty не станет равным NO? Или пока не останется данных для добавления (обычно вызывается метод [AVAssetWriterInput markAsFinished]). Затем выйдите из блока.
После выхода из блока [AVAssetWriterInput markAsFinished]it не был вызван. После AVAssetWriterInput обработки данных значение свойства AVAssetWriterInput.readyForMoreMediaData будет изменено YES, и блок будет вызван снова для получения дополнительных данных.

10: Окончательная обработка:

- (void)finishWritingTracksWithPhoto:(NSString *)photoFile video:(NSString *)videoFile complete:(void (^)(BOOL success, NSString *photoFile, NSString *videoFile, NSError *error))complete { 
    [self.reader cancelReading] ; 
    [self.writer finishWritingWithCompletionHandler:^{ 
        if (complete) complete(YES, photoFile, videoFile, nil); 
    }]; 
}

Просто прекратите чтение и завершите запись. После того, как видеофайл будет полностью сгенерирован, произойдет обратный вызов. В обратном вызове видео будет храниться в библиотеке фотографий.

11: Завершение:

- (void)useAssetWriter:(NSURL *)photoURL video:(NSURL *)videoURL identifier:(NSString *)identifier complete:(void (^)(BOOL success, NSString *photoFile, NSString *videoFile, NSError *error))complete { 
    // Photo 
    NSString *photoName = [photoURL lastPathComponent]; 
    NSString *photoFile = [self filePathFromDoc:photoName]; 
    [self addMetadataToPhoto:photoURL outputFile:photoFile identifier:identifier];     // Video     NSString *videoName = [videoURL lastPathComponent];     NSString *videoFile = [self filePathFromDoc:videoName];     [self addMetadataToVideo:videoURL outputFile:videoFile identifier:identifier];     if (!self.group) return;     dispatch_group_notify(self.group, dispatch_get_main_queue(), ^{
        [self finishWritingTracksWithPhoto:photoFile video:videoFile complete:complete]; 
    }); 
}

Сохранение в фото-библиотеку

1: проверьте, поддерживает ли устройство динамические фотографии.

BOOL available = [PHAssetCreationRequest supportsAssetResourceTypes:@[@(PHAssetResourceTypePhoto), @(PHAssetResourceTypePairedVideo)]]; 
if (!available) { 
    NSLog(@"No permission to save"); 
    return; 
}

2: Добавить разрешение для списка:

Перед доступом к фото-библиотеке требуется авторизация.
Сначала Info.plstдобавьте в файл пару NSPhotoLibraryUsageDescription ключ-значение.
При запуске программа запрашивает авторизацию.

[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { 
    if (status != PHAuthorizationStatusAuthorized) { 
        NSLog(@"Photo Library access denied."); 
        return; 
    } 
}];

3. Сохранить в библиотеке фотографий

NSURL *photo = [NSURL fileURLWithPath:photoFile]; 
NSURL *video = [NSURL fileURLWithPath:videoFile]; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{     PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAsset];     [request addResourceWithType: PHAssetResourceTypePhoto fileURL: photo options: Nil];     [request addResourceWithType:PHAssetResourceTypePairedVideo fileURL:video options:nil]; } completionHandler:^(BOOL success, NSError * _Nullable error) {     if (success) { NSLog(@"Saved."); }     else { NSLog(@ "Save error: %@", error); } }];

Ссылки: