AVFoundation学习笔记: 媒体的创建与编辑

Posted 人生如梦91

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AVFoundation学习笔记: 媒体的创建与编辑相关的知识,希望对你有一定的参考价值。

组合媒体

媒体组合就是将需要的相关片断从资源中提取出来,再将他们组合成一个临时排列。AVFoundation明确定义了相关功能,可以让开发者简单地将多个音频和视频资源组合成一个新资源。

AVFoundation有关资源组合的功能源于AVAsset的子类AVComposition。AVComposition中的轨道都是AVAssetTrack的子类AVCompositionTrack。一个组合轨迹本身由一个或多个媒体片断组成,由AVCompositionTrackSegment类定义,代表这个组合中的实际媒体区域,描述如下图所示:

AVComposition和AVCompositionTrack都是不可变对象,提供对资源的只读操作。这些对象提供了一个合适的接口让应用程序的一部分可以进行播放或处理。但当需要创建自己的组合时,就需要使用AVMutableComposition和AVMutableCompositionTrack所提供的可变子类。要创建自定义组合,需指定在将要添加到组合的源媒体的时间范围,还要指定要添加片段的每个轨道的位置。

时间处理

CMTime

通常苹果开发者使用NSTimeInterval表示时间,虽然在大多数情况下,它可以满足要求,但是它的不精确性导致它无法用于更高级的媒体开发中。因此CoreMedia框架中定义了CMTime数据类型作为时间格式。定义如下:

typedef struct 
    CMTimeValue value;
    CMTimeScale timescale;
    CMTimeFlags flags;
    CMTimeEpoch epoch;
CMTime;

其中,CMTimeValue和CMTimeScale分别是64位和32位有符号整数,CMTime是分数形式,CMTimeFlags是一个掩码位,用于表示时间的指定定状态,我们可以使用CMTimeMake函数来创建一个时间,并使用CMTimeShow将时间值输出到控制台。另外,还有CMTimeAdd和CMTimeSubtract函数可以简单的完成时间的加减。

CMTimeRange

CoreMedia框架还为时间范围提供了一个数据类型,称为CMTimeRange,它在有关资源编辑的API中有重要地位。CMTimeRangem上两个CMTime组成,第一个表示时间范围的起点,第二个值表示时间范围的持续时间。定义如下:

typedef struct 
    CMTime start;
    CMTime duration;
CMTimeRange;

同样的,我们可以使用CMTimeRangeMake函数来创建一个CMTimeRange,使用CMTimeRangeShow输出到控制台,另外一种创建的方法是CMTimeRangeTimeToTime,提供一个时间范围的起点和终点的CMTime值来创建。

创建组合资源

使用loadValuesAsynchronouslyForKeys:completionHandler:加载资源之后,就可以开始创建组合资源。通用代码如下所示:

AVMutableComposition* composition = [AVMutableComposition composition];

AVMutableCompositionTrack* videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo perferredTrackID:kCMPersistentTrackID_Invalid];

AVMutableComposition* audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio perferredTrackID:kCMPersistentTrackID_Invalid];

上面的示例创建了一个AVMutableComposition并用它的addMutableTrackWithMediaType:perferredTrackID:方法添加了两条轨道,并为其设置两个ID,这个值通用值kCMPersistentTrackID_Invalid常量,这个常量的意思是我们需要创建一个合适轨道的ID的任务委托给框架,标识符会以1..n排列。示意图如下:

创建两个轨道之后, 接下来就是将独立的媒体片段插入到组合的轨道内,代码如下所示:

说明如下:

  1. 定义一个CMTime变量cursorTime来表示我们所指的插入光标点。轨道的这个时间点就是我们插入媒体片段的位置。
  2. 我们取视频片断前5秒钟的内容,所以创建一个CMTimeRange,令其从kCMTimeZero开始,持续时间5秒。
  3. 使用tracksWithMediaType:方法从第一个AVAsset中提取视频轨道,然后使用insertTimeRange:ofTrack:atTime:error:将视频轨道插入到轨道中。
  4. 使用CMTimeAdd函数来移动光标的插入时间,使得下一段内容可以在另一段内容最后插入。
  5. 同样的,提取资源视频轨道并将其插入到组合资源的视频轨道上
  6. 重置cursorTime,同样的插入对应的音频轨道,此时轨道示视图如下:

