iOS-视频播放

引言

当前比较火的软件都是和视频挂钩的,所以这次打算由浅到深的学一下视频的各种操作,本篇文章主要讲解以各种的方式播放视频(侧重AVPlayer)
demo传送门:https://github.com/YJExpand/videoPlayerTest

目录

  • 一、MPPlayerController

  • 二、MPPlayerViewController

  • 三、AVPlayerViewController

  • 四、AVPlayer(本篇主讲)

    1. 增加对视频的拉伸处理(AVLayerVideoGravity)
    2. 增加对视频的旋转处理
    3. 视频进度,缓存
    4. 视频手势处理
    5. 对系统声音的操作
    6. 视频速率
    7. 切换视频

原生框架

  1. #import <MediaPlayer/MediaPlayer.h>
  2. #import <AVFoundation/AVFoundation.h>
  3. #import <AVKit/AVKit.h>

播放方式优缺点分析

视频播放方式.jpg

一、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(本篇主讲)

视频播放AVPlayer.jpg

依赖

 

#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->播放进度 构成。

Snip20200402_2.png

 

/// 缓存进度
@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组成

Snip20200402_3.png

 

/*
 ❗️注意 : 测试亮度和音量大小需使用真机
 */
/// 系统操作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

效果图

结果.gif



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值