在使用懒猪计划的时候,发现了开启白噪声循环播放的时候会有明显的空白期,于是就查看了音频播放的问题,顺便也对中间引用的HDCommonTools这个库完善下。

在iOS音频播放中,提供了多种接口,最常用的就是AVAudioPlayerAVPlayer

相比较而言,AVAudioPlayer 使用简单,接口多,只能播放本地音频,不支持流媒体播放,每一个audioplayer对象就是一段音频,而AVPlayer接口较少,但是可以使用AVPlayer播放本地音频和支持流媒体播放,所以两者比较中,在HDCommonTools中播放音频的时候,选用了AVPlayer。

先说说循环播放的几个网络方案

1、AVAudioPlayer的默认接口

在如果使用AVAudioPlayer 的话,已经提供了循环播放的接口,例如

audioPlayer.numberOfLoops = 1;//设置音乐播放次数  -1为一直循环
[audioPlayer prepareToPlay];
[audioPlayer play]; //播放

2、AVPlayer的完成通知

这个方案就是通过AVPlayerItemDidPlayToEndTimeNotification这个字段,获取到AVPlayer的播放完成通知,然后在完成通知中,去将AVPlayer的进度到kCMTimeZero最开始

//给AVPlayerItem添加播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:avPlayer.currentItem];

/**
 *  播放完成通知
 *
 *  @param notification 通知对象
 */
- (void)playbackFinished:(NSNotification *)notification {
    NSLog(@"视频播放完成.");
    // 播放完成后重复播放
    // 跳到最新的时间点开始播放
    [avPlayer seekToTime: kCMTimeZero];
    [avPlayer play];
}

这两个方案都是将播放器的播放进度在结束的时候重置,然后再继续播放,但是这两种方案在循环的时候,都会有明显的大概1s的空白期。所以这两个虽然也可以实现循环播放的目的,但是效果并不理想。

3、通过属性观察

参考两个前者方案,空白期的产生猜测是因为结束到重新加载需要时间,所以可以考虑在即将播放完成的时候提前加载新的播放器,那么就需要知道什么时候将要结束。

最开始考虑检测播放item的时长和player的状态,在快播放完成的时候去做一个循环的方案。

[item addObserver:self forKeyPath:@"duration" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    AVPlayerItem *item = object;
    CGFloat duration =  item.duration.value / item.duration.timescale;
    [NSTimer scheduledTimerWithTimeInterval:duration - 1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        //重新去初始化播放
    }];
}

但是这样怎么去重新播放呢,如果依旧是调用seekToTime的话,还是会发现有空白情况,那么最好的方案就是重新创建另外一个avPlayer,然后两个去循环播放,但是这样的haul,item就需要重新创建一个新的,然后去添加观察,这个思路对,但是需要重新改良。

所以可以使用下面这个通过间隔不断的去检测播放的时长:

[avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {}];

解决方案

因为加载中会有空白期,所以这里在刚开始加载的时候创建两个播放器,当一个播放器播放即将完成时,去播放另外那个播放器,同时将现在这个播放器停止播放,进度重置。

//第一个
[avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    CGFloat duration =  avPlayer.currentItem.duration.value / avPlayer.currentItem.duration.timescale; //视频总时间
    CGFloat currentTime =avPlayer.currentItem.currentTime.value / avPlayer.currentItem.currentTime.timescale;//视频当前运行时间
    NSLog(@"第一个准备好播放了,总时间:%f,%f",duration,currentTime);
       if (currentTime >= duration - 2.0) {
           if (avRepeatPlayer) {
                [avRepeatPlayer play];
            }
       }
       if (currentTime >= duration - 1.0) {
         [item seekToTime:kCMTimeZero completionHandler:^(BOOL finished) {
            if (finished) {
               [avPlayer seekToTime:kCMTimeZero];
               [avPlayer pause];
             }
          }];
          }
}];

//新建一个重复的去提前播放
AVPlayerItem *item2 = [AVPlayerItem playerItemWithURL:musicUrl];
avRepeatPlayer = [[AVPlayer alloc] initWithPlayerItem:item2];
[avRepeatPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
     CGFloat duration =  avRepeatPlayer.currentItem.duration.value / avRepeatPlayer.currentItem.duration.timescale; //视频总时间
      CGFloat currentTime =avRepeatPlayer.currentItem.currentTime.value / avRepeatPlayer.currentItem.currentTime.timescale;//视频当前运行时间
      NSLog(@"第二个准备好播放了,总时间:%f,%f",duration,currentTime);
      if (currentTime >= duration - 2.0) {
         if (avPlayer) {
           [avPlayer play];
         }
      }
      if (currentTime >= duration - 1.0) {
         [item2 seekToTime:kCMTimeZero completionHandler:^(BOOL finished) {
          if (finished) {
              [avRepeatPlayer seekToTime:kCMTimeZero];
               [avRepeatPlayer pause];
           }
         }];
                
      }
}];

该功能在HDCommonTools1.3.2中已经实现,如果还是不会使用,可以现在最新代码测试下。

参考文章


☟☟可点击下方广告支持一下☟☟

最后修改:2018 年 04 月 16 日
请我喝杯可乐,请随意打赏: ☞已打赏列表