示例

如下是我随便截取自《Titanic》的两个片断,本来想上传gif的,但是CSDN限制只能低于2M,怎么搞都搞不出来,所以只能上传两张静态图片:

然后使用上述代码,最终将得到两个片断中的部分内容,并且播放的是第一个视频的声音,如下所示:

音频混合

我们在日常生活中经常有这种场景,开始播放声音的时候,音量从小到大,到结束的时候,声音又从大到小。AVFoundation提供了AVAudioMix来实现这些功能。

AVAudioMix所具有的音频处理方法是由它的输入参数集定义的,它的参数是AVAudioMixInputParameters类型的对象。AVAudioMixInputParameters的实例关联组合中的单独音频轨道,并在添加到音频混合时定义基于轨道的处理方法。AVAudioMix和其他相关的AVAudioMixInputParameters集合都是不可变对象,意味着它个适用于火AVPlayerItem和其相关联的AVAssetExportSession之类的客户端提供相关数据,但不能操作它们的状态,当需要创建一个自定义音频混合时,需要改用它们的可变子类AVMutableAudioMix和AVMutableAudioMixInputParameters。

自动调节音量

在应用音频混合的问题上,最核心的处理就是调整组合音频轨道的音量。当一个组合资源播放或导出时,默认行为是以最大音量或正常音量播放音频轨道。当只有一个单音频轨道时这样的方法才能被接收,但当一个组合资源包含多个音频轨道时就会出现问题,因为每个声音都在争夺设备。这就不可避免会导致一些声音可能无法被听到。AVFoundation使用一种更简单的模型,把音量定义为一个标准化的浮点型数值,范围从0.0(静音)〜1.0(最大音量),不过可以使用AVMutableAudioMixInputParameters实例修改这个值。这个对象允许在一个指定时间点或给定的时间范围自动调节音量。AVMutableAudioMixInputParameters提供了两个方法来实现音量调节:

  • setVolume:atTime: 在指定时间点立即调节音量。音量在音频轨道持续时间内会保持不变,直到有另外一个音量调节出现。
  • setVolumeRampFromStartVolume:toEndVolume:timeRange: 允许在一个给定时间范围内平滑地将音量从一个值调节到另外一个值。当需要在一个时间范围内调整音量时,音量会立即变为指定值的初始音量并在持续时间内逐渐调整到指定的结束值。

实例代码如下:

首先创建一个新的与要操作的轨道关联的AVMutableAudioMixInputParameters实例。轨道的默认音量是1.0,然后设置开始时的音量为0.5, 在轨道中选定2秒,然后使用setVolumeRampFromStartVolume:toEndVolume:timeRange:方法应用一个从0.5~0.8的音量渐变。最后在7秒的时候将音量下调到0.3。之后创建AVMutableAudioMix,并将参数添加到对象中,然后将AVMutableAudioMix设置到AVPlayerItem或AVAssetExportSession的audioMix属性进行播放或导出。如下图所示:

视频过渡

大部分视频编辑应用程序的一个重要功能就是创建动态视频过渡效果。AVFoundation对这一功能的支持提供了很高的可靠性。

AVVideoComposition

视频过渡类API中最核心的类是AVVideoComposition。这个类对两个或多个视频轨道组合在一起的方法给出了一个总体描述。它由一组时间范围和描述组合行为的介绍内容组成。这些信息出现在组合资源内的任意时间点。除了包含描述输入视频层组合的信息之外,还提供了配置视频组合的渲染尺寸、缩放和帧时长等属性。

AVVideoCompositionInstruction

AVVideoComposition是由一组AVVideoCompositionInstruction对象格式定义的指令组成。这个对象所提供的最关键的一段数据是组合对象时间轴内的时间范围信息。这一时间范围是在某一组合形式出现时的时间范围。要执行的组全特质是通过其layerInstructions集合定义的。

AVVideoCompositionLayerInstruction

AVVideoCompositionLayerInstruction用于定义对给定视频轨道应用的模糊、变形和裁剪效果。它提供了一些方法用于在特定的时间点或在一个时间范围内对这些值进行修改。在一段时间内对这些值应用渐变操作可以让开发者创建出动态的过渡效果,如溶解或渐淡。

