iOS 视频编辑 - AVAssetReader && AVAssetWriter

4,315 阅读7分钟

AVAssetReader && AVAssetWriter

对媒体数据资源进行简单的转码或裁剪,使用 AVAssetExportSession 类便足够了,但是更深层次的修改媒体资源,便需要用到 AVAssetReader 类和 AVAssetWriter 类。

AVAssetReader

AVAssetReader 只能与一个资源 asset 相关联,且不能用来读取实时数据,在开始读取数据之前,需要为 reader 添加 AVAssetReaderOutput 的实例对象。这个实例对象描述的是待读取的数据资源来源类型,通常使用 AVAssetReaderAudioMixOutput 、AVAssetReaderTrackOutput 、AVAssetReaderVideoCompositionOutput 三种子类。

使用该类读取媒体资源,其提供的初始化方法与一个 asset 相关联。

//对于提供的参数 asset ,如果是可被修改的,那么在开始读取操作后,对其进行了修改,之后的读取操作都是无效的
+ (nullable instancetype)assetReaderWithAsset:(AVAsset *)asset error:(NSError * _Nullable * _Nullable)outError;
- (nullable instancetype)initWithAsset:(AVAsset *)asset error:(NSError * _Nullable * _Nullable)outError NS_DESIGNATED_INITIALIZER;

//当前读取操作的状态,可取值有 AVAssetReaderStatusUnknown 、AVAssetReaderStatusReading 、
AVAssetReaderStatusCompletedAVAssetReaderStatusFailedAVAssetReaderStatusCancelled
@property (readonly) AVAssetReaderStatus status;
//当 status 的值为 AVAssetReaderStatusFailed 时,描述错误信息
@property (readonly, nullable) NSError *error;


//限制可读取的资源的时间范围
@property (nonatomic) CMTimeRange timeRange;

//判断能否添加该数据源
- (BOOL)canAddOutput:(AVAssetReaderOutput *)output;
//添加数据源
- (void)addOutput:(AVAssetReaderOutput *)output;

//开始读取
- (BOOL)startReading;
//结束读取
- (void)cancelReading;

AVAssetReaderOutput

AVAssetReaderOutput 是用来描述待读取的数据的抽象类,读取资源时,应创建该类的对象,并添加到相应的 AVAssetReader 实例对象中去。

//获取的媒体数据的类型
@property (nonatomic, readonly) NSString *mediaType;

//是否拷贝缓存中的数据到客户端,默认 YES ,客户端可以随意修改数据,但是为优化性能,通常设为 NO
@property (nonatomic) BOOL alwaysCopiesSampleData NS_AVAILABLE(10_8, 5_0);

//同步获取下一个缓存数据,使用返回的数据结束后,应使用 CFRelease 函数将其释放
//当错误或没有数据可读取时,返回 NULL ,返回空后,应检查相关联的 reader 的状态
- (nullable CMSampleBufferRef)copyNextSampleBuffer CF_RETURNS_RETAINED;

//是否支持重新设置数据的读取时间范围,即能否修改 reader 的 timeRange 属性
@property (nonatomic) BOOL supportsRandomAccess NS_AVAILABLE(10_10, 8_0);
//设置重新读取的时间范围,这个时间范围集合中的每一个时间范围的开始时间必需是增长的且各个时间范围不能重叠
//应在 reader 调用 copyNextSampleBuffer 方法返回 NULL 之后才可调用
- (void)resetForReadingTimeRanges:(NSArray<NSValue *> *)timeRanges NS_AVAILABLE(10_10, 8_0);
//该方法调用后,上面的方法即不可再调用,同时 reader 的状态也不会被阻止变为 AVAssetReaderStatusCompleted 了
- (void)markConfigurationAsFinal NS_AVAILABLE(10_10, 8_0);

AVAssetReaderTrackOutput

AVAssetReaderTrackOutput 是 AVAssetReaderOutput 的子类,它用来描述待读取的数据来自 asset track ,在读取前,还可以对数据的格式进行修改。

