使用 AVPlayer 制作一個播放器

AVPlayer 是一個強大的視頻播放器,可以播放多種格式的視頻厨埋,缺點是沒有控制界面邪媳,需要自己去實現(xiàn)。

效果圖


AVPlayer
AVPlayer

項目地址

先看下它的結(jié)構(gòu)

overview
overview

首先初始化播放器揽咕,設(shè)置播放URL悲酷。

self.avPlayerView = [[XYAVPlayerView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, VIDEO_HEIGHT)];
self.avPlayerView.videoUrl = m3u;
[self.view addSubview:self.avPlayerView];

初始化方法套菜,添加一個視頻控制器亲善。


- (instancetype)initWithFrame:(CGRect)frame
{
    if ([super initWithFrame:frame]) {
        self.backgroundColor = [UIColor blackColor];
        [self addSubview:self.playControlView];
    }
    return self;
}

設(shè)置視頻URL


- (void)setVideoUrl:(NSString *)videoUrl
{
    _videoUrl = videoUrl.copy;
    [self createAVPlayer];

}

初始化AVPlayer,給_playControlView 引用AVPlayer逗柴,方便進行控制蛹头, [_playControlView addObserver];[_playControlView playerTimerAction];會在后面說明。


- (void)createAVPlayer
{
    
    NSURL *url = [NSURL URLWithString:self.videoUrl];
    if (!url) {
        return;
    }
  

    /**
     *  AVPlayer
     */
    _avPlayerItem = [AVPlayerItem playerItemWithURL:url];
    _avPlayer = [AVPlayer playerWithPlayerItem:_avPlayerItem];
    _avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:_avPlayer];
    _avPlayerLayer.frame = self.bounds;
    _avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspect;//
    
    
    [self.layer addSublayer:_avPlayerLayer];
    
    [_avPlayer play];
    
    self.autoresizesSubviews = YES; //子視圖Size自適應(yīng)
    
    _playControlView.avPlayer = self.avPlayer;
    _playControlView.avPlayerItem = self.avPlayerItem;
    _playControlView.avPlayerLayer = self.avPlayerLayer;
    
    [_playControlView addObserver];
    [_playControlView playerTimerAction];
    
    
    [self bringSubviewToFront:self.playControlView];
}

到這里只是創(chuàng)建了一個View,上面加載了一個AVPlayer渣蜗,一個視頻控制器視圖屠尊。

視頻控制器代碼:
我用的xib創(chuàng)建的View,所以初始化方法是awakeFromNib


- (void)awakeFromNib
{
    [super awakeFromNib];
    
    [self configureVolume];
    
    
    UITapGestureRecognizer * screenTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(screenTap)];
    [self addGestureRecognizer:screenTap];
    
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRecognizerAction:)];
    pan.delegate = self;
    
    [_adjustView addGestureRecognizer:pan];
    
    
    [self.slider setThumbImage:[UIImage imageNamed:@"verify_code_button"] forState:UIControlStateNormal];
    
    // slider開始滑動事件
    [_slider addTarget:self action:@selector(progressSliderTouchBegan:) forControlEvents:UIControlEventTouchDown];
    // slider滑動中事件
    [_slider addTarget:self action:@selector(progressSliderValueChanged:) forControlEvents:UIControlEventValueChanged];
    // slider結(jié)束滑動事件
    [_slider addTarget:self action:@selector(progressSliderTouchEnded:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchUpOutside];
    // slider 添加點擊手勢
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(sliderTap:)];
    [_slider addGestureRecognizer:tap];
    
    
    [self hiddenViews];

}

/**
 *  獲取系統(tǒng)音量
 */
