1. 程式人生 > >iOS開發之AVPlayer的精彩使用--->網易新聞視訊播放介面的另類實現

iOS開發之AVPlayer的精彩使用--->網易新聞視訊播放介面的另類實現

遇到個需求需要涉及到視訊播放,那麼沒辦法,先找資料開始進一步瞭解下這個不熟悉的東西.一個是MP,一個AVMP是封裝好的,用起來非常簡單,但是自定義樣式就基本不可能了。AVPlayer存在於AVFundation中,更接近於底層,所以靈活性更強大,廢話不多說,咱們先簡單寫個Demo看下他的工作原理,然後模仿網易新聞寫個介面出來,這裡用到了一個封裝的框架,如果不熟悉內部原理的同學可以先看看我寫的第一個Demo,基本所有邏輯都有。

開發中,單純的使用AVPlayer類是無法播放視訊的,需要將視訊層新增到AVPLayerLayer層,這樣視訊才能顯示出來,Layer的定義方式有兩種,一種是下面這種直接使用

PlayerLayer,還有一個就是自己做一個View,然後把他自身的Layer改成playerLayer

第一種方式:

self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.videoGravity     = AVLayerVideoGravityResizeAspect;
self.playerLayer.frame = self.view.bounds;
[self.view.layer addSublayer:self.playerLayer];

第二種方式:

//修改當前view的 layer的 class
+(Class)layerClass
{
    //AVPlayerLayer
    return [AVPlayerLayer class];
}

      

只能上傳2M的東東,這視訊一幀一幀消耗太快了,都不敢多錄了,各位大爺將就著看吧。。。。。。

不要來打我,不然我讓我表哥打死你     

先簡單介紹下AVPlayer的用法

很多朋友應該和我一樣,一開始接觸視訊的時候都不知道用什麼東東來寫,如果是大神

就直接下載Demo吧。小白來介紹下,我也第一次用

第一:初始化播放器

// 初始化播放器item
    self.playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:@"http://flv2.bn.netease.com/videolib3/1608/30/zPuaL7429/SD/zPuaL7429-mobile.mp4"]];
    self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];
    // 初始化播放器的Layer
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    // layer的frame
    self.playerLayer.frame = self.backView.bounds;
    // layer的填充屬性 和UIImageView的填充屬性類似
    // AVLayerVideoGravityResizeAspect 等比例拉伸,會留白
    // AVLayerVideoGravityResizeAspectFill // 等比例拉伸,會裁剪
    // AVLayerVideoGravityResize // 保持原有大小拉伸
    self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    // 把Layer加到底部View上
    [self.backView.layer insertSublayer:self.playerLayer atIndex:0];
第二:給播放器加監聽以及螢幕旋轉的通知
// 監聽播放器狀態變化
    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    // 監聽快取進去,就是大家所看到的一開始進去底部灰色的View會迅速載入
    [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    
    
    //旋轉螢幕通知
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(onDeviceOrientationChange)
                                                 name:UIDeviceOrientationDidChangeNotification
                                               object:nil
     ];