//初始化方法,参数中指定了 track 和 媒体的格式
//指定的 track 应在 reader 的 asset 中
+ (instancetype)assetReaderTrackOutputWithTrack:(AVAssetTrack *)track outputSettings:(nullable NSDictionary<NSString *, id> *)outputSettings;
- (instancetype)initWithTrack:(AVAssetTrack *)track outputSettings:(nullable NSDictionary<NSString *, id> *)outputSettings NS_DESIGNATED_INITIALIZER;

//指定音频处理时的算法
@property (nonatomic, copy) NSString *audioTimePitchAlgorithm NS_AVAILABLE(10_9, 7_0);

AVAssetReaderAudioMixOutput

AVAssetReaderAudioMixOutput 是 AVAssetReaderOutput 的子类,它用来描述待读取的数据来自音频组合数据。创建该类实例对象提供的参数 audioTracks 集合中的每一个 asset track 都属于相应的 reader 中的 asset 实例对象,且类型为 AVMediaTypeAudio 。

+ (instancetype)assetReaderAudioMixOutputWithAudioTracks:(NSArray<AVAssetTrack *> *)audioTracks audioSettings:(nullable NSDictionary<NSString *, id> *)audioSettings;
- (instancetype)initWithAudioTracks:(NSArray<AVAssetTrack *> *)audioTracks audioSettings:(nullable NSDictionary<NSString *, id> *)audioSettings NS_DESIGNATED_INITIALIZER

AVAssetReaderVideoCompositionOutput

AVAssetReaderVideoCompositionOutput 是 AVAssetReaderOutput 的子类,该类用来表示要读取的类是组合的视频数据。 同 AVAssetReaderAudioMixOutput 类似,该类也提供了两个创建实例的方法,需要提供的参数的 videoTracks 集合中每一个 track 都是 与 reader 相关联的 asset 中的 track 。

+ (instancetype)assetReaderVideoCompositionOutputWithVideoTracks:(NSArray<AVAssetTrack *> *)videoTracks videoSettings:(nullable NSDictionary<NSString *, id> *)videoSettings;
- (instancetype)initWithVideoTracks:(NSArray<AVAssetTrack *> *)videoTracks videoSettings:(nullable NSDictionary<NSString *, id> *)videoSettings NS_DESIGNATED_INITIALIZER;

AVAssetWriter

AVAssetWriter 类可以将媒体数据从多个源写入指定文件格式的单个文件。我们也不需要将 asset writer 对象与特定 asset 相关联,但必须为要创建的每个输出文件使用单独的 asset writer。由于 asset writer 可以从多个来源写出媒体数据,因此我们必须为每个要被写入到输出文件的轨道创建一个 AVAssetWriterInput 对象。每个 AVAssetWriterInput 对象都期望以 CMSampleBufferRef 对象的形式接收数据,但是如果要将 CVPixelBufferRef 对象附加到 asset writer,可以使用 AVAssetWriterInputPixelBufferAdaptor 类。

// AVAssetWriter 创建函数,需要指定输出文件的 URL 和所需的文件类型
+ (nullable instancetype)assetWriterWithURL:(NSURL *)outputURL fileType:(AVFileType)outputFileType error:(NSError * _Nullable * _Nullable)outError;
- (nullable instancetype)initWithURL:(NSURL *)outputURL fileType:(AVFileType)outputFileType error:(NSError * _Nullable * _Nullable)outError NS_DESIGNATED_INITIALIZER;

AVAssetWriterInput

想要用 asset writer 来写媒体数据,必须至少设置一个 asset writer input。例如,如果数据源已经使用 CMSampleBufferRef 类来表示,那么使用 AVAssetWriterInput 类即可。

- (instancetype)initWithMediaType:(AVMediaType)mediaType outputSettings:(nullable NSDictionary<NSString *, id> *)outputSettings;
使用例子
在本例子中,我们创建 ResourceSession 用于管理我们的资源读取和写入,该类主要有以下两个方法

@interface ResourceSession : NSObject
// 使用指定的 asset 初始化 session
- (instancetype)initWithAsset:(AVAsset *)asset;

