美文网首页
四、iOS 视频播放

四、iOS 视频播放

作者: smallLabel | 来源:发表于2018-11-14 17:21 被阅读73次

视频播放

    一般在iOS开发中,视频播放主要是用MediaPlayer和AVFoundation框架实现。本集合主要讲AV Foundation,所以这里主要说一下AVPlayer如何使用。

播放功能综述

    播放功能主要用到下图中几个类,其关系如图所示。

AV Foundation播放类.png

其中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);
        }
    }];
}

相关文章

网友评论

      本文标题:四、iOS 视频播放

      本文链接:https://www.haomeiwen.com/subject/fvwjfqtx.html