与所有AVFoundation媒体编辑类一样,视频组合API具有不可变或可变两种形式。不可变对象适用于客户端对象,如AVPlayerItem或AVAssetExportSession,不过当我们创建自己的视频组合应用程序时,应该使用可变子类。

部署视频

要在剪辑间添加过渡,首先需要将两个轨道间的视频段重新部署。大多数情况下,两个轨道就可以满足需求,不过为了满足一些特殊需求而加入更多轨道也是可以的。首先,我们创建两个视频轨道,如下所示:

AVMutableComposition* composition = [AVMutableComposition composition];

AVMutableCompositionTrack* trackA = [composition addMutableTrackWithMediaType:AVMediaTypeVideo perferredTrackID:kCMPersistentTrackID_Invalid];

AVMutableCompositionTrack* trackB = [composition addMutableTrackWithMediaType:AVMediaTypeVideo perferredTrackID:kCMPersistentTrackID_Invalid];

NSArray* videoTracks = @[trackA, trackB];

要添加过渡效果,需要以交错方式排列视频片断,并且需要根据过渡时间的持续时长来确定片段的重叠情况。如下示意图:

如图,两个交错的片断在左右两侧均有一定时间的交错。如果不这样,就无法实现视频过渡效果。代码如下所示:

计算通过和过渡的时间范围

AVVideoComposition由一组AVVideoCompositionInstruction对象组成。其中最重要的数据是时间范围,它用来表示某种出现的组合方式持续的时长。在开始创建AVVideoCompositionInstruction实例前,首先需要为组合对象计算一系列时间范围。

需要计算两个类型的时间范围,第一个通常被认为是通过(pass-through)时间范围,在这个时间范围内希望一个轨道的所有帧序列都不在与其轨道进行混合的情况下通过某一区域。第二个时间范围类型为过渡(transition)时间范围。它定义了在组合中视频片段重叠的区域,并在时间轴上标记出应用过渡效果的区域。如下图所示:

创建所需的过渡时间范围被认为是整个处理过程中最重要的一步操作。虽然从表面上看,它并不是最难解决的问题,但确非常容易出错。在进行这一步处理时一定要记住两点:

  • 计算时间范围必须没有任何空隙或重叠,必须是紧接上一个片段之后按需排列的最新时间范围。
  • 计算必须考虑组合对象持续时间。如果组合中还包含额外的轨道,就需要使它们遵循目前的视频时间轴,或者根据它们的持续时长扩展最终的时间范围。

如果没有注意到以上两点,组合对象仍可播放,不过视频内容不会被渲染,只会显示一个黑屏。苹果提供了一个工具类,可以帮助开发者诊断这些问题。在Apple Developer Center中能找到一个AVCompositionDebugViewer的项目,可以帮助我们呈现组合。

创建组合和层指令

接下来是创建AVVideoCompositionInstruction和AVVideoCompositionLayerInstruction实例,代码如下:

  1. 首先遍历之前计算的所有通过时间的范围。循环在两个需要创建指令的视频轨道间前后切换。
  2. 创建一个新的AVMutableVideoCompositionInstruction实例并设置当前通过CMTimeRange人微言轻它的timeRange属性
  3. 之后为活动组合创建一个新的AVMutableVideoCompositionLayerInstruction,将它添加到数组中并设置它人微言轻组合指令的layerInstructions属性。组合的通过时间范围区域只需要一个与要呈现视频帧的轨道相关的单独指令层指令。
  4. 要创建过渡时间范围指令,需要得到前一个轨道和后一个轨道的引用,按这种方式查找轨道可以确保轨道的引用始终顺序正确。
  5. 创建一个新的AVMutableVideoCompositionInstruction实例,设置当前过渡时间范围为它的timeRange属性
  6. 为每个轨道创建一个AVMutableVideoCompositionLayerInstruction实例。在这些层指令上定义一个从场景到另一个场景的过渡效果。
  7. 将两个层指令添加到NSArray中,并设置它们作为当前组合指令的layerInstructionsn属性。对这一数组中的元素进行排序十分重要,因为它定义了组合输出中视频图层的Z轴顺序。

创建和配置AVVideoComposition

