AVFoundation-03视频播放

概述

AVFoundation 是一个可以用来使用和创建基于时间的视听媒体数据的框架。AVFoundation 的构建考虑到了目前的硬件环境和应用程序,其设计过程高度依赖多线程机制。充分利用了多核硬件的优势并大量使用block和GCD机制,将复杂的计算机进程放到了后台线程运行。会自动提供硬件加速操作,确保在大部分设备上应用程序能以最佳性能运行。该框架就是针对64位处理器设计的,可以发挥64位处理器的所有优势。

iOS 媒体环境.png

AVPlayer

AVFoundation 的播放都围绕着 AVPlayer 类展开,AVPlayer 是一个用来播放基于时间的视听媒体的控制器对象。支持播放从本地、分布下载、或通过HLS协议得到的流媒体。AVPlayer 是一个不可见的组件。我们需要将视频界面呈现给用户的时候,我们需要使用 AVPlayerLayer 类。AVPlayer 只管理一个单独资源的播放,不过我们也可以使用 AVQueuePlayer 来播放、管理一个队列。

AVFoundation 播放类图.png

AVPlayerLayer

AVPlayerLayer 构建于Core Animation之上,由于它是基于 OpenGL 的,所以具有很好的性能,能非常好的满足 AVFoundation 的各种需要。AVPlayerLayer 扩展了 CALayer 类,通过框架在屏幕上显示内容。创建AVPlayerLayer 的时候,我们需要一个 AVPlayer 实例,这就将图层和播放器紧密地绑在了一起。AVPlayerLayer 使用起来简单,我们需要关注的是 videoGravity,即画面的填充模式。默认值为 AVLayerVideoGravityResizeAspect。我们可以使用一下几种模式。

AVF_EXPORT NSString *const AVLayerVideoGravityResizeAspect NS_AVAILABLE(10_7, 4_0);

AVF_EXPORT NSString *const AVLayerVideoGravityResizeAspectFill NS_AVAILABLE(10_7, 4_0);

AVF_EXPORT NSString *const AVLayerVideoGravityResize NS_AVAILABLE(10_7, 4_0);

AVPlayerItem

在之前的 AVFoundation-02资源 中介绍了可以用 AVAsset 获取媒体文件的元数据、时长、创建日期等。不过无法获取当前时间,也没有查找特定位置的方法。这是因为 AVAsset 模型只包含媒体资源的静态信息,这就意味着仅用 AVAsset 是无法实现播放功能的当我们需要播放媒体的时候,我们需要通过 AVPlayerItem 和 AVPlayerItemTrack 类来构建相应的动态内容。AVPlayerItem 会建立媒体资源动态视角的数据模型并保存 AVPlayer 在播放资源时的呈现状态。 以下是 AVPlayerItem 相关方法:

@property (nonatomic, readonly) CGSize presentationSize;

- (CMTime)currentTime;

- (void)seekToTime:(CMTime)time;

AVplayerItem 具有一个名为 status 的AVPlayerItemStatus类型的属性。在对象创建之初,播放条目由AVPlayerItemStatusUnknown状态开始,该状态表示当前媒体还未载入并且还不在播放队列中,但是在具体内容可播放前,需要等待对象的状态由AVPlayerItemStatusUnknown变为AVPlayerItemStatusReadyToPlay。我们可以通过KVO机制监视status属性的值来跟踪这一变化过程。

时间处理

AVPlayer 和 AVplayerItem 都是基于时间的对象。我们更倾向于用日子、小时、分钟和秒的方式表示时间。在表示时间的时候,我们一般选用双精度浮点型数据。不过使用浮点型数据存在一些问题,因为浮点型数据的运算会导致不精确的情况。当进行多时间计算累加的时候这些不精确的情况就会特别严重,经常导致时间的明显偏移,使得媒体的多个数据流几乎无法实现同步。此外,浮点型数据呈现时间无法做到自我描述,这就使得用不同的时间轴进行进行比较和运算比较困难。AVFoundation 使用 CMTime 的数据结构来描述时间。

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

这个结构最关键的两个值是value和timescale,在时间呈现样式中分别作为分子和分母。我们可以通过 CMTime CMTimeMake(int64_t value, int32_t timescale) 来快速创建 CMTime。

播放器

  • 新建QMPlayerView类,作为视频播放的预览试图。在 + (Class)layerClass;方法中返回AVPlayerLayer的Class。
//
//  QMPlayerView.h
//  AVFoundation
//
//  Created by qinmin on 2017/7/3.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface QMPlayerView : UIView
- (instancetype)init;
- (void)setPlayer:(AVPlayer *)player;
@end
//
//  QMPlayerView.m
//  AVFoundation
//
//  Created by qinmin on 2017/7/3.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

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

@implementation QMPlayerView

+ (Class)layerClass
{
    return [AVPlayerLayer class];
}

- (instancetype)init
{
    if (self = [super initWithFrame:[UIScreen mainScreen].bounds]) {
        [self setBackgroundColor:[UIColor blackColor]];
    }

    return self;
}

- (void)setPlayer:(AVPlayer *)player
{
    if (!player) {
        return;
    }
    
    [(AVPlayerLayer *)[self layer] setPlayer:player];
}
@end
  • QMPlayer类,在我这里使用资源异步加载的方式 loadValuesAsynchronouslyForKeys:completionHandler:,同时我们监听AVPlayerItem的status属性,当状态为AVPlayerItemStatusReadyToPlay时,开始播放视频。
//
//  QMPlayer.h
//  AVFoundation
//
//  Created by qinmin on 2017/7/3.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface QMPlayer : NSObject
@property (nonatomic, strong) UIView *previewView;
@property (nonatomic, assign) float playbackVolume;

- (instancetype)initWithURL:(NSURL *)assetURL;

- (void)setVideoFillMode:(NSString *)fillMode;

- (void)play;
- (void)pause;
- (void)stop;

// 字幕
- (NSArray<AVMediaSelectionGroup *> *)mediaSelectionGroups;
- (void)selectMediaOption:(AVMediaSelectionOption *)mediaSelectionOption
    inMediaSelectionGroup:(AVMediaSelectionGroup *)mediaSelectionGroup;

- (NSTimeInterval)currentPlaybackTime;
- (void)setCurrentPlaybackTime:(NSTimeInterval)aCurrentPlaybackTime;

- (UIImage *)thumbnailImageAtCurrentTime;

@end
//
//  QMPlayer.m
//  AVFoundation
//
//  Created by qinmin on 2017/7/3.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMPlayer.h"
#import "QMPlayerView.h"

static NSString *QMPlayerItemContext;

@interface QMPlayer ()
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerItem *playerItem;
@property (nonatomic, strong) AVAsset *asset;
@end

@implementation QMPlayer

- (instancetype)initWithURL:(NSURL *)assetURL
{
    if (assetURL && (self = [super init])) {
        _asset = [AVAsset assetWithURL:assetURL];
        _previewView = [[QMPlayerView alloc] init];
        
        [self prepare];
    }
    
    return self;
}

- (void)prepare
{
    NSArray *requestedKeys = @[@"playable"];
    
    [_asset loadValuesAsynchronouslyForKeys:requestedKeys
                         completionHandler:^{
                             dispatch_async( dispatch_get_main_queue(), ^{
                                 [self didPrepareToPlayAsset:_asset withKeys:requestedKeys];
                             });
                         }];

    
}

- (void)didPrepareToPlayAsset:(AVAsset *)asset withKeys:(NSArray *)requestedKeys
{
    NSArray *keys = @[@"tracks", @"duration", @"commonMetadata"];
    _playerItem = [AVPlayerItem playerItemWithAsset:_asset automaticallyLoadedAssetKeys:keys];
    
    [_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:&QMPlayerItemContext];
    
    _player = [AVPlayer playerWithPlayerItem:_playerItem];
    
    [(QMPlayerView *)_previewView setPlayer:_player];
    
}

