引言
当前比较火的软件都是和视频挂钩的,所以这次打算由浅到深的学一下视频的各种操作,本篇文章主要讲解以各种的方式播放视频(侧重AVPlayer)
demo传送门:https://github.com/YJExpand/videoPlayerTest
目录
-
一、MPPlayerController
-
二、MPPlayerViewController
-
三、AVPlayerViewController
-
四、AVPlayer(本篇主讲)
- 增加对视频的拉伸处理(AVLayerVideoGravity)
- 增加对视频的旋转处理
- 视频进度,缓存
- 视频手势处理
- 对系统声音的操作
- 视频速率
- 切换视频
原生框架
- #import <MediaPlayer/MediaPlayer.h>
- #import <AVFoundation/AVFoundation.h>
- #import <AVKit/AVKit.h>
播放方式优缺点分析
一、MPPlayerController
MPMoviePlayerController继承NSObject,显示视频需使用MPMoviePlayerController.view,必须设置frame
依赖
#import <MediaPlayer/MediaPlayer.h>
使用方式
/// 初始化
// 网络路径
// NSURL *webUrl =[NSURL URLWithString:@"http://www.xxxx.com/x/x.mp4"];
// 本地路径
NSURL *localUrl = [[NSBundle mainBundle] URLForResource:@"lin" withExtension:@"mp4"];
self.playerController = [[MPMoviePlayerController alloc] initWithContentURL:localUrl];
// 播放控件的样式
self.playerController.controlStyle = MPMovieControlStyleDefault;
// 是否自动播放
self.playerController.shouldAutoplay = NO;
// 播放模式
self.playerController.repeatMode = MPMovieRepeatModeOne;
/*----------还有视频时间、大小等等。。自行查看头文件---------*/
// 重点
[self.videoView addSubview:self.playerController.view];
// 必须要设置Frame
self.playerController.view.frame = self.videoView.bounds;
/* 对视频的操作 ***********/
// 播放前,停止所有的播放
[self.playerController prepareToPlay];
// 播放
[self.playerController play];
// 暂停
[self.playerController pause];
// 停止
[self.playerController stop];
注意事项
/// 如果使用临时变量保存,则会出现黑屏,必须使用全局变量缓存(iOS9.0后,已弃用)
@property (strong ,nonatomic) MPMoviePlayerController *playerController;
二、MPPlayerViewController
MPMoviePlayerViewController继承UIViewController,所以可以直接跳转控制器(push、presentViewController等),也可以直接将MPMoviePlayerViewController.moviePlayer.view添加到视图。主要使用MPMoviePlayerViewController.moviePlayer操作视频,具体可参考上面的《一、MPPlayerController》
依赖
#import <MediaPlayer/MediaPlayer.h>
使用方式
// 网络路径
// NSURL *webUrl =[NSURL URLWithString:@"http://www.xxxx.com/x/x.mp4"];
// 本地路径
NSURL *localUrl = [[NSBundle mainBundle] URLForResource:@"lin" withExtension:@"mp4"];
self.playerVC = [[MPMoviePlayerViewController alloc] initWithContentURL:localUrl];
/*
MPMoviePlayerViewController 简单粗暴,不需要写UI,但是自定义UI不行
视频的操作都在 MPMoviePlayerViewController.moviePlayer
具体可以参考《MPPlayerController -> 使用》
*/
// [self.videoView addSubview:self.playerVC.moviePlayer.view];
// self.playerVC.moviePlayer.view.frame = self.videoView.bounds;
// [self.playerVC.moviePlayer play];
注意事项
/// 若想addSubview到当前的self.view中,就必须使用全局变量,具体原因可参考《一、MPPlayerController》(iOS9.0后,已弃用)
@property (strong , nonatomic )MPMoviePlayerViewController *playerVC;
三、AVPlayerViewController
AVPlayerViewController继承UIViewController,可以说是MPMoviePlayerViewController的替代品,目前也是苹果推荐使用的。主要使用AVPlayerViewController.player操作视频,具体可参考下面的《四、AVPlayer》
依赖
#import <AVKit/AVKit.h>
使用方式
// 本地路径
NSURL *localUrl = [[NSBundle mainBundle] URLForResource:@"lin" withExtension:@"mp4"];
AVPlayerViewController *playVC = [[AVPlayerViewController alloc] init];
/// 基本操作都是player , 可参考YJAVPlayerViewController(AVPlayer的使用)
playVC.player = [AVPlayer playerWithURL:localUrl];
// 具体使用参考<AVPlayerViewControllerDelegate>
playVC.delegate = self;
[playVC.player play];
/*
可以使用控制器的方式使用,也可以[self.view addSubview:playVC.view],
设置playVC.view.frame就好了
*/
[self presentViewController:playVC animated:YES completion:nil];
四、AVPlayer(本篇主讲)
依赖
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
全局缓存视频相关的属性
@property (weak, nonatomic) IBOutlet UIView *videoView;
@property (weak, nonatomic) IBOutlet UIView *videoBackgroundView;
@property (strong , nonatomic) AVPlayer *player;
@property (strong , nonatomic) AVPlayerItem *playerItem;
@property (strong , nonatomic) AVPlayerLayer *playerLayer;
使用方式
/// 初始化
NSURL *localUrl = [[NSBundle mainBundle] URLForResource:@"lin" withExtension:@"mp4"];
self.playerItem = [[AVPlayerItem alloc] initWithURL:localUrl];
// 在线链接
// NSURL *webUrl =[NSURL URLWithString:@"http://www.xxxx.com/x/x.mp4"];
// AVAsset *asset = [AVAsset assetWithURL:webURL];
// self.playerItem = [[AVPlayerItem alloc] initWithAsset:asset];
self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];
/*
AVPlayer要显示,就要结合AVPlayerLayer
*/
self.playerLayer = [[AVPlayerLayer alloc] init];
// 设置
self.playerLayer.player = self.player;
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
self.playerLayer.contentsScale = [UIScreen mainScreen].scale;
// 添加到页面
[self.videoBackgroundView.layer addSublayer:self.playerLayer];
// 必须设置frame,建议在viewDidLayoutSubviews设置
self.playerLayer.frame = self.videoView.bounds;
/***操作******/
// 播放
[self.player play];
// 暂停
[self.player pause];
拓展
1、增加对视频的拉伸处理(AVLayerVideoGravity)
// AVLayerVideoGravityResizeAspect : 默认原画(不拉伸,不平铺)
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// AVLayerVideoGravityResizeAspectFill : 不拉伸,平铺
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
// AVLayerVideoGravityResize : 拉伸平铺
self.playerLayer.videoGravity = AVLayerVideoGravityResize;
2、增加对视频的旋转处理
/*
CGAffineTransform CGAffineTransformMake (CGFloat a,CGFloat b,CGFloat c,CGFloat d,CGFloat tx,CGFloat ty);
其中tx用来控制在x轴方向上的平移
ty用来控制在y轴方向上的平移
a用来控制在x轴方向上的缩放
d用来控制在y轴方向上的缩放
abcd共同控制旋转
*/
// 正常竖屏
CATransform3D normal = CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(0));
// 倒竖屏
CATransform3D inverted = CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(M_PI));
// 右横屏
CATransform3D right = CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(-M_PI_2));
// 左横屏
CATransform3D left = CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(M_PI_2));
switch (self.style) {
case videoShowStyleVertical_normal:
self.playerLayer.transform = left;
self.style = videoShowStyleHorizontal_left;
break;
case videoShowStyleHorizontal_left:
self.playerLayer.transform = right;
self.style = videoShowStyleHorizontal_right;
break;
case videoShowStyleHorizontal_right:
self.playerLayer.transform = normal;
self.style = videoShowStyleVertical_normal;
break;
default:
break;
}
self.playerLayer.frame = self.videoView.bounds;
注释
/// 视频显示样式
typedef enum : NSUInteger {
/// 普通竖屏
videoShowStyleVertical_normal = 0,
/// 左横屏
videoShowStyleHorizontal_left = 1,
/// 右横屏
videoShowStyleHorizontal_right = 2,
} videoShowStyle;
/// 视频显示样式
@property (assign , nonatomic) videoShowStyle style;
3、视频进度,缓存
UI方面主要用了UIProgressView->缓存进度、UISlider->播放进度 构成。
/// 缓存进度
@property (weak, nonatomic) IBOutlet UIProgressView *cacheProgressView;
/// 播放进度
@property (weak, nonatomic) IBOutlet UISlider *playProgressView;
/// 总时间
@property (weak, nonatomic) IBOutlet UILabel *totalTimeLB;
/// 播放时间
@property (weak, nonatomic) IBOutlet UILabel *currentTimeLB;
准备就绪、缓存进度使用KVO获取
static NSString * const PlayerItemStatusContext = @"PlayerItemStatusContext";
static NSString * const PlayerItemLoadedTimeRangesContext = @"PlayerItemLoadedTimeRangesContext";
/*
可以监听self.playerItem.status 判断当前媒体的状态
AVPlayerItemStatusUnknown -> 不在播放队列
AVPlayerItemStatusReadyToPlay -> 可播放
AVPlayerItemStatusFailed -> 播放失败
// 代码
[self.playerItem addObserver:self forKeyPath:@"status" options:0 context:nil];
*/
/// 增加监听 ,注意要在dealloc移除,否则崩溃
[self.playerItem addObserver:self
forKeyPath:@"status"
options:0
context:(__bridge void *)(PlayerItemStatusContext)];
// 监听当前视频的缓存程度
[self.playerItem addObserver:self
forKeyPath:@"loadedTimeRanges"
options:0
context:(__bridge void *)PlayerItemLoadedTimeRangesContext];
//监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
AVPlayerItem *item = (AVPlayerItem *)object;
if (context == (__bridge void *)(PlayerItemStatusContext)) {
NSLog(@"item.status -> %zd",item.status);
/// 当前播放时间
NSTimeInterval current = CMTimeGetSeconds(item.currentTime);
/// 总时间
NSTimeInterval total = CMTimeGetSeconds(item.duration);
self.totalTimeLB.text = [self formatWithTime:total];
self.currentTimeLB.text = [self formatWithTime:current];
}
if (context == (__bridge void *)PlayerItemLoadedTimeRangesContext) {
[self.cacheProgressView setProgress:[self availableDurationWithplayerItem:item]];
}
}
/// 获取当前缓存进度算法
- (NSTimeInterval)availableDurationWithplayerItem:(AVPlayerItem *)playerItem{
NSArray *loadedTimeRanges = [playerItem loadedTimeRanges];
CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];// 获取缓冲区域
NSTimeInterval startSeconds = CMTimeGetSeconds(timeRange.start);
NSTimeInterval durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds; // 计算缓存总进度
return result;
}
/// 格式化时间
- (NSString *)formatWithTime:(NSTimeInterval)duration{
int minute = 0, hour = 0, secend = duration;
minute = (secend % 3600)/60;
hour = secend / 3600;
secend = secend % 60;
return [NSString stringWithFormat:@"%02d:%02d:%02d", hour, minute, secend];
}
移动控制条,调节视频进度
主要UISlider的touchDown、touchUpInside控制
/// touchDown
- (IBAction)sliderBeginClick:(UISlider *)sender {
// 当可以播放视频时才能操作进度条
if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
if (self.operationBtns.selectedSegmentIndex == 0) { // 播放状态
[self.player pause];
}
}
}
/// touchUpInside
- (IBAction)sliderEndClick:(UISlider *)sender {
// 当可以播放视频时才能操作进度条
if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
/// 当前定位
float progress = sender.value;
NSTimeInterval currentTime = CMTimeGetSeconds(self.playerItem.duration) * progress;
[self playVideoWithTime:currentTime];
}
}
/// 定点播放视频
- (void)playVideoWithTime:(NSTimeInterval)currentTime{
CMTime time = CMTimeMake(currentTime, 1);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
}];
if (self.operationBtns.selectedSegmentIndex == 0) { // 播放状态
[self.player play];
}
}
视频进度影响进度条
/*
AVPlayer提供一个block,当播放进度改变时,则会自动调取block,
@param interval 在正常回放期间,根据播放机当前时间的进度,调用块的间隔。
*/
__weak __typeof(self) weakSelf = self;
[self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
/// 当前播放时间
NSTimeInterval current = CMTimeGetSeconds(time);
/// 总时间
NSTimeInterval total = CMTimeGetSeconds(weakSelf.player.currentItem.duration);
/// 当前进度
float progress = current / total;
[weakSelf.playProgressView setValue:progress animated:YES];
weakSelf.totalTimeLB.text = [weakSelf formatWithTime:total];
weakSelf.currentTimeLB.text = [weakSelf formatWithTime:current];
}];
4、视频手势处理
声音、亮度UI:
主要以UILabel和UIProgressView组成
/*
❗️注意 : 测试亮度和音量大小需使用真机
*/
/// 系统操作view
@property (weak, nonatomic) IBOutlet UIView *systemOperationView;
/// 操作名称
@property (weak, nonatomic) IBOutlet UILabel *systemOperationNameLB;
/// 操作进度
@property (weak, nonatomic) IBOutlet UIProgressView *systemOperationProgressView;
/// 亮度
@property (assign , nonatomic) float brightnessProgress;
/// 音量
@property (assign , nonatomic) float volumeProgress;
/// 系统音量控件
@property (strong , nonatomic) UISlider* volumeViewSlider;
参考了众多的播放器,都是
左右滑动->进度
左屏 上下滑动 -> 亮度
右屏 上下滑动 -> 声音
// 初始化亮度、声音
self.brightnessProgress = [UIScreen mainScreen].brightness;
self.volumeProgress = [[AVAudioSession sharedInstance] outputVolume];
// 添加手势
UIPanGestureRecognizer *moveTag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(movePanWithGesture:)];
self.videoView.userInteractionEnabled = YES;
[self.videoView addGestureRecognizer:moveTag];
/// 手势滑动
- (void)movePanWithGesture:(UIPanGestureRecognizer *)tag{
// 不可拖动
if (self.playerItem.status != AVPlayerItemStatusReadyToPlay) return;
/*
translationInView:方法获取View的偏移量;
setTranslation:方法设置手势的偏移量;
velocityInView:方法获取速度;
*/
// 获取view的偏移量
CGPoint point = [tag translationInView:self.videoView];
if (tag.state == UIGestureRecognizerStateBegan) { // 开始拖动
NSLog(@"Began : point.x -> %f , point.y -> %f",point.x,point.y);
self.progress_panBeginPoint = point;
} else if (tag.state == UIGestureRecognizerStateEnded) { // 停止拖动
NSLog(@"Ended : point.x -> %f , point.y -> %f",point.x,point.y);
switch (self.currentPanStyle) {
case panGestureStyleProgress:{
/// 当前定位,开始播放
float progress = self.playProgressView.value;
NSTimeInterval currentTime = CMTimeGetSeconds(self.playerItem.duration) * progress;
[self playVideoWithTime:currentTime];
break;
}
case panGestureStyleBrightness:
case panGestureStyleVolume:{
// 一秒后隐藏
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.systemOperationView.hidden = true;
});
break;
}
default:
break;
}
// 格式化
self.currentPanStyle = panGestureStyleNone;
}else if (tag.state == UIGestureRecognizerStateChanged) { // 拖动中
NSLog(@"Changed : point.x -> %f , point.y -> %f",point.x,point.y);
if (self.currentPanStyle == panGestureStyleNone) { // 未区分手势滑动的样式
float moveX = fabs(point.x - self.progress_panBeginPoint.x);
float moveY = fabs(point.y - self.progress_panBeginPoint.y);
CGPoint currentPoint = [tag locationInView:self.videoBackgroundView];
if (moveX >= moveY) { // 属于进度操作
self.currentPanStyle = panGestureStyleProgress;
if (self.operationBtns.selectedSegmentIndex == 0) { // 正在播放,暂停播放
[self.player pause];
}
}else if (currentPoint.x <= self.videoView.frame.size.width / 2){ // 亮度操作
self.currentPanStyle = panGestureStyleBrightness;
}else{// 音量操作
self.currentPanStyle = panGestureStyleVolume;
}
}
switch (self.currentPanStyle) {
case panGestureStyleProgress:{ // 进度
float move = point.x - self.progress_panBeginPoint.x;
float progress = (move / self.videoView.frame.size.width);
float playProgressValue = self.playProgressView.value;
float progressEnd = playProgressValue + progress;
if (progressEnd > 1) { // 直接加载完
[self.playProgressView setValue:1 animated:YES];
} else if (progressEnd < 0){ // 重新加载
[self.playProgressView setValue:0 animated:YES];
}else{
[self.playProgressView setValue:progressEnd animated:YES];
}
break;
}
case panGestureStyleVolume:{ // 音量
self.systemOperationView.hidden = false;
float move = self.progress_panBeginPoint.y - point.y;
self.volumeProgress = (move / self.videoView.frame.size.height) * 4 + self.volumeProgress; // *4 -> 更容易改变音量,不需要滑动更多
self.systemOperationNameLB.text = @"音量";
if (self.volumeProgress > 1) { // 已经是最大了
self.volumeProgress = 1;
} else if (self.volumeProgress < 0){ // 已经是最小了
self.volumeProgress = 0;
}
[self.systemOperationProgressView setProgress:self.volumeProgress];
// 调节音量
[self.volumeViewSlider setValue:self.volumeProgress];
break;
}
case panGestureStyleBrightness:{ // 亮度
self.systemOperationView.hidden = false;
float move = self.progress_panBeginPoint.y - point.y;
self.brightnessProgress = (move / self.videoView.frame.size.height) * 4 + self.brightnessProgress; // *4 -> 更容易改变亮度,不需要滑动更多
self.systemOperationNameLB.text = @"亮度";
if (self.brightnessProgress > 1) { // 已经是最大了
self.brightnessProgress = 1;
} else if (self.brightnessProgress < 0){ // 已经是最小了
self.brightnessProgress = 0;
}
[self.systemOperationProgressView setProgress:self.brightnessProgress];
// 调节亮度
[[UIScreen mainScreen] setBrightness:self.brightnessProgress];
break;
}
default:
break;
}
self.progress_panBeginPoint = point;
}
}
注释
/// 当前手势操作
typedef enum : NSUInteger {
/// 默认无
panGestureStyleNone = 0,
/// 进度操作
panGestureStyleProgress = 1,
/// 屏幕亮度操作
panGestureStyleBrightness = 2,
/// 音量大小操作
panGestureStyleVolume = 3,
} panGestureStyle;
/// 当前手势操作
@property (assign , nonatomic) panGestureStyle currentPanStyle;
5、对系统声音的操作
依赖
#import <MediaPlayer/MediaPlayer.h>
若要对系统声音进行操作,必须要取出系统的MPVolumeSlider
// 设置音量控件
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
volumeView.frame = CGRectZero;
[self.view addSubview:volumeView];
for (UIView *view in [volumeView subviews]){
if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
// 缓存
self.volumeViewSlider = (UISlider*)view;
break;
}
}
[self.volumeViewSlider setValue:self.volumeProgress animated:YES];
[self.volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
// 监听音量变化
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(systemVolumeDidChangeNoti:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
/// 音量变化
- (void)systemVolumeDidChangeNoti:(NSNotification *)noti{
float voiceSize = [[noti.userInfo valueForKey:@"AVSystemController_AudioVolumeNotificationParameter"]floatValue];
self.volumeProgress = voiceSize;
}
6、视频速率
主要设置player.rate 属性
// 修改速率
self.player.rate = rate;
7、切换视频
主要是监听视频播放完成 - > 切换视频/重播
/// 监听视频是否播放完成
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayEndNoti:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
/// 视频播放完毕
- (void)videoPlayEndNoti:(NSNotification *)noti{
self.switchBackgroundView.hidden = false;
}
选择操作
- (IBAction)videoSwitchBtnClick:(UIButton *)sender {
self.switchBackgroundView.hidden = true;
switch (sender.tag) {
case 1: // 重播
[self playVideoWithTime:0];
break;
case 2: // 下一个
// 1、先移除监听,避免崩溃
[self playerItemRemoveObserver];
// 2、重写playerItem
self.playerItem = [AVPlayerItem playerItemWithURL:[YJManager getLocalVideoURL]];
// 3、替换
[self.player replaceCurrentItemWithPlayerItem:self.playerItem];
// 4、添加监听
[self playerItemAddObserver];
// 5、播放
[self.player play];
self.operationBtns.selectedSegmentIndex = 0;
break;
default:
break;
}
}
总结
关于视频播放的操作基本完成,应该可以满足大多数的需求,本次主要讲解实现思路,未进行封装处理,后续有时间再封装个框架
本文修改时间2020.04.02
效果图