第三步:實現KVO的監聽方法
// 監聽播放器的變化屬性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"])
    {
         AVPlayerItemStatus statues = [change[NSKeyValueChangeNewKey] integerValue];
        switch (statues) {
                // 監聽到這個屬性的時候,理論上視訊就可以進行播放了
            case AVPlayerItemStatusReadyToPlay:
                
                // 最大值直接用sec,以前都是
                // CMTimeMake(幀數(slider.value * timeScale), 幀/sec)
                self.slider.maximumValue = CMTimeGetSeconds(self.playerItem.duration);
                [self initTimer];
                // 啟動定時器 5秒自動隱藏
                if (!self.autoDismissTimer)
                {
                    self.autoDismissTimer = [NSTimer timerWithTimeInterval:8.0 target:self selector:@selector(autoDismissView:) userInfo:nil repeats:YES];
                    [[NSRunLoop currentRunLoop] addTimer:self.autoDismissTimer forMode:NSDefaultRunLoopMode];
                }
                break;
                
            case AVPlayerItemStatusUnknown:
                
                
                
                break;
                // 這個就是不能播放嘍,載入失敗了
            case AVPlayerItemStatusFailed:
                
                // 這時可以通過`self.player.error.description`屬性來找出具體的原因
                
                break;
                
            default:
                break;
        }
    }
    else if ([keyPath isEqualToString:@"loadedTimeRanges"]) // 監聽快取進度的屬性
    {
        // 計算快取進度
        NSTimeInterval timeInterval = [self availableDuration];
        // 獲取總長度
        CMTime duration = self.playerItem.duration;
        
        CGFloat durationTime = CMTimeGetSeconds(duration);
        // 監聽到了給進度條賦值
        [self.progressView setProgress:timeInterval / durationTime animated:NO];
    }
}

AVPlayerItemStatusReadyToPlay

AVPlayerItemStatusFailed

這兩個屬性還比較好理解,是個人都知道,但是這個是什麼鬼

AVPlayerItemStatusUnknown內部是這麼解釋的

Indicates that the status of the player item is not yet known because it has not tried to load new media resourcesfor playback.



第四步:呼叫Player的方法觀察時間變化更新播放進度


// 呼叫plaer的物件進行UI更新
- (void)initTimer
{
    // player的定時器
    __weak typeof(self)weakSelf = self;
    // 每秒更新一次UI Slider
    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        
        // 當前時間
        CGFloat nowTime = CMTimeGetSeconds(weakSelf.playerItem.currentTime);
        // 總時間
        CGFloat duration = CMTimeGetSeconds(weakSelf.playerItem.duration);
        // sec 轉換成時間點
        weakSelf.nowLabel.text = [weakSelf convertToTime:nowTime];
        weakSelf.remainLabel.text = [weakSelf convertToTime:(duration - nowTime)];
        
        // 不是拖拽中的話更新UI
        if (!weakSelf.isDragSlider)
        {
            weakSelf.slider.value = CMTimeGetSeconds(weakSelf.playerItem.currentTime);
        }
        
    }];
}
// sec 轉換成指定的格式
- (NSString *)convertToTime:(CGFloat)time
{
    // 初始化格式物件
    NSDateFormatter *fotmmatter = [[NSDateFormatter alloc] init];
    // 根據是否大於1H,進行格式賦值
    if (time >= 3600)
    {
        [fotmmatter setDateFormat:@"HH:mm:ss"];
    }
    else
    {
        [fotmmatter setDateFormat:@"mm:ss"];
    }
    // 秒數轉換成NSDate型別
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:time];
    // date轉字串
    return [fotmmatter stringFromDate:date];
}

第五步:給背景View加個手勢,點選的時候讓title和時間進度條消失或者幾秒鐘自動消失
// 啟動定時器 5秒自動隱藏
                // 咱們這種初始化定時器的方式需要自己手動加到runloop上
                // scheduledTimerWithTimeInterval用這個的時候就不需要手動加到runloop中
                if (!self.autoDismissTimer)
                {
                    self.autoDismissTimer = [NSTimer timerWithTimeInterval:8.0 target:self selector:@selector(autoDismissView:) userInfo:nil repeats:YES];
                    [[NSRunLoop currentRunLoop] addTimer:self.autoDismissTimer forMode:NSDefaultRunLoopMode];
                }
#pragma mark - 自動隱藏bottom和top
- (void)autoDismissView:(NSTimer *)timer
{
    // player的屬性rate
    /* indicates the current rate of playback; 0.0 means "stopped", 1.0 means "play at the natural rate of the current item" */
    if (self.player.rate == 0)
    {
        // 暫停狀態就不隱藏
    }
    else if (self.player.rate == 1)
    {
        if (self.bottomView.alpha == 1)
        {
            [UIView animateWithDuration:1.0 animations:^{
               
                self.bottomView.alpha = 0;
                self.topView.alpha = 0;
                
            }];
        }
    }
}