// 开始转换
- (void)startWithOutputURL:(NSURL *)url;

@end

我们需要将 asset 资源中 videoTrack 和 audioTrack 的数据给读取出来,并写入到指定文件中;所以这里我们统一使用 SampleBufferChannel 这个类目去处理 SampleBuffer 的读取和写入功能

@interface SampleBufferChannel () {
    // 写入队列
    dispatch_queue_t _serializationQueue;
    // 完成回调
    dispatch_block_t _completionHandler;
}
// 媒体类型
@property (nonatomic, copy) NSString *mediaType;
// 数据读取
@property (nonatomic, strong) AVAssetReaderOutput *assetReaderOutput;
// 数据写入
@property (nonatomic, strong) AVAssetWriterInput *assetWriterInput;

@end

@implementation SampleBufferChannel
// 初始化当前输入输出
- (instancetype)initWithAssetReaderOutput:(AVAssetReaderOutput *)assetReaderOutput
                         assetWriterInput:(AVAssetWriterInput *)assetWriterInput {
    if (self = [super init]) {
        _assetReaderOutput = assetReaderOutput;
        _assetWriterInput = assetWriterInput;
        _serializationQueue = dispatch_queue_create("com.SampleBufferChannel.serialQueue", NULL);
    }
    return self;
}
// 开始读取
- (void)startWithDelegate:(id <SampleBufferChannelDelegate>)delegate completionHandler:(dispatch_block_t)completionHandler {
    _completionHandler = [completionHandler copy];
    __weak typeof(self) weakSelf = self;
    // AVAssetWriterInput 请求媒体数据,当 readyForMoreMediaData 的时候可以写入
    [self.assetWriterInput requestMediaDataWhenReadyOnQueue:_serializationQueue usingBlock:^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        BOOL completedOrFailed = NO;
        while ([strongSelf.assetWriterInput isReadyForMoreMediaData] && !completedOrFailed) {
        // 获取下一个 SampleBuffer 写入,当到结束时间点的时候,返回空
            CMSampleBufferRef sampleBuffer = [strongSelf.assetReaderOutput copyNextSampleBuffer];
            if (sampleBuffer) {
                // 写入数据
                BOOL success = [strongSelf.assetWriterInput appendSampleBuffer:sampleBuffer];
                CFRelease(sampleBuffer);
                sampleBuffer = NULL;
                completedOrFailed = !success;
            } else {
                completedOrFailed = YES;
            }
        }
        if (completedOrFailed) {
            [strongSelf callCompletionHandler];
        }
    }];
}

- (void)callCompletionHandler {
    // let the asset writer know that we will not be appending any more samples to this input
    [self.assetWriterInput markAsFinished];
    if (_completionHandler) {
        _completionHandler();
    }
}

@end

回到我们的 ResourceSession,实现我们的初始化函数

- (instancetype)initWithAsset:(AVAsset *)asset {
    self = [super init];
    if (self) {
        _asset = asset;
        _serializationQueue = dispatch_queue_create("com.ResourceSession.serialQueue", NULL);
    }
    return self;
}

开始转换

- (void)startWithOutputURL:(NSURL *)url {
    self.outputURL = url;
    NSArray *keys = @[@"tracks", @"duration"];
    __weak typeof(self) weakSelf = self;
    // 判断当前属性是否已经准备好
    [self.asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_async(strongSelf->_serializationQueue, ^{
            BOOL success = YES;
            NSError *localError = nil;
            // 获取对应属性
            success = ([strongSelf.asset statusOfValueForKey:@"tracks" error:&localError] == AVKeyValueStatusLoaded);
            if (success) {
                success = ([strongSelf.asset statusOfValueForKey:@"duration" error:&localError] == AVKeyValueStatusLoaded);
            }
            if (success) {
                strongSelf.timeRange = CMTimeRangeMake(kCMTimeZero, strongSelf.asset.duration);
                NSFileManager *fm = [NSFileManager defaultManager];
                NSString *path = [url path];
                if ([fm fileExistsAtPath:path]) {
                    success = [fm removeItemAtPath:path error:&localError];
                }
            }
            if (success) {
                // 创建并初始化 Reader、Writer
                success = [strongSelf setupReaderAndWriterReturningError:&localError];
            }
            if (success) {
                // 开始转换
                success = [self startReadingAndWritingReturningError:&localError];
            }
            if (!success) {
                NSLog(@"not success");
                [self.assetReader cancelReading];
                [self.assetWriter cancelWriting];
            }
        });
    }];
}
创建并初始化 Reader、Writer