AVMutableVideoComposition* videoComposition = [AVMutableComposition videoComposition];
videoComposition.instructions = compositionInstrutions;
videoComposition.renderSize = CGSizeMake(1280.0f, 720.0f);
videoComposition.frameDuration = CMTimeMake(1, 30);
  • instructions:用于设置以上创建的组合指令。这些指令向组合器描述时间范围和执行组合的种类
  • renderSize:用于定义组合应该被渲染的尺寸,这个值应该对应于组合中的视频原始大小
  • farmeDuration:用于设置有效的帧率。
  • renderScale:定义了视频组合应用的缩放,大部分情况下设置为1.0

应用过渡效果

AVFoundation支持3种过渡类型:溶解、推入和擦除。

溶解

实现溶解效果很容易,只需要对AVMutableVideoCompositionLayerInstruction对象设置一个模糊渐变,在过渡时间内将模糊时间从默认的1.0(完全模糊)调整到0.0(完全透明)。还可以设置交叉溶解,对另一个AVMutableVideoCompositionLayerInstruction设置从0.0(完全透明)到1.0(完全模糊)的渐变即可实现。

[fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:timeRange];

推入

推入就是一个平移的过程,代码如下所示:

CGAffineTransform identityTransform = CGAffineTransformIdentity;

CGFloat videoWidth = videoComposition.renderSize.width;

CGAffineTransform fromDestTransform = CGAffineTransformMakeTranslation(-videoWidth, 0.0);
CGAffineTransform toStartTransform = CGAffineTransformMakeTranslation(videoWidth, 0.0);

[fromLayer setTransformRampFromStartTransform:identityTransform toEndTransform:fromDestTransform timeRange:timeRange];

[toLayer setTransformRampFromStartTransform:toStartTransform toEndTransform:identityTransform timeRange:timeRange];

定义一个对于输入视频层的变换。CGAffineTransform可以修改层的转化、旋转和缩放。对层应用一个渐变的变化可以衍生出许多效果。这里我们将fromLayer移到左侧,使其完全移出视图,并将toLayer从右移动到左。

擦除

CGFloat videoWidth = videoComposition.renderSize.width;
CGFloat videoHeight = videoComposition.renderSize.height;

CGRect startRect = CGRectMake(0.0f, 0.0f, videoWidth, videoHeight);
CGRect endRect = CGRectMake(0.0f, videoHeight, videoWidth, 0.0f);

[fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];

要实现这个效果,需要从视频组合的renderSize中获取宽和高。这些值用于创建擦除动画效果的开始和结束CGRect值。初始矩形为最大的宽度和高度,最终矩形在高度上有所削减,在fromLayer上生成一个向上擦除的效果。

演练

效果图如下,可以看到在三个片断之间的溶解切换。

代码如下所示:





动画层内容

使用CoreAnimation为视频应用程序创建叠加效果的方法同使用它在ios或OS X平台创建实时动画的方法几乎一样,最大的区别在于运行动画的时间模型,当创建实时动画时,CAAnimation实例从系统主机时钟获取执行时间。

主机时间从系统启动开始计算并单向向前推进,将动画执行时间同主机时间相关联在实时动画方面非常适用,但是对于创建视频动画来说就不合适了。视频动画需要基于“影片时间”来操作。开始的时间应该是影片开始的时间。主机时间一直向前推进,不会停止、倒退或快进。因为动画时间需要与视频时间轴绑定,所以需要使用不同的执行时间模式。

AVSynchronizedLayer

AVFoundation提供了一个专门与CALayer子类AVSynchronizedLayer,用于与给定AVPlayerItem实例同步时间。这个图层本身并不展示任何内容。仅用来于图层子树协同时间。这样所有在继承关系中附属于该图层的动画都可以从激活的AVPlayerItem实例中获取相应的执行时间。

通常使用AVSynchronizedLayer时会将其整合到播放器视图的图层继承关系中。同步图层直接呈现在视频图层之上。这样就可以添加动画标题、水印或下沿字幕到播放的视频中,并与播放器的播放栏行为保持完美同步。

AVVideoCompositionCoreAnimationTool

要将CoreAnimation图层和动画整合到导出视频中,就需要使用AVVideoCompositionCoreAnimationTool类。在创建叠加效果时应该注意以下几个问题:

  • CoreAnimation框架的默认行为是执行动画并在动画行为完成后进行处理。通常这些行为就是我们希望在实时案例中使用的,因为时间一旦过去就没法返回了。不过对于视频动画就会有问题,所以需要将动画的removedOnCompletion属性设置为NO。如果没有设置,则动画效果是一次性的,那么重新播放视频的时候,就无法再次看到动画。
  • 动画的beginTime属性被设置为0.0的话是不会看到动画效果的。CoreAnimation将值为0.0的beginTime对象转换为CACurrentMediaTime(),这是当前主机时间。如果希望在影片开头加入动画,将动画的beginTime属性设置为AVCoreAnimationBeginTimeZero常量。

很简单,其中layer参数即为添加了对应元素及动画的CALayer,playerItem即为对应的AVPlayerItem。

导出CoreAnimation

在导出场景中整合CoreAnimation图层,需要使用AVVideoCompositionCoreAnimationTool对象,这个对象的使用比较简单,如下所示:

使用该工具需要注意以下几点:

  • 首先创建两个CALayer实例,组合的视频帧被放在videoLayer中,animationLayer将被渲染并生成导出使用的最终视频帧
  • videoLayer必须是animationLayer的subLayer,同时设置animationLayer的geometryFlipped属性为YES来确保标题被正确渲染。如果没有设置这个值将导致位置颠倒

演练

可以看到,在屏幕的左上角,有一行文本”This is a Test Text Layer”不停的闪烁。

#import <AVFoundation/AVFoundation.h>
#import "ViewController.h"

#define k720pSize CGSizeMake(1280.0f, 720.0f)
@interface ViewController ()

@property(nonatomic, strong) AVPlayerLayer* thePlayerLayer;

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];

    AVAsset* asset = [AVAsset assetWithURL:[[NSBundle mainBundle] URLForResource:@"seg3" withExtension:@"mp4"]];
    [asset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^
        dispatch_async(dispatch_get_main_queue(), ^
            [self playAsset:asset];
        );
        return ;
    ];

    return ;


-(void)playAsset:(AVAsset*)asset 
    AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:asset];
    AVPlayer* player = [AVPlayer playerWithPlayerItem:playerItem];
    _thePlayerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    _thePlayerLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:_thePlayerLayer];

    AVSynchronizedLayer* syncLayer = [AVSynchronizedLayer synchronizedLayerWithPlayerItem:playerItem];
    CALayer* textLayer = [self buildTextLayerWithText:@"This is a Test Text Layer"];
    [syncLayer addSublayer:textLayer];

    [_thePlayerLayer addSublayer:syncLayer];
    [player play];

    return ;


-(void)viewWillLayoutSubviews 
    [super viewWillLayoutSubviews];

    _thePlayerLayer.frame = self.view.bounds;
    return ;


-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation 
    return UIInterfaceOrientationPortrait;


-(BOOL)shouldAutorotate 
    return YES;


-(UIInterfaceOrientationMask)supportedInterfaceOrientations 
    return UIInterfaceOrientationMaskAllButUpsideDown;


-(CALayer*)buildTextLayerWithText:(NSString*)text 
    CATextLayer* textLayer = [[CATextLayer alloc] init];
    textLayer.string = text;
    textLayer.fontSize = 20.0f;
    textLayer.foregroundColor = [UIColor redColor].CGColor;
    textLayer.frame = CGRectMake(0, 0, k720pSize.width, k720pSize.height);

    CABasicAnimation* anim = [CABasicAnimation animation];
    anim.keyPath = @"foregroundColor";
    anim.byValue = (id)[UIColor redColor].CGColor;
    anim.toValue = (id)[UIColor yellowColor].CGColor;
    anim.removedOnCompletion = NO;
    anim.repeatCount = HUGE_VALF;
    anim.duration = 2.0f;
    anim.beginTime = AVCoreAnimationBeginTimeAtZero;
    anim.fillMode = kCAFillModeForwards;
    [textLayer addAnimation:anim forKey:nil];

    return textLayer;


@end

至此,关于《AVFoundation开发秘籍》的全部内容结束。

以上是关于AVFoundation学习笔记: 媒体的创建与编辑的主要内容,如果未能解决你的问题,请参考以下文章

视频编解码·学习笔记3. H.264视频编解码工程JM的下载与编解码

AVFoundation 初解

AVFoundation学习笔记:视频播放相关

AVFoundation自己定义音视频频播放

iOS学习笔记31-音频

iOS多媒体总结