第六步:來個全屏小螢幕切換示例

其實切換的時候就是把只之前的Layer移除,然後重新佈局,加到KeyWindow中去

// 全屏顯示
-(void)toFullScreenWithInterfaceOrientation:(UIInterfaceOrientation )interfaceOrientation{
    // 先移除之前的
    [self.backView removeFromSuperview];
    // 初始化
    self.backView.transform = CGAffineTransformIdentity;
    if (interfaceOrientation==UIInterfaceOrientationLandscapeLeft) {
        self.backView.transform = CGAffineTransformMakeRotation(-M_PI_2);
    }else if(interfaceOrientation==UIInterfaceOrientationLandscapeRight){
        self.backView.transform = CGAffineTransformMakeRotation(M_PI_2);
    }
    // BackView的frame能全屏
    self.backView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight);
    // layer的方向寬和高對調
    self.playerLayer.frame = CGRectMake(0, 0, kScreenHeight, kScreenWidth);
    
    // remark 約束
    [self.bottomView mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(50);
        make.top.mas_equalTo(kScreenWidth-50);
        make.left.equalTo(self.backView).with.offset(0);
        make.width.mas_equalTo(kScreenHeight);
    }];
    
    [self.topView mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(50);
        make.left.equalTo(self.backView).with.offset(0);
        make.width.mas_equalTo(kScreenHeight);
    }];
    
    [self.closeButton mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.backView).with.offset(5);
        make.height.mas_equalTo(30);
        make.width.mas_equalTo(30);
        make.top.equalTo(self.backView).with.offset(10);
        
    }];

    [self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.topView).with.offset(45);
        make.right.equalTo(self.topView).with.offset(-45);
        make.center.equalTo(self.topView);
        make.top.equalTo(self.topView).with.offset(0);
    }];
    
    [self.nowLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.slider.mas_left).with.offset(0);
        make.top.equalTo(self.slider.mas_bottom).with.offset(0);
        make.size.mas_equalTo(CGSizeMake(100, 20));
    }];
    
    [self.remainLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.right.equalTo(self.slider.mas_right).with.offset(0);
        make.top.equalTo(self.slider.mas_bottom).with.offset(0);
        make.size.mas_equalTo(CGSizeMake(100, 20));
    }];
    // 加到window上面
    [[UIApplication sharedApplication].keyWindow addSubview:self.backView];
}


// 縮小到cell
-(void)toCell{
    // 先移除
    [self.backView removeFromSuperview];
    
    __weak typeof(self)weakSelf = self;
    [UIView animateWithDuration:0.5f animations:^{
        weakSelf.backView.transform = CGAffineTransformIdentity;
        weakSelf.backView.frame = CGRectMake(0, 80, kScreenWidth, kScreenHeight / 2.5);
        weakSelf.playerLayer.frame =  weakSelf.backView.bounds;
        // 再新增到View上
        [weakSelf.view addSubview:weakSelf.backView];
        
        // remark約束
        [self.bottomView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(weakSelf.backView).with.offset(0);
            make.right.equalTo(weakSelf.backView).with.offset(0);
            make.height.mas_equalTo(50);
            make.bottom.equalTo(weakSelf.backView).with.offset(0);
        }];
        [self.topView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(weakSelf.backView).with.offset(0);
            make.right.equalTo(weakSelf.backView).with.offset(0);
            make.height.mas_equalTo(50);
            make.top.equalTo(weakSelf.backView).with.offset(0);
        }];
        
        [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(weakSelf.backView).with.offset(5);
            make.centerY.equalTo(weakSelf.topView);
            make.size.mas_equalTo(CGSizeMake(30, 30));
        }];
    }completion:^(BOOL finished) {
        
    }];
}