- (BOOL)setupReaderAndWriterReturningError:(NSError **)outError {
    BOOL success = YES;
    NSError *localError = nil;
    // 创建 AVAssetReader
    self.assetReader = [[AVAssetReader alloc] initWithAsset:self.asset error:&localError];
    success = self.assetReader != nil;
    if (success) {
        // 创建 AVAssetWriter
        self.assetWriter = [[AVAssetWriter alloc] initWithURL:self.outputURL fileType:AVFileTypeMPEG4 error:&localError];
        success = self.assetWriter != nil;
    }
    if (!success) {
        *outError = localError;
        return success;
    }
    AVAssetTrack *audioTrack = nil;
    AVAssetTrack *videoTrack = nil;
    // 获取 Audio Track
    NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio];
    if ([audioTracks count] > 0) {
        audioTrack = [audioTracks objectAtIndex:0];
    }
    // 获取 Video Track
    NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo];
    if ([videoTracks count] > 0) {
        videoTrack = [videoTracks objectAtIndex:0];
    }
    if (audioTrack) {
        // 设置指定读入格式为 PCM
        NSDictionary *decompressionAudioSettings = @{AVFormatIDKey: @(kAudioFormatLinearPCM)};
        // 指定 track、setting 初始化 AVAssetReaderOutput
        AVAssetReaderOutput *output = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:decompressionAudioSettings];
        [self.assetReader addOutput:output];

        AudioChannelLayout stereoChannelLayout = {
            .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo,
            .mChannelBitmap = 0,
            .mNumberChannelDescriptions = 0
        };
        NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)];

        // 指定输出格式
        NSDictionary *compressionAudioSettings = @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),
                                                   AVEncoderBitRateKey: @(128000),
                                                   AVSampleRateKey: @(44100),
                                                   AVChannelLayoutKey: channelLayoutAsData,
                                                   AVNumberOfChannelsKey: @(2)};
        AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:[audioTrack mediaType] outputSettings:compressionAudioSettings];
        // 添加 input
        [self.assetWriter addInput:input];
        self.audioSampleBufferChannel = [[SampleBufferChannel alloc] initWithAssetReaderOutput:output assetWriterInput:input];
    }
    if (videoTrack) {
         // 设置指定读入格式 ARGB
        NSDictionary *decompressionVideoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB),
                                                     (id)kCVPixelBufferIOSurfacePropertiesKey: @{}};
        AVAssetReaderOutput *output = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:videoTrack outputSettings:decompressionVideoSettings];
        [self.assetReader addOutput:output];

        // 获取当前视频 formatDescription
        CMFormatDescriptionRef formatDescription = NULL;
        NSArray *formatDescriptions = [videoTrack formatDescriptions];
        if ([formatDescriptions count] > 0) {
            formatDescription = (__bridge CMFormatDescriptionRef)[formatDescriptions objectAtIndex:0];
        }

        // 获取视频尺寸
        CGSize trackDimensions = CGSizeZero;
        if (formatDescription) {
            trackDimensions = CMVideoFormatDescriptionGetPresentationDimensions(formatDescription, false, false);
        } else {
            trackDimensions = [videoTrack naturalSize];
        }

        // Grab clean aperture, pixel aspect ratio from format description
        NSDictionary *compressionSettings = nil;
        if (formatDescription) {
            NSDictionary *cleanAperture = nil;
            NSDictionary *pixelAspectRatio = nil;
            CFDictionaryRef cleanApertureFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_CleanAperture);
            if (cleanApertureFromCMFormatDescription) {
                cleanAperture = @{AVVideoCleanApertureWidthKey: (NSNumber *)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureWidth),
                                  AVVideoCleanApertureHeightKey:  (NSNumber *)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHeight),
                                  AVVideoCleanApertureHorizontalOffsetKey: (NSNumber *)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHorizontalOffset),
                                  AVVideoCleanApertureVerticalOffsetKey: (NSNumber *)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureVerticalOffset)};
            }
            CFDictionaryRef pixelAspectRatioFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_PixelAspectRatio);
            if (pixelAspectRatioFromCMFormatDescription) {
                pixelAspectRatio = @{AVVideoPixelAspectRatioHorizontalSpacingKey: (NSNumber *)CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioHorizontalSpacing),
                                     AVVideoPixelAspectRatioVerticalSpacingKey: (NSNumber *)CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioVerticalSpacing)};
            }
            if (cleanAperture || pixelAspectRatio)
            {
                NSMutableDictionary *mutableCompressionSettings = [NSMutableDictionary dictionary];
                if (cleanAperture)
                    [mutableCompressionSettings setObject:cleanAperture forKey:AVVideoCleanApertureKey];
                if (pixelAspectRatio)
                    [mutableCompressionSettings setObject:pixelAspectRatio forKey:AVVideoPixelAspectRatioKey];
                compressionSettings = mutableCompressionSettings;
            }
        }
        // 指定输出格式 H.264
        NSMutableDictionary *videoSettings = @{AVVideoCodecKey: AVVideoCodecTypeH264,
                                               AVVideoWidthKey: @(trackDimensions.width),
                                               AVVideoHeightKey: @(trackDimensions.height)}.mutableCopy;
        if (compressionSettings) {
            videoSettings[AVVideoCompressionPropertiesKey] = compressionSettings;
        }
        AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:[videoTrack mediaType] outputSettings:videoSettings];
        [self.assetWriter addInput:input];
        self.videoSampleBufferChannel = [[SampleBufferChannel alloc] initWithAssetReaderOutput:output assetWriterInput:input];
    }
    if (outError) {
        *outError = localError;
    }
    return success;
}

