你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> XLVideoPlayer——基於AVFoundation自定義的視頻播放器

XLVideoPlayer——基於AVFoundation自定義的視頻播放器

編輯:IOS開發基礎

作者:shelin(簡書)

本文聊點關於最近寫的這個自定義播放器。支持UITableViewCell上小屏、全屏播放,手動及屏幕旋轉切換,包括右下角的小窗懸停播放,不依賴於視圖控制器和第三方,盡量的讓使用起來更簡單,具體代碼詳情請戳Github,先看看效果如何!

1121012-97f1d83cb7470a06.gif

1121012-1832f4a772839eb0.gif

這是基於AVFoundation下自定義的一個播放器,先簡單介紹幾個用到的類。

介紹:

  • AVPlayer:可以理解為播放器對象,靈活性好,可以高度化的自定義UI,但它本身不能顯示視頻,顯示需要另一個類AVPlayerLayer來顯示,繼承於CALayer,下面是摘自官方的一段介紹:

AVPlayer works equally well with local and remote media files.

You can display the visual content of items played by an instance of AVPlayer in a CoreAnimation layer of class AVPlayerLayer.

You can observe the status of a player using key-value observing.

主要是說它支持本地/網絡媒體播放,需要CoreAnimation下的AVPlayerLayer來顯示視頻,我們可以通過KVO監聽player的播放狀態。

  • AVPlayerItem:存有相關媒體信息的類,一個視頻資源對應一個AVPlayerItem對象,當你需要循環播放多個視頻資源時也需創建多個AVPlayerItem對象。建議大家可以多看看官方的英文文檔解釋(題外話)。

An AVPlayerItem represents the presentation state of an asset that’s played by an AVPlayer object, and lets you observe that state.

  • AVAsset:主要用於獲取多媒體信息,可以理解為一個抽象類,不能直接使用,操作針對它的子類AVURLAsset,根據你視頻的url創建一個包含視頻媒體信息的AVURLAsset對象。

  • CMTime:還會用到這個媒體時間相關的類,如有不明白可以看之前一個帖子的解釋。

層級關系:

基於以上幾個類就能實現視頻的基本功能了,例如暫停、播放,快進、後退、顯示播放/緩沖進度。然後UI層面,層級很簡單,XLVideoPlayer繼承於UIView,上面我們說到顯示視頻需要AVPlayerLayer,我們將AVPlayerLayer加到view的layer上。

1121012-f86b3e428e604109.jpg

下面貼出主要的代碼,初始化AVPlayer對象

- (AVPlayerLayer *)playerLayer {
    if (!_playerLayer) {
        _playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
        _playerLayer.backgroundColor = kPlayerBackgroundColor;
        _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;//視頻填充模式
    }
    return _playerLayer;
}

- (AVPlayer *)player{
    if (!_player) {
        AVPlayerItem *playerItem = [self getAVPlayItem];
        self.playerItem = playerItem;
        _player = [AVPlayer playerWithPlayerItem:playerItem];

        [self addProgressObserver];

        [self addObserverToPlayerItem:playerItem];
    }
    return _player;
}