#pragma mark - PublicMethod
- (void)play
{
    [_player play];
    [UIApplication sharedApplication].idleTimerDisabled = YES;
}

- (void)pause
{
    [_player pause];
}

- (void)stop
{
    [_player pause];
    [UIApplication sharedApplication].idleTimerDisabled = NO;
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)setVideoFillMode:(NSString *)fillMode
{
    AVPlayerLayer *playerLayer = (AVPlayerLayer *)[_previewView layer];
    playerLayer.videoGravity = fillMode;
}

- (NSTimeInterval)currentPlaybackTime
{
    if (!_player)
        return 0.0f;
    
    return CMTimeGetSeconds([_player currentTime]);
}

- (void)setCurrentPlaybackTime:(NSTimeInterval)aCurrentPlaybackTime
{
    if (!_player)
        return;
    
    [_player seekToTime:CMTimeMakeWithSeconds(aCurrentPlaybackTime, NSEC_PER_SEC)
      completionHandler:^(BOOL finished) {
          if (!finished)
              return;
          
          dispatch_async(dispatch_get_main_queue(), ^{
              [_player play];
          });
      }];
}

- (void)setPlaybackVolume:(float)playbackVolume
{
    _playbackVolume = playbackVolume;
    if (_player != nil && _player.volume != playbackVolume) {
        _player.volume = playbackVolume;
    }
}

- (NSArray<AVMediaSelectionGroup *> *)mediaSelectionGroups
{
    NSMutableArray *mediaSelectionGroups = [NSMutableArray array];
    NSArray *mediaCharacteristics = [self.asset availableMediaCharacteristicsWithMediaSelectionOptions];
    for (NSString *mediaCharacteristic in mediaCharacteristics) {
        [mediaSelectionGroups addObject:[self.asset mediaSelectionGroupForMediaCharacteristic:mediaCharacteristic]];
    }
    return [mediaSelectionGroups copy];
}

- (void)selectMediaOption:(AVMediaSelectionOption *)mediaSelectionOption
    inMediaSelectionGroup:(AVMediaSelectionGroup *)mediaSelectionGroup
{
    [self.playerItem selectMediaOption:mediaSelectionOption inMediaSelectionGroup:mediaSelectionGroup];
}

- (UIImage *)thumbnailImageAtCurrentTime
{
    AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:_asset];
    NSError *error = nil;
    CMTime time = CMTimeMakeWithSeconds(self.currentPlaybackTime, 1);
    CMTime actualTime;
    CGImageRef cgImage = [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    return image;
}

#pragma mark - Observe
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context
{
    if (context == &QMPlayerItemContext) {
        AVPlayerItemStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
        
        switch (status) {
            case AVPlayerItemStatusUnknown:
                 NSLog(@"%@ : error:%@", @"AVPlayerItemStatusUnknown", [_playerItem.error localizedDescription]);
                break;
                
            case AVPlayerItemStatusReadyToPlay:
                NSLog(@"%@", @"AVPlayerItemStatusReadyToPlay");
                [_player play];
                break;
                
            case AVPlayerItemStatusFailed:
                NSLog(@"%@ : error:%@", @"AVPlayerItemStatusFailed", [_playerItem.error localizedDescription]);
                break;
        }
    }
}

@end
  • 使用播放器。
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"1" withExtension:@"mp4"];
_player = [[QMPlayer alloc] initWithURL:fileURL];

_player.previewView.frame = self.view.bounds;
[self.view addSubview:_player.previewView];

参考

AVFoundation开发秘籍:实践掌握iOS & OSX应用的视听处理技术

源码地址:AVFoundation开发 https://github.com/QinminiOS/AVFoundation

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容