开始转换

- (BOOL)startReadingAndWritingReturningError:(NSError **)outError {
    BOOL success = YES;
    NSError *localError = nil;

    // start
    success = [self.assetReader startReading];
    if (!success) {
        localError = self.assetReader.error;
    }
    if (success) {
        success = [self.assetWriter startWriting];
        if (!success) {
            localError = self.assetWriter.error;
        }
    }
    if (success) {
        dispatch_group_t dispatchGroup = dispatch_group_create();
        [self.assetWriter startSessionAtSourceTime:self.timeRange.start];
        if (self.audioSampleBufferChannel) {
            dispatch_group_enter(dispatchGroup);
            [self.audioSampleBufferChannel startWithDelegate:self completionHandler:^{
                dispatch_group_leave(dispatchGroup);
            }];
        }
        if (self.videoSampleBufferChannel)
        {
            dispatch_group_enter(dispatchGroup);
            [self.videoSampleBufferChannel startWithDelegate:self completionHandler:^{
                dispatch_group_leave(dispatchGroup);
            }];
        }
        dispatch_group_notify(dispatchGroup, _serializationQueue, ^{
            // end
            BOOL finalSuccess = YES;
            NSError *finalError = nil;
            if (self.cancelled) {
                [self.assetReader cancelReading];
                [self.assetWriter cancelWriting];
            } else {
                if (self.assetReader.status == AVAssetReaderStatusFailed) {
                    finalSuccess = NO;
                    finalError = self.assetReader.error;
                }
                if (finalSuccess) {
                    [self.assetWriter finishWritingWithCompletionHandler:^{
                        NSLog(@"finished");
                    }];
                }
            }
        });
    }
    if (outError) {
        *outError = localError;
    }

    return success;
}

更多iOS 视频编辑分享