iOS AVPlayer 的使用实践

EulahRoybal 7年前
   <p>前两天在网上看到一篇博客,介绍AVPlayer的使用,但是只简单介绍了一下单个的本地文件如何播放,心血来潮,就想着做一个类似于播放器的东西,能够实现播放网络歌曲,循环播放多首音乐,下面我们来实现一下</p>    <p>首先明确一下,在本文中需要讲到的几点:</p>    <ul>     <li>实现网络歌曲的播放</li>     <li>实现在后台也能播放歌曲</li>     <li>实现多首歌曲的循环播放</li>     <li>需要有播放/暂停和下一首的功能</li>     <li>需要在播放期间能够得知该首歌曲的总时长和当前播放时长</li>    </ul>    <p>本文中就暂时将这名多,后面还会丰富,例如实现缓存下载,实现歌曲缓存的进度查看,实现能够使用耳机按钮控制播放等等。</p>    <h3>播放网络歌曲</h3>    <p>因为需要播放网络歌曲,我就往七牛云上传了几首歌,就不用再自己到处去找歌曲了</p>    <p>首先,明确我们播放歌曲使用的是AVPlayer,至于为什么使用它不使用其他的,因为它好用啊,苹果封装了强大的功能,让我们使用,干嘛不用!其实还有其他原因,这个就等着你自己去搜索了。</p>    <p>AVQueuePlayer</p>    <p>AVQueuePlayer是AVPlayer的一个子类,他可以实现多首歌曲播放,所以我们直接使用它了</p>    <pre>  <code class="language-objectivec">//传入多个AVPlayerItem来初始化AVQueuePlayer  + (instancetype)queuePlayerWithItems:(NSArray<AVPlayerItem *> *)items;  </code></pre>    <p>AVPlayerItem</p>    <p>AVPlayerItem是一个资源对象,我们加载歌曲的时候都是使用它,它提供了两种初始化方法</p>    <pre>  <code class="language-objectivec">//初始化网络资源  + (instancetype)playerItemWithURL:(NSURL *)URL;    //初始化本地资源,本地的音乐或者影片资源都是通过AVAsset拿出来  + (instancetype)playerItemWithAsset:(AVAsset *)asset;  </code></pre>    <p>先来试一下:</p>    <pre>  <code class="language-objectivec">//初始化AVPlayerItem  NSMutableArray *items = [NSMutableArray array];  NSArray *urls = @[MUSIC_URL1,MUSIC_URL2,MUSIC_URL3,MUSIC_URL4,MUSIC_URL5];  for (NSString *url in urls) {          AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:url]];          [items addObject:item];      }  //初始化AVQueuePlayer  AVQueuePlayer *player = [AVQueuePlayer queuePlayerWithItems: items];  //测试播放  if(player.status == AVPlayerStatusReadyToPlay){   [player play];  }  </code></pre>    <p>上面的代码看起来没有错,但是我在做的时候,却遇到一个问题,第一次点击的时候,并不会播放,第二次第三次就会开始播放了。</p>    <p>其实这里是有一个缓冲的原因,因为是网络资源,涉及到一个缓冲,后面我们会对这个做处理,歌曲确实是能够播放的</p>    <p>就这样,简单实现了多首歌曲的播放,但是我们还需要实现循环播放,这个就相对麻烦一点了。</p>    <p>要实现循环播放,我们就需要知道AVQueuePlayer的播放机制,对于AVQueuePlayer播放,是有一个队列,每次播放完成一首歌曲过后,这首歌曲就会从队列中删除,即这个item会从队列中删除,并且如果我们想直接再将这个item再次加入队列,是不能够加入的,我们必须要在new 一个item,再次加载到这个队列当中,才能够实现再次播放。这个也是挺蛋疼的。</p>    <p>知道了这个,我们就有想法了,我们能够在player最后一首歌曲即将播放完成后,再来新建一个队列啊。思路是正确的,但是我们不能够直接得到player正在播放最后一首歌曲,这时候我想到的是一个timer检测,通过timer去检测player的播放队列是否还剩下一首歌曲,如果是的话,我们就新建队列,加入到player的播放序列中</p>    <p>首先,我们在开始播放歌曲的时候,就需要将timer启动,监测player</p>    <pre>  <code class="language-objectivec">self.checkMusicTimer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(checkMusic) userInfo:nil repeats:YES];  </code></pre>    <p>在checkMusic我们判断当前是否队列中只有一首歌曲</p>    <pre>  <code class="language-objectivec">- (void)checkMusic  {        if (self.player.items.count == 1){          [self prepareItems];//这个方法即是再次创建队列,加入到player播放序列中          [self play];      }  }  </code></pre>    <pre>  <code class="language-objectivec">// 准备歌曲  // 因为需要歌曲循环播放,每次AVQueuePlayer播放完成一首歌曲,就会将其从队列中移除  // 所以我们需要在歌曲最后一首播放完之前重新为AVQueuePlayer创建一个播放队列,这样就能够实现循环播放  //  //  - (void)prepareItems{      NSMutableArray *items = [NSMutableArray array];      NSArray *urls = @[MUSIC_URL1,MUSIC_URL2,MUSIC_URL3,MUSIC_URL4,MUSIC_URL5];      for (NSString *url in urls) {          AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:url]];          [items addObject:item];  //这里是添加每首歌曲的监测,我们后面会讲到          [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];          [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:item];      }      self.playerItems = items;      for (AVPlayerItem *item in items) {          if ([self.player canInsertItem:item afterItem:self.player.items.lastObject]) {              [self.player insertItem:item afterItem:self.player.items.lastObject];          }      }  }  </code></pre>    <p>这样一来,我们就能够实现循环播放了,这里的代码和后面要讲到的有关联,所以这里看不清晰也没关系,接着往后看</p>    <p>上面我们讲了,有个缓冲的原因,导致首次点击播放的时候,不能够成功播放,在AVPlayerItem中有一个属性loadedTimeRanges,表示的是缓存状态,我们可以对他进行观察,来进行播放</p>    <pre>  <code class="language-objectivec">//上面的代码已经写出了对缓冲的检测  [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];  </code></pre>    <p>然后我们在观察者中</p>    <pre>  <code class="language-objectivec">-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{      if ([keyPath isEqualToString:@"loadedTimeRanges"]) {          NSLog(@"缓冲");          [self play];      }  }  </code></pre>    <p>我们在每个item中加入了观察者,在什么时候移除呢,当然是在每首歌曲播放完成后移除,如果不移除将会崩溃</p>    <p>再次对每个item进行观测,播放结束时</p>    <pre>  <code class="language-objectivec">[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:item];  </code></pre>    <p>在播放结束,移除观察者</p>    <pre>  <code class="language-objectivec">- (void)playbackFinished:(NSNotification *)notice {      NSLog(@"播放完成");      [self.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];  }  </code></pre>    <h3>实现后台播放</h3>    <p>要实现后台播放,很简单只需要加入几行代码</p>    <pre>  <code class="language-objectivec">//设置可后台播放  NSError *error = nil;  [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];  [[AVAudioSession sharedInstance] setActive:YES error:nil];  </code></pre>    <p>然后我们还需要在项目里设置</p>    <p><img src="https://simg.open-open.com/show/0e3f33aa9c60fb36b6667b2e3ae7199b.jpg"></p>    <h3>播放暂停</h3>    <p>这个就很简单了</p>    <p>直接调方法就行</p>    <p>上一首下一首也是直接调用方法就行</p>    <pre>  <code class="language-objectivec">/*!   @method  play   @abstract  Signals the desire to begin playback at the current item's natural rate.   @discussion Equivalent to setting the value of rate to 1.0.   */     - (void)play;  - /*!   @method  pause   @abstract  Pauses playback.   @discussion Equivalent to setting the value of rate to 0.0.   */  - (void)pause;    /*!      @method     advanceToNextItem      @abstract   Ends playback of the current item and initiates playback of the next item in the player's queue.      @discussion Removes the current item from the play queue.  */  - (void)advanceToNextItem;  </code></pre>    <h3>时长计算</h3>    <p>为player加一个观察者就行</p>    <pre>  <code class="language-objectivec">-(void)playerDidPlay{      // //添加播放进度观察者          __weak typeof(self) weakSelf = self;          self.timeObserver = [self.manager.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0,1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {              float current = CMTimeGetSeconds(time);              float total = CMTimeGetSeconds(weakSelf.manager.currentItem.duration);              weakSelf.total = [NSString stringWithFormat:@"%.2f",total];              weakSelf.current = [NSString stringWithFormat:@"%.f",current];              weakSelf.label.text = [NSString stringWithFormat:@"%@/%@",weakSelf.current,weakSelf.total];          }];      _isPlaying = YES;      [self.play setTitle:@"暂停" forState:UIControlStateNormal];  }  </code></pre>    <p>其中的CMTime指的是帧数</p>