基本邏輯差不多介紹完了,效果就這樣的



  

下面咱們試著寫個網易播放視訊的Demo,在tableView中使用下,效果圖已經在最上面了

這裡無非多了幾個屬性

@property (nonatomic,strong)NSIndexPath *currentIndexPath; // 當前播放的cell

@property (nonatomic,assign)BOOL isSmallScreen; //是否放置在window上

@property(nonatomic,strong)ViedoTableViewCell *currentCell; // 當前cell

分析1:全屏小屏切換的時候回到指定的cell,那麼先點選播放記錄位置

1.第一種cell播放:Layer是載入到cell上的背景圖片區域的 滾動的時候要記錄當前cell

2.第二種全屏播放:Layer是載入到Window上的 frame全屏

3.第三種小窗播放:它其實就是全屏播放的一個特例,也是載入到Window上的,frame自定義

其實不同狀態的切換無非就是Layer所在View的位置不停切換

下面這個方法就是記錄當前播放的cell下標

#pragma mark - 播放器播放

- (void)startPlayVideo:(UIButton *)sender
{
    // 獲取當前的indexpath
    self.currentIndexPath = [NSIndexPath indexPathForRow:sender.tag inSection:0];
    
    // iOS 7 和 8 以上獲取cell的方式不同
    if ([UIDevice currentDevice].systemVersion.floatValue>=8||[UIDevice currentDevice].systemVersion.floatValue<7) {
        self.currentCell = (ViedoTableViewCell *)sender.superview.superview;
    }else{//ios7系統 UITableViewCell上多了一個層級UITableViewCellScrollView
        self.currentCell = (ViedoTableViewCell *)sender.superview.superview.subviews;
    }
    ViedoModel *model = [self.viedoLists objectAtIndex:sender.tag];
    
    // 小視窗的時候點選播放另一個 先移除掉
    if (self.isSmallScreen) {
        [self releaseWMPlayer];
        self.isSmallScreen = NO;
        
    }
    // 當有上一個在播放的時候 點選 就先release
    if (self.wmPlayer) {
        [self releaseWMPlayer];
        self.wmPlayer = [[WMPlayer alloc]initWithFrame:self.currentCell.mainImageView.bounds];
        self.wmPlayer.delegate = self;
        self.wmPlayer.closeBtnStyle = CloseBtnStyleClose;
        self.wmPlayer.URLString = model.mp4URL;
        self.wmPlayer.titleLabel.text = model.title;
        //        [wmPlayer play];
    }else{
        // 當沒有一個在播放的時候
        self.wmPlayer = [[WMPlayer alloc]initWithFrame:self.currentCell.mainImageView.bounds];
        self.wmPlayer.delegate = self;
        self.wmPlayer.closeBtnStyle = CloseBtnStyleClose;
        self.wmPlayer.titleLabel.text = model.title;
        self.wmPlayer.URLString = model.mp4URL;
    }
    // 把播放器加到當前cell的imageView上面
    [self.currentCell.mainImageView addSubview:self.wmPlayer];
    [self.currentCell.mainImageView bringSubviewToFront:self.wmPlayer];
    [self.currentCell.playButton.superview sendSubviewToBack:self.currentCell.playButton];
    [self.tableView reloadData];

分析2:上下滾動的時候根據座標切換cell顯示還是小窗顯示

#pragma mark scrollView delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if(scrollView ==self.tableView){
        if (self.wmPlayer==nil) {
            return;
        }
        
        if (self.wmPlayer.superview) {
            // 當前cell在tableView中的frame
//            (lldb) po rectInTableView
//            (origin = (x = 0, y = 0), size = (width = 375, height = 300))
            CGRect rectInTableView = [self.tableView rectForRowAtIndexPath:self.currentIndexPath];
            // 把當前的frame從tableView轉換到螢幕View上面去
//            (lldb) po rectInSuperview
//            (origin = (x = 0, y = 61), size = (width = 375, height = 300))
            CGRect rectInSuperview = [self.tableView convertRect:rectInTableView toView:[self.tableView superview]];
            NSLog(@"Y軸變化:%lf,currentCell:%lf",rectInSuperview.origin.y,self.currentCell.mainImageView.frame.size.height);
            // 當網上移出螢幕的時候或者往下移出螢幕的時候,根據邏輯是否載入到小窗上來
            if (rectInSuperview.origin.y<-self.currentCell.mainImageView.frame.size.height ||rectInSuperview.origin.y>kScreenHeight-kNavbarHeight-kTabBarHeight) {//往上拖動
                // 如果已經小螢幕顯示了,就不做任何操作
                if ([[UIApplication sharedApplication].keyWindow.subviews containsObject:self.wmPlayer]&&self.isSmallScreen) {
                    self.isSmallScreen = YES;
                }else{
                    //放widow上,小屏顯示 這裡的邏輯和展示到全屏是一樣的道理,只是位置和frame自己定義就好了,想放哪就放哪
                    [self toSmallScreen];
                }
                
            }else{
                // 如果已經在cell裡面了,那麼就不做任何操作
                if ([self.currentCell.mainImageView.subviews containsObject:self.wmPlayer]) {
                    
                }else{
                    // 如果進入螢幕,而且未在cell上,那麼動畫回currentCell
                    [self toCell];
                }
            }
        }
        
    }
}

// 滾動的時候小螢幕,放window上顯示
-(void)toSmallScreen{
    //放widow上
    [self.wmPlayer removeFromSuperview];
    __weak typeof(self)weakSelf = self;
    [UIView animateWithDuration:0.5f animations:^{
       weakSelf.wmPlayer.transform = CGAffineTransformIdentity;
        // 設定window上的位置
        weakSelf.wmPlayer.frame = CGRectMake(kScreenWidth/2,kScreenHeight-kTabBarHeight + 40 -(kScreenWidth/2)*0.75, kScreenWidth/2, (kScreenWidth/2)*0.75);
        weakSelf.wmPlayer.playerLayer.frame =  weakSelf.wmPlayer.bounds;
        // 下面就是更新佈局的程式碼,此處省略了,需要的去下載Demo看看

分析3:用MJRefresh做個JD的載入動畫(隨便做的,大家隨便感受下)


MKJRefreshHeader * header = [MKJRefreshHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)];
    header.stateLabel.hidden = YES;
    header.lastUpdatedTimeLabel.hidden = YES;
    header.mj_h = 80;
    self.tableView.mj_header = header;

    

這是JD的載入動畫View以及重寫的MJHeader檔案

這裡簡單的寫個重寫的方法示例,具體需要看的大家去下載Demo

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    // 根據狀態做事情
    // 重新整理完畢
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) {
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView1.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態
                if (self.state != MJRefreshStateIdle) return;
                
                self.loadingView1.alpha = 1.0;
                [self.loadingView1 endRefresing];
                self.arrowView.hidden = NO;
            }];
        } else { // 拉倒即將重新整理的時候,又往回縮,不進行重新整理
            [self.loadingView1 endRefresingDown];
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity;
            }];
        }
    } else if (state == MJRefreshStatePulling) { // 繼續往下拉的時候
        [self.loadingView1 refreing];
        NSLog(@"連線點");
        self.arrowView.hidden = NO;
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
        }];
    } else if (state == MJRefreshStateRefreshing) { // 重新整理
        self.loadingView1.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行
        [self.loadingView1 refreing];
        self.arrowView.hidden = YES;
    }
}

尼瑪啊,一口氣寫了那麼多,語文水平還沒及格的我真的感覺身體被掏空了


小白寫的東東,希望能幫到大家,大神的話可以給點意見,有問題留言哦