//initialize AVPlayerItem
- (AVPlayerItem *)getAVPlayItem{

    NSAssert(self.videoUrl != nil, @"必須先傳入視頻url!!!");

    if ([self.videoUrl rangeOfString:@"http"].location != NSNotFound) {
        AVPlayerItem *playerItem=[AVPlayerItem playerItemWithURL:[NSURL URLWithString:[self.videoUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
        return playerItem;
    }else{
        AVAsset *movieAsset  = [[AVURLAsset alloc]initWithURL:[NSURL fileURLWithPath:self.videoUrl] options:nil];
        AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:movieAsset];
        return playerItem;
    }
}

同時我們注冊KVO,監控視頻播放過程,這可以獲取視頻的播放進度。AVPlayer有一個屬性currentItem是AVPlayerItem類型,表示當前播放的視頻對象。

#pragma mark - monitor video playing course

-(void)addProgressObserver{

    //get current playerItem object
    AVPlayerItem *playerItem = self.player.currentItem;
    __weak typeof(self) weakSelf = self;

    //Set once per second
    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {

        float current = CMTimeGetSeconds(time);
        float total = CMTimeGetSeconds([playerItem duration]);
        weakSelf.progressLabel.text = [weakSelf timeFormatted:current];
        if (current) {
//            NSLog(@"%f", current / total);
            weakSelf.slider.value = current / total;

            if (weakSelf.slider.value == 1) {      //complete block
                if (weakSelf.completedPlayingBlock) {
                    weakSelf.completedPlayingBlock(weakSelf);
                }else {       //finish and loop playback
                    weakSelf.playOrPauseBtn.selected = NO;
                    [weakSelf showOrHidenBar];
                    CMTime currentCMTime = CMTimeMake(0, 1);
                    [weakSelf.player seekToTime:currentCMTime completionHandler:^(BOOL finished) {
                        weakSelf.slider.value = 0.0f;
                    }];
                }
            }
        }
    }];
}

以及監聽AVPlayerItem對象的status/loadedTimeRanges屬性變化,status對應播放狀態,loadedTimeRanges網絡緩沖狀態,當loadedTimeRanges的改變時,每緩沖一部分數據就會更新此屬性,可以獲得本次緩沖加載的視頻范圍(包含起始時間、本次網絡加載時長)

#pragma mark - PlayerItem (status,loadedTimeRanges)
-(void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{
    //監控狀態屬性,注意AVPlayer也有一個status屬性,通過監控它的status也可以獲得播放狀態
    [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //network loading progress
    [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}

在這獲取視頻的總時長,網絡的視頻緩沖進度,做相應的顯示。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    AVPlayerItem *playerItem = object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
        if(status == AVPlayerStatusReadyToPlay){
            self.totalDuration = CMTimeGetSeconds(playerItem.duration);
            self.totalDurationLabel.text = [self timeFormatted:self.totalDuration];
        }
    }else if([keyPath isEqualToString:@"loadedTimeRanges"]){
        NSArray *array = playerItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次緩沖時間范圍
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//緩沖總長度
        self.slider.middleValue = totalBuffer / CMTimeGetSeconds(playerItem.duration);
//        NSLog(@"totalBuffer:%.2f",totalBuffer);
        //remove loading animation
        if (self.slider.middleValue <= self.slider.value) {
            self.activityIndicatorView.center = self.center;
            [self addSubview:self.activityIndicatorView];
            [self.activityIndicatorView startAnimating];
        }else {
            [self.activityIndicatorView removeFromSuperview];
        }
    }
}

下面這部分是定位視頻的某個位置播放,也就是快進後退。

這裡需要注意的是在用戶拖拽slider的過程中需要先暫停,否則手動改變進度和播放的進度會有沖突,用戶拖拽完畢再去播放視頻。

- (void)finishChange {
    _inOperation = NO;
    [self hiden];
    CMTime currentCMTime = CMTimeMake(self.slider.value * self.totalDuration, 1);
    [self.player seekToTime:currentCMTime completionHandler:^(BOOL finished) {
        [self.player play];
        self.playOrPauseBtn.selected = YES;
    }];
}

關於屏幕旋轉

這部分還是遇到一些坑,可以看到並沒有在plist文件設置工程支持橫屏,所有都是通過強制旋轉屏幕實現,在用戶旋轉屏幕的通知或者點擊事件中調用強制旋轉的代碼。會發現當你旋轉屏幕時,其實UITableView和其他控件是不會隨屏幕一起旋轉的,強制旋轉涉及到iOS8+和之前的系統的問題,當我們調用之前的時,在iOS7和iOS8+的效果是不一樣的,我從網上摘了來兩個圖。

[[UIApplication sharedApplication] setStatusOrientation:XX]

第一張圖iOS 7的,第二張圖是iOS 8+,很明顯我們發現iOS7當你調用這個方法UIscreen和UIWindow一起轉過來了,而iOS8後UIScreen並沒有轉過來,這樣就會導致調用這個方法在iOS8+會存在部分區域點擊無響應,因為它超出UIScreen的那部分范圍,而且我在測試過程中還發現用這種方法旋轉在點擊Home鍵再次進入程序會導致屏幕錯位。

1121012-b6e641de187b2482.jpg

1121012-293506d254fec468.jpg

怎麼辦呢!後面又找到這個方法:

[[UIDevice currentDevice]setOrientation:UIInterfaceOrientationPortrait];

但是現在蘋果已經將該方法私有化了,直接pass掉。之後在stackoverflow做了些嘗試,找到現在用的這個方法,它並沒有把系統的status bar旋轉過來。

NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight];
[[UIDevice currentDevice] setValue:value forKey:@"orientation"];

之後還查了一些相關的東西,有興趣大家可以看看:

詳解UICoordinateSpace和UIScreen在iOS 8上的坐標問題

屏幕旋轉學習筆記

寫一個播放器還需要注意很多細節,只能根據需求一步步的完善,這裡只能說一些需要關注的點。如果大家覺得不錯希望可以在點擊右上角Star,謝謝支持!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved