视频播放
一般在iOS开发中,视频播放主要是用MediaPlayer和AVFoundation框架实现。本集合主要讲AV Foundation,所以这里主要说一下AVPlayer如何使用。
播放功能综述
播放功能主要用到下图中几个类,其关系如图所示。
其中AVAsset前面已经讲过,就是资源。AVAssetTrack资源中的流,如音频流、视频流等。这里重点解释一下简单播放视频所需要的AVPlayerItem、AVPlayer、AVPlayerLayer。
AVPlayerItem
AVPlayerItem也是媒体资源的数据模型,与AVAsset不同的是可以保存播放资源时的呈现状态。创建AVPlayerItem需要制定一个媒体资源url或者AVAsset。
初始化方式有:
- (instancetype)initWithURL:(NSURL *)URL;
- (instancetype)initWithAsset:(AVAsset *)asset;
// automaticallyLoadedAssetKeys:由AVAsset定义的一些属性集合,如duration等属性,具体可以查看AVAsset.h
- (instancetype)initWithAsset:(AVAsset *)asset automaticallyLoadedAssetKeys:(nullable NSArray<NSString *> *)automaticallyLoadedAssetKeys
AVPlayer
AV Foundation的核心类,是一个用来播放基于时间的视听媒体控制对象,它并不是一个窗口或视图。AVPlayer只管理一个单独资源的播放,有一个子类AVQueuePlayer可以用来管理一个资源队列,这里暂时不讲。AVPlayer创建需要AVPLayerItem,其初始化方法如下:
//会隐式创建AVPlayerItem
+ (instancetype)playerWithURL:(NSURL *)URL;
- (instancetype)initWithURL:(NSURL *)URL;
//通过AVPlayerItem创建
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item;
- (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item;
通过URL的方式创建会隐式创建AVPlayerItem,并成为AVPlayer的currentItem。
AVPlayerLayer
视频需要一个窗口才能被用户所看到,因此视频框架必然会提供一个渲染图层来展现视频媒体,AVPlayerLayer就是用来渲染视频帧的图层,其继承自CALayer。创建AVPlayerLayer需要一个AVPlayer。
+ (AVPlayerLayer *)playerLayerWithPlayer:(nullable AVPlayer *)player;
AVPlayerLayer是视频展现形式,开发者可以设置videoGravity属性来设置视频的拉伸。videoGravity有三个属性:
typedef NSString * AVLayerVideoGravity NS_STRING_ENUM;
//保持原视频宽高比,并且在图层范围内,默认值,适用于大部分情况
AVF_EXPORT AVLayerVideoGravity const AVLayerVideoGravityResizeAspect
//保持原视频宽高比,充满图层,通常会导致超出屏幕范围的内容被裁剪
AVF_EXPORT AVLayerVideoGravity const AVLayerVideoGravityResizeAspectFill
//拉伸视频内容,使整个视频内容充满图层,但原视频宽高比可能会发生改变,即视频内容拉伸变形,一般不使用
AVF_EXPORT AVLayerVideoGravity const AVLayerVideoGravityResize
总结一下,AVPlayerItem负责媒体资源数据,AVPlayer负责播放功能控制,AVPlayerLayer负责视频展示。
下面为播放视频的一个简单代码段。为了明白过程,代码写的比较多。
NSURL *url = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"mov"];
//创建资源对象
AVAsset *asset = [AVAsset assetWithURL:url];
self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
//创建播放器对象
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
//创建视频展示图层
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[self.view.layer addSublayer:playerLayer];
此时播放视频还需要差一点,当AVPlayerItem有一个status属性,表示媒体当前的状态,有三个状态
//初始化时状态
AVPlayerItemStatusUnknown,
//媒体资源准备完成,已加入播放队列,可以播放
AVPlayerItemStatusReadyToPlay,
//媒体资源准备失败,状态失败时可以获取error属性
AVPlayerItemStatusFailed
通过kvo监测status属性变化,当状态变为AVPlayerItemStatusReadyToPlay表示可以播放。
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:&PlayerItemStatusContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == &context) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.playerItem removeObserver:self forKeyPath:@"status"];
if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
//可以播放视频
}
});
}
}
CMTime
AVPlayer是基于时间的,在AV Fundation框架中,时间和其他框架中的表达有些不一样,如我们常见的NSTimeInterval是double类型。但是在AV Foundation中使用CMTime表示时间。CMTime是一个结构体。
typedef struct
{
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;
在这个结构体中首先了解一下最关键的value和timescale。value是一个64位整数值,作为分子;timescale是一个32位整数值,作为分母。分子和分母的概念有点奇怪,但是当多使用几次之后就会慢慢习惯这种方式,比如计算一个采样频率时间
//1秒
CMTime oneSecond = CMTimeMake(1, 1);
CMTime soneSample = CMTimeMake(1, 44100);
时间监听
定期监听
在播放过程中获取通知,比如在页面上更新播放时间等。利用
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval
queue:(nullable dispatch_queue_t)queue
usingBlock:(void (^)(CMTime time))block
可以实时监听时间变化。
interval:制定周期间隔;
queue:通知发送的队列;
block:指定时间间隔在队列上调用的代码块,提供CMTime值表示播放器当前时间。
边界时间监听
播放过程中,监听特定时间段,如当播放到中间时间时插播其他画面等操作。利用
- (id)addBoundaryTimeObserverForTimes:(NSArray<NSValue *> *)times queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(void))block;
监听特定时间片段。
times:CMTime组成的一个集合;
queue:用来发送消息的调度队列;
block:触发特定时间段时执行的代码块。
条目结束监听
与上述两种基于时间的监听不同,当条目播放完成时,AVPlayerItem会发送一个AVPlayerItemDidPlayToEndTimeNotification通知消息,用于处理播放完成事件,如将播放光标重新定位到开头位置等。通知消息的监听这里就不介绍了。
图片生成
在AV Foundation中有一个AVAssetImageGenerator工具类,可以在AVAsset中提取图片。
有两个方法可以实现该功能:
//在指定时间捕捉图片,如果生成一张图片比较合适,可以用于缩略图展示
- (nullable CGImageRef)copyCGImageAtTime:(CMTime)requestedTime actualTime:(nullable CMTime *)actualTime error:(NSError * _Nullable * _Nullable)outError
//在指定的时间段生成一组图片,性能很好
- (void)generateCGImagesAsynchronouslyForTimes:(NSArray<NSValue *> *)requestedTimes completionHandler:(AVAssetImageGeneratorCompletionHandler)handler;
下面贴一段生成一组图片的示例代码:
- (void)generatorThumbnails {
self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:_asset];
//自动对图片尺寸进行缩放并显著提高性能;宽度固定,根据视频宽高比自动计算高度,缩放的宽高比由 apertureMode属性控制
//该属性不会放大图片,所以即使宽或者高设置很大,最大也只能是视频原有宽高
self.imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f);
//视频总长
CMTime duration = self.asset.duration;
//转换为秒
Float64 durationSeconds = CMTimeGetSeconds(duration);
//保存每一帧的时间
NSMutableArray *times = [NSMutableArray array];
Float64 totalFrame = durationSeconds * 24;//24:fps
for (int i = 1; i <= totalFrame ; i++) {
//每一帧的时间
CMTime timeFrame = CMTimeMake(i, totalFrame);//第i帧 总帧数
NSValue *value = [NSValue valueWithCMTime:timeFrame];
[times addObject:value];
}
//请求生成图片的最初时间和实际生成图片的时间可能有偏差,两个时间可以在block中获取,设置为kCMTimeZero可以尽可能降低偏差
self.imageGenerator.requestedTimeToleranceAfter = kCMTimeZero;
self.imageGenerator.requestedTimeToleranceBefore = kCMTimeZero;
[self.imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
if (result == AVAssetImageGeneratorSucceeded) {
//写入相册
UIImage *uiimage = [UIImage imageWithCGImage:image];
UIImageWriteToSavedPhotosAlbum(uiimage, self, nil, nil);
} else if (result == AVAssetImageGeneratorFailed) {
NSLog(@"%@", error.localizedDescription);
}
}];
}
网友评论