- (void)configureVolume {
    
    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
    _volumeSlider = nil;
    for (UIView *view in [volumeView subviews]){
        if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
            _volumeSlider = (UISlider *)view;
            break;
        }
    }
    
    // 使用這個category的應(yīng)用不會隨著手機靜音鍵打開而靜音耕拷,可在手機靜音下播放聲音
    NSError *setCategoryError = nil;
    BOOL success = [[AVAudioSession sharedInstance]
                    setCategory: AVAudioSessionCategoryPlayback
                    error: &setCategoryError];
    
    if (!success) { /* handle the error in setCategoryError */ }
    
}

第一個手勢screenTap讼昆,是控制下方控制條的顯示與隱藏的

- (void)screenTap {
    self.controlStripView.hidden = !self.controlStripView.hidden;
}

第二個手勢則是用來控制快進、快退骚烧、音量浸赫、亮度調(diào)節(jié)的

- (void)panGestureRecognizerAction: (UIPanGestureRecognizer *)sender {
    
    //根據(jù)在view上Pan的位置,確定是調(diào)音量還是亮度
    CGPoint locationPoint = [sender locationInView:self];
    
    CGPoint veloctyPoint = [sender velocityInView:self];
    
    switch (sender.state) {
        case UIGestureRecognizerStateBegan:{
            // 使用絕對值來判斷移動的方向
            CGFloat x = fabs(veloctyPoint.x);
            CGFloat y = fabs(veloctyPoint.y);
            
            if (x > y) { //水平移動
                [self showViews];
                [self playerPause];
                self.pandirection = PanDirectionHorizontalMoved;
            }else { //垂直移動
                self.pandirection = PanDirectionVerticalMoved;
            }
            break;
        }
        case UIGestureRecognizerStateChanged:{
            switch (_pandirection) {
                case PanDirectionHorizontalMoved:{
                    [self horizontalMoved:veloctyPoint.x];
                    break;
                }
                case PanDirectionVerticalMoved:{
                    if (locationPoint.x > self.bounds.size.width / 2) {//音量調(diào)節(jié)-右側(cè)
                        [self verticalMovedForVolume:veloctyPoint.y];
                    }else {//亮度調(diào)節(jié)-左側(cè)
                        [self verticalMovedForBrightness:veloctyPoint.y];
                    }
                    break;
                }
            }
            break;
        }
        case UIGestureRecognizerStateEnded:{
            switch (_pandirection) {
                case PanDirectionHorizontalMoved:{
                    [self hiddenViews];
                    [self playerPlay];
                    break;
                }
                case PanDirectionVerticalMoved:{
                    break;
                }
            }
            break;
        }
        default:break;
    }
}

對于slider的方法赃绊,在開始手勢的時候暫停視頻播放既峡,顯示時間label


- (void)progressSliderTouchBegan: (UISlider *)sender {
    
    self.changeTimeLabel.hidden = NO;
    
    [self playerPause];
    
    
    NSLog(@" --- began touch");
}

在開始手勢的時候seek到slider對應(yīng)時間的時間點,然后開始視頻播放碧查,隱藏時間label


- (void)progressSliderTouchEnded: (UISlider *)sender {
    CMTime durationTime = self.avPlayerItem.duration;
    
    NSTimeInterval currentTime = CMTimeGetSeconds(durationTime) * sender.value;
    
    [self seekWithTime:currentTime];
    
    self.changeTimeLabel.hidden = YES;
    
    [self playerPlay];
    
    
    NSLog(@" ---- end touch");
}

slider滑動的時候运敢,只是改變label上顯示的時間


- (void)progressSliderValueChanged: (UISlider *)sender {
    
    CGFloat currentTime = sender.value * CMTimeGetSeconds(self.avPlayerItem.duration);
    
    NSString *tempCurrentTime = [self timeFormatterForServiceWithTimeStamp:currentTime];
    
    //當前播放時間
    self.currentTimeLabel.text = tempCurrentTime;
    
    //屏幕中間時間
    self.changeTimeLabel.text = tempCurrentTime;
    
    
    CGFloat sliderProgress = sender.value / sender.maximumValue;
    
    if (self.progressView.progress < sliderProgress) {
        self.progressView.progress = sender.value / sender.maximumValue;
    }
    
    
    
    NSLog(@" --- event touch  %f",sender.value);
    
    
}

