《AV Foundation 开发秘籍》读书笔记(四)
第四章 视频播放
1. 主要框架
AVPlayer
AV Foundation 的播放都围绕 AVPlayer 类展开,AVPlayer 是一个用来播放基于时间的视听媒体的控制器对象,支持播放从本地、分布下载或通过 HTTP Live Streaming 协议得到的流媒体。
AVPlayer 是一个不可见组件,如果播放音频文件,没有可视化的用户界面也不会有什么问题,如果播放视频文件,要将视频资源导出到用户界面,需要使用 AVPlayerLayer 类。
AVPlayer 只管理一个单独资源的播放,不过框架还提供了 AVPlayer 的一个子类 AVQueuePlayer,可以用来管理一个资源队列。当你需要在一个序列中播放多个条目或者为音频、视频资源设置播放循环时可使用该子类。
AVPlayerLayer
AVPlayerLayer 构建于 Core Animation 之上,Core Animation 本身具有基于时间的属性,并且由于它基于 OpenGL,所以具有很好的性能,能非常好地满足 AV Foundation 的各种需要。
AVPlayerLayer 使用起来简单,开发者可以自定义的只有 videoGravity 属性,用来确定视频的拉伸或缩放程度。
AVPlayerItem
我们最终的目的是使用 AVPlayer 播放 AVAsset。但是 AVAsset 只包含媒体资源的静态信息,无法实现播放功能,所以,需要通过 AVPlayerItem 和 AVPlayerItemTrack 构建响应的动态内容。
AVPlayerItem 会建立媒体资源动态视角的数据模型,保存 AVPlayer 在播放时呈现的状态。AVPlayerItem 由一个或者多个媒体曲目组成,由 AVPlayerItemTrack 类建立模型。AVPlayerItemTrack 实例用于表示播放器条目中的类型统一的媒体流,比如音频或者视频。AVPlayerItem 中的曲目直接与基础 AVAsset 中的 AVAssetTrack 实例相对应。
2. 视频播放
- (void)viewDidLoad {
[super viewDidLoad];
// AVPlayerItem:提供数据
NSURL *assetURL = [[NSBundle mainBundle] URLForResource:@"一骑当千01" withExtension:@"mp4"];
AVAsset *asset = [AVAsset assetWithURL:assetURL];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
[playerItem addObserver:self forKeyPath:@"status" options:0 context:nil];
// AVPlayer:控制播放
_player = [AVPlayer playerWithPlayerItem:playerItem];
// AVPlayerLayer:显示播放
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
playerLayer.frame = self.view.bounds;
[self.view.layer addSublayer:playerLayer];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItem *playerItem = (AVPlayerItem *)object;
if (playerItem.status == AVPlayerItemStatusReadyToPlay) {
[self.player play];
}
}
}
3. 时间处理
AVAudioPlayer 使用 NSTimeInterval 表示时间,但是 double 类型的运算会导致不精确的情况。此外,double 类型呈现时间信息无法做到自我描述,这就导致在使用不同时间轴进行比较和运算时比较困难。所以 AV Foundation 使用一种可靠性更高的方法来展示时间信息,这就是 CMTime
typedef struct {
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;
CMTime 是一种结构体,最关键的两个值为 value 和 timescale,value 是一个 64 位整数值,timescale 是一个 32 位整数值,分别为分子和分母。
// 0.5 s
CMTime halfSecond = CMTimeMake(1, 2);
// 5 s
CMTime fiveSeconds = CMTimeMake(5, 1);
// 44.1 kHz
CMTime oneSample = CMTimeMake(1, 44100);
// Zero time value
CMTime zeroTime = kCMTimeZero;
4. 显示字幕
显示字幕需要用到两个类:AVMediaSelectionGroup 和 AVMediaSelectionOption,AVMediaSelectionOption 表示 AVAsset 中的备用媒体呈现方式,比如音频、视频、文本轨道。这些轨道可能是指定语言的音频轨道、备用相机角度、指定语言字幕。
NSArray *mediaCharacteristics = self.asset.availableMediaCharacteristicsWithMediaSelectionOptions;
该数组中可能包含的字符串值为:
- AVMediaCharacteristicVisual(视频)
- AVMediaCharacteristicAudible(音频)
- AVMediaCharacteristicLegible(字幕或隐藏式字幕)
获取所有可选择字幕语言数组
- (NSArray *)getSubtitles {
AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
if (group) {
NSMutableArray *subtitles = [NSMutableArray array];
for (AVMediaSelectionOption *option in group.options) {
[subtitles addObject:option.displayName]; // 语言字幕轨道名称,比如 English、Italian、Russian
}
return subtitles;
} else {
return nil;
}
}
设置字幕
- (void)subtitleSelected:(NSString *)subtitle {
AVMediaSelectionGroup *group =
[self.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
for (AVMediaSelectionOption *option in group.options) {
if ([option.displayName isEqualToString:subtitle]) {
[self.playerItem selectMediaOption:option inMediaSelectionGroup:group];
return;
}
}
[self.playerItem selectMediaOption:nil inMediaSelectionGroup:group];
}
5. 缩略图
AVAssetImageGenerator 定义了两个方法实现从视频资源中检索图片:
-
copyCGImageAtTime:actualTime:error:
允许在指定时间点捕捉图片,适合在展示视频缩略图。 -
generateCGImagesAsynchronouslyForTimes:completionHandler:
允许按照第一个参数所指定的时间段生成一个图片序列,该方法具有很高的性能,只需要调用这一个方法就可以生成一组图片
下面方法为第一种 copyCGImageAtTime:actualTime:error:
方法:
- (void)generateThumbnails {
self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:self.asset];
// 默认情况下,捕捉的图片都保持原始维度。设置 maximumSize 的宽度,则会根据视频的宽高比自动设置高度值
self.imageGenerator.maximumSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 0.0f);
CMTime duration = self.asset.duration;
// 将视频时间轴平均分为 20 个 CMTime 值
NSMutableArray *times = [NSMutableArray array];
CMTimeValue increment = duration.value / 20;
CMTimeValue currentValue = 2.0 * duration.timescale;
while (currentValue <= duration.value) {
CMTime time = CMTimeMake(currentValue, duration.timescale);
[times addObject:[NSValue valueWithCMTime:time]];
currentValue += increment;
}
__block NSUInteger imageCount = times.count;
__block NSMutableArray *images = [NSMutableArray array];
AVAssetImageGeneratorCompletionHandler handler;
// requestedTime : 请求的最初时间,它对应于生成图像的调用中指定的 times 数组中的值
// imageRef : 生成的 CGImageRef,如果在给定的时间点没有生成图片则为 NULL
// actualTime : 图片实际生成的时间,可能和请求时间不同。可以在生成图片前通过 AVAssetImageGenerator 实例设置 requestedTimeToleranceBefore 和 requestedTimeToleranceAfter 值来调整 requestedTime 和 actualTime 的接近程度
// result : AVAssetImageGeneratorResult 用来表示生成图片成功、失败、取消
handler = ^(CMTime requestedTime,
CGImageRef imageRef,
CMTime actualTime,
AVAssetImageGeneratorResult result,
NSError *error) {
if (result == AVAssetImageGeneratorSucceeded) {
MYThumbnail *thumbnail =
[MYThumbnail thumbnailWithImage:[UIImage imageWithCGImage:imageRef] time:actualTime];
[images addObject:thumbnail];
} else {
NSAssert(error.localizedDescription, error.localizedDescription);
}
// 如果 imageCount 等于 0 这表明所有图片都处理完成
if (--imageCount == 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.playerView.transport setStillsImages:images];
});
}
};
[self.imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:handler];
}
6. 封装框架
有很多知识点通过笔记是无法表达清楚的,具体实现还需看代码理解的透彻。根据 Learning-AV-Foundation Chapter 4 自行封装了一个视频播放、设置字幕、获取视频缩略图的框架,下载地址:https://github.com/Mayan29/MYAVFoundation