概述
AVFoundation 是一个可以用来使用和创建基于时间的视听媒体数据的框架。AVFoundation 的构建考虑到了目前的硬件环境和应用程序,其设计过程高度依赖多线程机制。充分利用了多核硬件的优势并大量使用block和GCD机制,将复杂的计算机进程放到了后台线程运行。会自动提供硬件加速操作,确保在大部分设备上应用程序能以最佳性能运行。该框架就是针对64位处理器设计的,可以发挥64位处理器的所有优势。
AVPlayer
AVFoundation 的播放都围绕着 AVPlayer 类展开,AVPlayer 是一个用来播放基于时间的视听媒体的控制器对象。支持播放从本地、分布下载、或通过HLS协议得到的流媒体。AVPlayer 是一个不可见的组件。我们需要将视频界面呈现给用户的时候,我们需要使用 AVPlayerLayer 类。AVPlayer 只管理一个单独资源的播放,不过我们也可以使用 AVQueuePlayer 来播放、管理一个队列。
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