在开发中,单纯使用AVPlayer类是无法显示视频的,要将视频层添加至AVPlayerLayer中,这样才能将视频显示出来。
属性含义:
/* 播放器 */
@property (nonatomic, strong) AVPlayer *player;
// 播放器的Layer
@property (weak, nonatomic) AVPlayerLayer *playerLayer;
//生成layer层
AVPlayerLayer * layer=[AVPlayerLayer playerLayerWithPlayer:_player];
//设置坐标
layer.frame=CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
//把layer层加入到self.View中
[self.view.layer addSublayer:layer];
//kvo 观察播放状态playerItem.status
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
//观察缓存现在的进度,KVO进行观察,观察 playerItem.loadedTimeRanges的属性
使用 AVPlayer 的时候,一定要注意 AVPlayer 、 AVPlayerLayer 和 AVPlayerItem 三者之间的关系。
AVPlayer 负责控制播放, layer 显示播放, item 提供数据,当前播放时间, 已加载情况。
Item 中一些基本的属性, status, duration, loadedTimeRanges, currentTime(当前播放时间)。
AVPlayerItemStatus是代表当前播放资源item 的状态(可以理解成这url链接or视频文件。。。可以播放成功/失败)
AVPlayerStatus是代表当前播放器的状态。
addPeriodicTimeObserverForInterval
给AVPlayer 添加time Observer 有利于我们去检测播放进度
但是添加以后一定要记得移除,其实不移除程序不会崩溃,但是这个线程是不会释放的,会占用你大量的内存资源
CMTime 结构体
连接的教程里面 给的参数是CMTimeMake(1, 1),其实就是1s调用一下block,
打个比方CMTimeMake(a, b)就是a/b秒之后调用一下block
介绍一个网站有关这个结构体的:
https://zwo28.wordpress.com/2015/03/06/%E8%A7%86%E9%A2%91%E5%90%88%E6%88%90%E4%B8%ADcmtime%E7%9A%84%E7%90%86%E8%A7%A3%EF%BC%8C%E4%BB%A5%E5%8F%8A%E5%88%A9%E7%94%A8cmtime%E5%AE%9E%E7%8E%B0%E8%BF%87%E6%B8%A1%E6%95%88%E6%9E%9C/
拖动slider 播放跳跃播放,要使用AVPlayer 对象的seekToTime:方法,
举个最简单的例子:假如一个video视频有20s,想要跳到10s进行播放(_palyer 为AVPlayer 对象)
[_player seekToTime:CMTimeMake(10,1)];
后面的参数写1,前面的参数写将要播放的秒数,我试验得出的结果,不要问我问什么,需要自己理解。
5.播放到结尾怎么回到开头呢?
[_player seekToTime:kCMTimeZero];
-(void)setupUI{
//创建播放器层
AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:self.player];
playerLayer.frame=aview.frame;
playerLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//视频填充模式
[aview.layer addSublayer:playerLayer];
}
-(void)addNotification{
//给AVPlayerItem添加播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:)name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
}
playerItemWithURL
或者 initWithURL:
在使用 AVPlayer 播放视频时,提供视频信息的是 AVPlayerItem,一个 AVPlayerItem 对应着一个URL视频资源。
初始化一个 AVPlayItem 对象后,其属性并不是马上就可以使用。我们必须确保 AVPlayerItem 已经被加载好了,可以播放了,才能使用。 毕竟凡是和网络扯上关系的都需要时间去加载。 那么,什么时候属性才能正常使用呢。 官方文档给出了解决方案:
直到 AVPlayerItem 的 status
属性为 AVPlayerItemStatusReadyToPlay
.
使用 KVO 键值观察者,其属性。
因此我们在使用的时候,使用 URL 初始化 AVPlayerItem 后,还要给它添加观察者。
AVPlayreItem 的属性需要当 status 为 ReadyToPlay 的时候才可以正常使用。
观察status属性
[_playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew) context:nil]; // 观察status属性,
// setAVPlayer self.player = [[AVPlayer alloc] init]; _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; [self.playerView.layer addSublayer:_playerLayer];
在第一步,布局初始化时,AVPlayer 并没有 AVPlayerItem,AVPlayer 提供了 - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;
方法,用于切换视频。
- (void)updatePlayerWithURL:(NSURL *)url { _playerItem = [AVPlayerItem playerItemWithURL:url]; // create item [_player replaceCurrentItemWithPlayerItem:_playerItem]; // replaceCurrentItem [self addObserverAndNotification]; // 注册观察者,通知 }
观察 AVPlayerItem 的 status
属性,当状态变为 AVPlayerStatusReadyToPlay
时才可以使用。
也可以观察 loadedTimeRanges
获取缓冲进度
注册观察者:
[_playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew) context:nil]; // 观察status属性
执行观察者方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { AVPlayerItem *item = (AVPlayerItem *)object; if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status = [[change objectForKey:@"new"] intValue]; // 获取更改后的状态 if (status == AVPlayerStatusReadyToPlay) { CMTime duration = item.duration; // 获取视频长度 // 设置视频时间 [self setMaxDuration:CMTimeGetSeconds(duration)]; // 播放 [self play]; } else if (status == AVPlayerStatusFailed) { NSLog(@"AVPlayerStatusFailed"); } else { NSLog(@"AVPlayerStatusUnknown"); } } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) { NSTimeInterval timeInterval = [self availableDurationRanges]; // 缓冲时间 CGFloat totalDuration = CMTimeGetSeconds(_playerItem.duration); // 总时间 [self.loadedProgress setProgress:timeInterval / totalDuration animated:YES]; // 更新缓冲条 } }
AVPlayer 提供了 play
, pause
, 和 - (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler
方法。
在看 AVPlayer 的 seekToTime 之前,先来认识一个结构体。
CMTime 是专门用于标识电影时间的结构体.
typedef struct{ CMTimeValue value; // 帧数 CMTimeScale timescale; // 帧率(影片每秒有几帧) CMTimeFlags flags; CMTimeEpoch epoch; } CMTime;
AVPlayerItem 的 duration 属性就是一个 CMTime 类型的数据。 如果我们想要获取影片的总秒数那么就可以用 duration.value / duration.timeScale 计算出来。也可以使用 CMTimeGetSeconds 函数
CMTimeGetSeconds(CMtime time)
double seconds = CMTimeGetSeconds(item.duration); // 相当于 duration.value / duration.timeScale
如果一个影片为60frame(帧)每秒, 当前想要跳转到 120帧的位置,也就是两秒的位置,那么就可以创建一个 CMTime 类型数据。
CMTime,通常用如下两个函数来创建.
CMTimeMake(int64_t value, int32_t scale)
CMTime time1 = CMTimeMake(120, 60);
CMTimeMakeWithSeconds(Flout64 seconds, int32_t scale)
CMTime time2 = CMTimeWithSeconds(120, 60);
CMTimeMakeWithSeconds 和CMTimeMake 区别在于,第一个函数的第一个参数可以是float,其他一样。
拖拽方法如下:
- (IBAction)playerSliderValueChanged:(id)sender { _isSliding = YES; [self pause]; // 跳转到拖拽秒处 // self.playProgress.maxValue = value / timeScale // value = progress.value * timeScale // CMTimemake(value, timeScale) = (progress.value, 1.0) CMTime changedTime = CMTimeMakeWithSeconds(self.playProgress.value, 1.0); [_playerItem seekToTime:changedTime completionHandler:^(BOOL finished) { // 跳转完成后 }]; }
AVPlayerItem 是使用 KVO 模式观察状态,和 缓冲进度。而 AVPlayer 给我们直接提供了 观察播放进度更为方便的方法。
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;
方法名如其意, “添加周期时间观察者” ,参数1 interal
为CMTime 类型的,参数2 为一个 返回值为空,参数为 CMTime 的block类型。
简而言之就是,每隔一段时间后执行 block。
比如: 我把时间间隔设置为, 1/ 30 秒,然后 block 里面更新 UI。就是一秒钟更新 30次UI。
播放进度代码如下:
// 观察播放进度 - (void)monitoringPlayback:(AVPlayerItem *)item { __weak typeof(self)WeakSelf = self; // 观察间隔, CMTime 为30分之一秒 _playTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 30.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { if (_touchMode != TouchPlayerViewModeHorizontal) { // 获取 item 当前播放秒 float currentPlayTime = (double)item.currentTime.value/ item.currentTime.timescale; // 更新slider, 如果正在滑动则不更新 if (_isSliding == NO) { [WeakSelf updateVideoSlider:currentPlayTime]; } } else { return; } }]; }
注意: 给 palyer 添加了 timeObserver 后,不使用的时候记得移除 removeTimeObserver
否则会占用大量内存。
比如,我在dealloc里面做了移除:
- (void)dealloc { [self removeObserveAndNOtification]; [_player removeTimeObserver:_playTimeObserver]; // 移除playTimeObserver}
AVPlaerItem 播放完成后,系统会自动发送通知,通知的定义详情请见 AVPlayerItem.h
.
/* Note that NSNotifications posted by AVPlayerItem may be posted on a different thread from the one on which the observer was registered. */ // notifications description AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); // the item's current time has changed discontinuously AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_0); // item has played to its end time AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3); // item has failed to play to its end time AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification NS_AVAILABLE(10_9, 6_0); // media did not arrive in time to continue playback AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new access log entry has been added AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new error log entry has been added // notification userInfo key type AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey NS_AVAILABLE(10_7, 4_3); // NSError
因此,如果我们想要在某个状态下,执行某些操作。监听 AVPlayerItem 的相关通知就行了。 比如,我想要播放完成后,暂停播放。 给AVPlayerItemDidPlayToEndTimeNotification
添加观察者。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; // 播放完成后 - (void)playbackFinished:(NSNotification *)notification { NSLog(@"视频播放完成通知"); _playerItem = [notification object]; [_playerItem seekToTime:kCMTimeZero]; // item 跳转到初始 //[_player play]; // 循环播放 }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
在cell中写的一个视频播放的代理方法:
if ([self.delegate respondsToSelector:@selector(clickVideoButton:)]) {
[self.delegate clickVideoButton:self.indexPath];
}
点击图片的播放按钮会执行代理方法。代理方法的内容如下:
#pragma mark VideoTableViewCell的代理方法
-(void)clickVideoButton:(NSIndexPath *)indexPath {
[self.playView resetPlayView];
VideoTableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
self.currentSelectedCell = cell;
VideoPlayView *playView = [VideoPlayView videoPlayView];
// TTVideo里面主要是一些模型数据
TTVideo *video = self.videoArray[indexPath.row];
playView.frame = video.videoFrame;
[cell addSubview:playView];
cell.playView = playView;
self.playView = playView;
self.playView.delegate = self;
// AVPlayerItem的初始化,根据URL获取AVPlayerItem
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:video.videouri]];
self.playView.playerItem = item;
}
最主要的VideoPlayView的文件有VideoPlayView.h,ViewPlayView.m,VideoPlayView.xib.
其中VideoPlayView.xib的样子如下:
#import
#import
@protocol VideoPlayViewDelegate <NSObject>
@optional
- (void)videoplayViewSwitchOrientation:(BOOL)isFull;
@end
@interface VideoPlayView : UIView
+ (instancetype)videoPlayView;
@property (weak, nonatomic) id<VideoPlayViewDelegate> delegate;
@property (nonatomic, strong) AVPlayerItem *playerItem;
-(void)suspendPlayVideo;
-(void)resetPlayView;
@end
#import "VideoPlayView.h"
@interface VideoPlayView()
/* 播放器 */
@property (nonatomic, strong) AVPlayer *player;
// 播放器的Layer
@property (weak, nonatomic) AVPlayerLayer *playerLayer;
@property (weak, nonatomic) IBOutlet UIImageView *imageView; //底部图片
@property (weak, nonatomic) IBOutlet UIView *toolView;
@property (weak, nonatomic) IBOutlet UIButton *playOrPauseBtn;
@property (weak, nonatomic) IBOutlet UISlider *progressSlider;
@property (weak, nonatomic) IBOutlet UILabel *timeLabel;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *progressView; //旋转的梅花
@property (nonatomic, weak) UITableView *tableView;
@property (nonatomic, assign) NSIndexPath *indexPath;
// 记录当前是否显示了工具栏
@property (assign, nonatomic) BOOL isShowToolView;
/* 定时器 */
@property (nonatomic, strong) NSTimer *progressTimer;
#pragma mark - 监听事件的处理
- (IBAction)playOrPause:(UIButton *)sender;
- (IBAction)switchOrientation:(UIButton *)sender;
- (IBAction)slider;
- (IBAction)startSlider;
- (IBAction)tapAction:(UITapGestureRecognizer *)sender;
- (IBAction)sliderValueChange;
@end
@implementation VideoPlayView
// 快速创建View的方法
+ (instancetype)videoPlayView
{
return [[[NSBundle mainBundle] loadNibNamed:@"VideoPlayView" owner:nil options:nil] firstObject];
}
// 采用了xib,读取xib的时候会调用这个方法
- (void)awakeFromNib
{
[super awakeFromNib];
//////////////////////////////////////////////////////
//第一步:初始化AVPlayer和AVPlayerLayer
self.player = [[AVPlayer alloc] init]; //AVPlayer的初始化
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; // AVPlayerLayer的初始化
//单纯使用AVPlayer类是无法显示视频的,要将视频层添加至AVPlayerLayer中,这样才能将视频显示出来
[self.imageView.layer addSublayer:self.playerLayer];
////////////////////////////////////////////////////////////
self.toolView.alpha = 0;
self.isShowToolView = NO;
[self.progressSlider setThumbImage:[UIImage imageNamed:@"thumbImage"] forState:UIControlStateNormal];
[self.progressSlider setMaximumTrackImage:[UIImage imageNamed:@"MaximumTrackImage"] forState:UIControlStateNormal];
[self.progressSlider setMinimumTrackImage:[UIImage imageNamed:@"MinimumTrackImage"] forState:UIControlStateNormal];
[self removeProgressTimer];
[self addProgressTimer];
self.playOrPauseBtn.selected = YES;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.playerLayer.frame = self.bounds;
}
#pragma mark - 设置播放的视频
//实例化playerItem对象的时候执行这个方法
- (void)setPlayerItem:(AVPlayerItem *)playerItem
{
_playerItem = playerItem;
////////////////////////////////////////////////////////////////////////////////
//这是在执行第二步,在第一步,布局初始化时,AVPlayer 并没有 AVPlayerItem,AVPlayer 提供了 - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item; 方法,用于切换视频。
[self.player replaceCurrentItemWithPlayerItem:playerItem];
////////////////////////////////////////////////////////////////////////////////
//第三步,注册观察者,观察status属性。
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
//播放
[self.player play];
}
// 执行观察者方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
AVPlayerItem *item = (AVPlayerItem *)object;
if (item.status == AVPlayerItemStatusReadyToPlay) {
[self.progressView stopAnimating];
}
}
///////////////////////////////////////////////////////////////////////////////////////
// 是否显示工具的View
- (IBAction)tapAction:(UITapGestureRecognizer *)sender {
[UIView animateWithDuration:0.5 animations:^{
if (self.isShowToolView) {
self.toolView.alpha = 0;//隐藏
self.isShowToolView = NO;
} else {
self.toolView.alpha = 1;
self.isShowToolView = YES;
}
}];
}
//
-(void)dealloc {
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.player replaceCurrentItemWithPlayerItem:nil];
}
/////////////////////////////////////////////////////////////////////////
//第四步:播放过程中响应:播放、 暂停、 跳转
// 暂停按钮的监听
- (IBAction)playOrPause:(UIButton *)sender {
sender.selected = !sender.selected;
if (sender.selected) {
[self.player play];
[self addProgressTimer];
} else {
[self.progressView stopAnimating];
[self.player pause];
[self removeProgressTimer];
}
}
/////////////////////////////////////////////////////////////////////////
- (void)suspendPlayVideo {
[self.progressView stopAnimating];
self.playOrPauseBtn.selected = NO;
self.toolView.alpha = 1;
self.isShowToolView = YES;
[self.player pause];
[self removeProgressTimer];
}
#pragma mark - 定时器操作
- (void)addProgressTimer
{
self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateProgressInfo) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.progressTimer forMode:NSRunLoopCommonModes];
}
- (void)removeProgressTimer
{
[self.progressTimer invalidate];
self.progressTimer = nil;
}
- (void)updateProgressInfo
{
// 1.更新时间
self.timeLabel.text = [self timeString];
// 2.设置进度条的value,CMTimeGetSeconds(self.player.currentItem.duration)的意思是获取要播放的视频的总秒数
self.progressSlider.value = CMTimeGetSeconds(self.player.currentTime) / CMTimeGetSeconds(self.player.currentItem.duration);
}
- (NSString *)timeString
{
NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration);
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentTime);
return [self stringWithCurrentTime:currentTime duration:duration];
}
// 切换屏幕的方向
- (IBAction)switchOrientation:(UIButton *)sender {
sender.selected = !sender.selected;
if ([self.delegate respondsToSelector:@selector(videoplayViewSwitchOrientation:)]) {
[self.delegate videoplayViewSwitchOrientation:sender.selected];
}
}
- (IBAction)slider {
[self addProgressTimer];
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentItem.duration) * self.progressSlider.value;
// 设置当前播放时间,NSEC_PER_SEC是默认值为1秒
[self.player seekToTime:CMTimeMakeWithSeconds(currentTime, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[self.player play];
}
- (IBAction)startSlider {
[self removeProgressTimer];
}
- (IBAction)sliderValueChange {
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentItem.duration) * self.progressSlider.value;
NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration);
self.timeLabel.text = [self stringWithCurrentTime:currentTime duration:duration];
}
- (NSString *)stringWithCurrentTime:(NSTimeInterval)currentTime duration:(NSTimeInterval)duration
{
NSInteger dMin = duration / 60; // 要播放的视频的总分数。
NSInteger dSec = (NSInteger)duration % 60; // 总的分数取出后剩余要播放的秒数
NSInteger cMin = currentTime / 60; // 现在播放的分数。
NSInteger cSec = (NSInteger)currentTime % 60; // 现在播放了的秒数
dMin = dMin<0?0:dMin;
dSec = dSec<0?0:dSec;
cMin = cMin<0?0:cMin;
cSec = cSec<0?0:cSec;
NSString *durationString = [NSString stringWithFormat:@"%02ld:%02ld", (long)dMin, (long)dSec];
NSString *currentString = [NSString stringWithFormat:@"%02ld:%02ld", (long)cMin, (long)cSec];
return [NSString stringWithFormat:@"%@/%@", currentString, durationString];
}
-(void)resetPlayView {
[self suspendPlayVideo];
[self.playerLayer removeFromSuperlayer];
// 替换PlayerItem为nil
[self.player replaceCurrentItemWithPlayerItem:nil];
// 把player置为nil
self.player = nil;
[self removeFromSuperview];
}
@end
其中上面有一个代理方法是
[self.delegate videoplayViewSwitchOrientation:sender.selected];这个方法的实现在另一个控制器中,内容为:
#pragma mark VideoPlayViewDelegate 视频播放时窗口模式与全屏模式切换
- (void)videoplayViewSwitchOrientation:(BOOL)isFull
{
if (isFull) {
self.isFullScreenPlaying = YES;
[self presentViewController:self.fullVc animated:YES completion:^{
self.playView.frame = self.fullVc.view.bounds;
[self.fullVc.view addSubview:self.playView];
}];
} else {
[self.fullVc dismissViewControllerAnimated:YES completion:^{
self.playView.frame = self.currentSelectedCell.video.videoFrame;
[self.currentSelectedCell addSubview:self.playView];
self.isFullScreenPlaying = NO;
}];
}
}
#pragma mark - 懒加载代码
- (FullViewController *)fullVc
{
if (_fullVc == nil) {
self.fullVc = [[FullViewController alloc] init];
}
return _fullVc;
}
#import "FullViewController.h"
@interface FullViewController ()
@end
@implementation FullViewController
- (instancetype)init
{
if (self = [super init]) {
// self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
}
return self;
}
// 当点击了那个按钮后,进入这个控制器的时候会自动执行这个方法,屏幕旋转后播放视频。
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscapeLeft;
}
@end