slider的單擊方法則是直接seek到對應(yīng)時間點,AVPlayer會自動處理的忠售。


/**
 *  Slider Tap
 */
- (void)sliderTap: (UITapGestureRecognizer *)sender {
    
    if ([sender.view isKindOfClass:[UISlider class]]) {
        
        UISlider *slider = (UISlider *)sender.view;
        CGPoint point = [sender locationInView:slider];
        CGFloat length = slider.frame.size.width;
        
        CGFloat tempValue = point.x / length;
        
        NSTimeInterval currentTime = CMTimeGetSeconds(self.avPlayerItem.duration) * tempValue;
        
        CGFloat progress = currentTime/CMTimeGetSeconds(self.avPlayerItem.duration);
        
        if (progress > slider.value) {
            self.progressView.progress = progress;
        }
        
        
        [self seekWithTime:currentTime];
    }
    
}

前面用的 addObserver方法传惠,則是給播放器添加觀察者,用來檢測播放器狀態(tài)稻扬,還有APP的狀態(tài)涉枫。


- (void)addObserver
{
    //監(jiān)控狀態(tài)屬性,注意AVPlayer也有一個status屬性腐螟,通過監(jiān)控它的status也可以獲得播放狀態(tài)
    [self.avPlayerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //監(jiān)控網(wǎng)絡(luò)加載情況屬性
    [self.avPlayerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    
    /**
     *  進入后臺  暫停播放
     *
     */
    [kNotificationCenter addObserver:self selector:@selector(applicationDidEnterBackground_Notification) name:ApplicationDidEnterBackground_Notification object:nil];
    
    /**
     *  進入活躍狀態(tài)  繼續(xù)播放
     *
     */
    [kNotificationCenter addObserver:self selector:@selector(applicationDidBecomeActive_Notification) name:ApplicationDidBecomeActive_Notification object:nil];
    
    [kNotificationCenter addObserver:self selector:@selector(playerReset) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    
}



/** * 通過KVO監(jiān)控播放器狀態(tài) *
 * @param keyPath 監(jiān)控屬性
 * @param object 監(jiān)視器
 * @param change 狀態(tài)改變
 * @param context 上下文 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    AVPlayerItem *playerItem=object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
        if(status==AVPlayerStatusReadyToPlay){
            //總時長
            self.allTimeLabel.text = [self timeFormatterForServiceWithTimeStamp:CMTimeGetSeconds(playerItem.duration)];
            self.currentTimeLabel.text = @"00:00:00";
            
            NSLog(@"正在播放...愿汰,視頻總長度:%.2f",CMTimeGetSeconds(playerItem.duration));
        }
    }
    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;//緩沖總長度
        NSLog(@"共緩沖:%.2f",totalBuffer);
        double ableScale = totalBuffer / CMTimeGetSeconds(playerItem.duration);
        if (ableScale <= 1) {
            self.progressView.progress = ableScale;
        }
    }
}

playerTimerAction方法則是制定每秒進行一次返回,返回當前播放進度乐纸。


- (void)playerTimerAction
{
    __weak XYAVControlView * weakSelf = self;
    
    [self.avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        
        float current=CMTimeGetSeconds(time);
        if (current) {
            //當前播放進度
            weakSelf.currentTimeLabel.text = [weakSelf timeFormatterForServiceWithTimeStamp:CMTimeGetSeconds(weakSelf.avPlayerItem.currentTime)];
            //滑塊進度
            double totalTempTime = CMTimeGetSeconds(weakSelf.avPlayerItem.duration);
            double scale = CMTimeGetSeconds(weakSelf.avPlayerItem.currentTime) / totalTempTime;
            weakSelf.slider.value = scale;
        }
    }];
}

最后在 ViewController 里面監(jiān)控屏幕方向的變化衬廷,來處理全屏效果


[kNotificationCenter  addObserver:self
                             selector:@selector(onDeviceOrientationChange)
                                 name:UIDeviceOrientationDidChangeNotification
                               object:nil];






/**
 *  屏幕方向發(fā)生變化會調(diào)用這里
 */
- (void)onDeviceOrientationChange
{
    UIDeviceOrientation orientation             = [UIDevice currentDevice].orientation;
    UIInterfaceOrientation interfaceOrientation = (UIInterfaceOrientation)orientation;
    switch (interfaceOrientation) {
        case UIInterfaceOrientationPortraitUpsideDown:
        case UIInterfaceOrientationPortrait:{
            self.view.frame = (CGRect){0,0,ScreenWidth,ScreenHeight};
            self.avPlayerView.frame = CGRectMake(0, 0,ScreenWidth,VIDEO_HEIGHT);
            self.avPlayerView.playControlView.isFullScreen = NO;
            break;
        }
        case UIInterfaceOrientationLandscapeLeft:
        case UIInterfaceOrientationLandscapeRight:{
            self.view.frame = (CGRect){0,0,ScreenWidth,ScreenHeight};
            self.avPlayerView.frame = self.view.bounds;
            self.avPlayerView.playControlView.isFullScreen = YES;
            break;
        }
        default:
            break;
    }
}

到這里一個簡單的視頻播放器就做完了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末汽绢,一起剝皮案震驚了整個濱河市吗跋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宁昭,老刑警劉巖跌宛,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異积仗,居然都是意外死亡疆拘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門寂曹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哎迄,“玉大人回右,你說我怎么就攤上這事∈浚” “怎么了翔烁?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長旨涝。 經(jīng)常有香客問我蹬屹,道長,這世上最難降的妖魔是什么白华? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任哩治,我火速辦了婚禮,結(jié)果婚禮上衬鱼,老公的妹妹穿的比我還像新娘业筏。我一直安慰自己,他們只是感情好鸟赫,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布蒜胖。 她就那樣靜靜地躺著,像睡著了一般抛蚤。 火紅的嫁衣襯著肌膚如雪台谢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天岁经,我揣著相機與錄音朋沮,去河邊找鬼。 笑死缀壤,一個胖子當著我的面吹牛樊拓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播塘慕,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼筋夏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了图呢?” 一聲冷哼從身側(cè)響起条篷,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛤织,沒想到半個月后赴叹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡指蚜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年乞巧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姚炕。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡摊欠,死狀恐怖丢烘,靈堂內(nèi)的尸體忽然破棺而出柱宦,到底是詐尸還是另有隱情些椒,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布掸刊,位于F島的核電站免糕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏忧侧。R本人自食惡果不足惜石窑,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蚓炬。 院中可真熱鬧松逊,春花似錦、人聲如沸肯夏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驯击。三九已至烁兰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間徊都,已是汗流浹背沪斟。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留暇矫,地道東北人主之。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像李根,于是被迫代替她去往敵國和親杀餐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫朱巨、插件史翘、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,033評論 4 62
  • 原文鏈接http://www.cnblogs.com/kenshincui/p/4186022.html 音頻在i...
    Hyman0819閱讀 21,670評論 4 74
  • iOS-視頻播放器的簡單封裝 封裝視頻播放器问欠,首先需要了解視頻播放器的實現(xiàn)允蜈,iOS9之前可以使用MediaPlay...
    xx_cc閱讀 10,164評論 49 129
  • 千萬不要以為在與己無關(guān)涩维!只要你人還在一天,就不可能無一天與在無關(guān)。惟有當你真的消失於這個世界上了捺癞,在方才與...
    l左溪l閱讀 205評論 0 1
  • 我忘不了唐片,這是一種折磨串前。 今天宵溅,姥姥姥爺突然來了,已經(jīng)是下午五點肿轨,當時我剛起床寿冕,在給表弟畫手抄報。沒錯...
    攝心_閱讀 301評論 0 0