Медленная буферизация при потоковой передаче нескольких удаленных видео с помощью AVPlayer и AVMutableComposition

[Изменить: мне удалось найти обходной путь для этого, см. Ниже.]

Я пытаюсь транслировать несколько удаленных MP4-клипов с S3, воспроизводя их последовательно как одно непрерывное видео (чтобы включить очистку внутри и между клипами) без заикания, без явной предварительной загрузки их на устройство. Тем не менее, я считаю, что клипы буферизуются очень медленно (даже при быстром сетевом подключении) и не смогли найти адекватный способ решения этой проблемы.

Я пытался использовать AVPlayer для этого, так как AVPlayer с AVMutableComposition воспроизводит поставленные видеодорожки как одну непрерывную дорожку (в отличие от AVQueuePlayer, который я собираю, воспроизводит каждое видео отдельно и, следовательно, не поддерживает непрерывную очистку между клипами).

Когда я втыкаю один из активов прямо в AVPlayerItem и играть в это (без AVMutableComposition), он быстро буферизуется. Но используя AVMutableComposition видео начинает очень сильно заикаться на втором клипе (в моем тестовом примере 6 клипов, каждый около 6 секунд), в то время как звук продолжается. После однократного воспроизведения он воспроизводится идеально ровно, если перемотать в начало, поэтому я предполагаю, что проблема заключается в буферизации.

Моя текущая попытка решить эту проблему кажется запутанной, учитывая, что это кажется довольно простым вариантом использования AVPlayer - Я надеюсь, что есть все более простое решение, которое работает правильно. Почему-то я сомневаюсь, что буферный плеер, который я использую ниже, действительно необходим, но у меня заканчиваются идеи.

Вот основной код, который устанавливает AVMutableComposition:

// Build an AVAsset for each of the source URIs
- (void)prepareAssetsForSources:(NSArray *)sources
{
  NSMutableArray *assets = [[NSMutableArray alloc] init]; // the assets to be used in the AVMutableComposition
  NSMutableArray *offsets = [[NSMutableArray alloc] init]; // for tracking buffering progress
  CMTime currentOffset = kCMTimeZero;
  for (NSDictionary* source in sources) {
    bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]];
    bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]];
    NSString *uri = [source objectForKey:@"uri"];
    NSString *type = [source objectForKey:@"type"];

    NSURL *url = isNetwork ?
      [NSURL URLWithString:uri] :
      [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];

    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
    currentOffset = CMTimeAdd(currentOffset, asset.duration);

    [assets addObject:asset];
    [offsets addObject:[NSNumber numberWithFloat:CMTimeGetSeconds(currentOffset)]];
  }
  _clipAssets = assets;
  _clipEndOffsets = offsets;
}

// Called with _clipAssets
- (AVPlayerItem*)playerItemForAssets:(NSMutableArray *)assets
{
  AVMutableComposition* composition = [AVMutableComposition composition];
  for (AVAsset* asset in assets) {
    CMTimeRange editRange = CMTimeRangeMake(CMTimeMake(0, 600), asset.duration);
    NSError *editError;

    [composition insertTimeRange:editRange
                         ofAsset:asset
                          atTime:composition.duration
                           error:&editError];
  }
  AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:composition];
  return playerItem; // this is used to initialize the main player
}

Моя первоначальная мысль была такова: поскольку он быстро буферизуется AVPlayerItem почему бы не поддерживать отдельный буферный плеер, который загружается с каждым активом по очереди (без AVMutableComposition) буферизовать активы для основного игрока?

- (void)startBufferingClips
{
  _bufferingPlayerItem = [AVPlayerItem playerItemWithAsset:_clipAssets[0] 
                              automaticallyLoadedAssetKeys:@[@"tracks"]];
  _bufferingPlayer = [AVPlayer playerWithPlayerItem:_bufferingPlayerItem];
  _currentlyBufferingIndex = 0;
}

// called every 250 msecs via an addPeriodicTimeObserverForInterval on the main player
- (void)updateBufferingProgress
{
  // If the playable (loaded) range is within 100 milliseconds of the clip
  // currently being buffered, load the next clip into the buffering player.
  float playableDuration = [[self calculateBufferedDuration] floatValue];
  CMTime totalDurationTime = [self playerItemDuration :_bufferingPlayer];
  Float64 totalDurationSeconds = CMTimeGetSeconds(totalDurationTime);
  bool bufferingComplete = totalDurationSeconds - playableDuration < 0.1;
  float bufferedSeconds = [self bufferedSeconds :playableDuration];

  float playerTimeSeconds = CMTimeGetSeconds([_player currentTime]);
  __block NSUInteger playingClipIndex = 0;

  // find the index of _player's currently playing clip
  [_clipEndOffsets enumerateObjectsUsingBlock:^(id offset, NSUInteger idx, BOOL *stop) {
    if (playerTimeSeconds < [offset floatValue]) {
      playingClipIndex = idx;
      *stop = YES;
    }
  }];

  // TODO: if  bufferedSeconds - playerTimeSeconds <= 0, pause the main player

  if (bufferingComplete && _currentlyBufferingIndex < [_clipAssets count] - 1) {
    // We're done buffering this clip, load the buffering player with the next asset
    _currentlyBufferingIndex += 1;
    _bufferingPlayerItem = [AVPlayerItem playerItemWithAsset:_clipAssets[_currentlyBufferingIndex]
                                automaticallyLoadedAssetKeys:@[@"tracks"]];
    _bufferingPlayer = [AVPlayer playerWithPlayerItem:_bufferingPlayerItem];
  }
}

- (float)bufferedSeconds:(float)playableDuration {
  __block float seconds = 0.0; // total duration of clips already buffered
  if (_currentlyBufferingIndex > 0) {
    [_clipEndOffsets enumerateObjectsUsingBlock:^(id offset, NSUInteger idx, BOOL *stop) {
      if (idx + 1 >= _currentlyBufferingIndex) {
        seconds = [offset floatValue];
        *stop = YES;
      }
    }];
  }
  return seconds + playableDuration;
}

- (NSNumber *)calculateBufferedDuration {
  AVPlayerItem *video = _bufferingPlayer.currentItem;
  if (video.status == AVPlayerItemStatusReadyToPlay) {
    __block float longestPlayableRangeSeconds;
    [video.loadedTimeRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
      CMTimeRange timeRange = [obj CMTimeRangeValue];
      float seconds = CMTimeGetSeconds(CMTimeRangeGetEnd(timeRange));
      if (seconds > 0.1) {
        if (!longestPlayableRangeSeconds) {
          longestPlayableRangeSeconds = seconds;
        } else if (seconds > longestPlayableRangeSeconds) {
          longestPlayableRangeSeconds = seconds;
        }
      }
    }];
    Float64 playableDuration = longestPlayableRangeSeconds;
    if (playableDuration && playableDuration > 0) {
      return [NSNumber numberWithFloat:longestPlayableRangeSeconds];
    }
  }
  return [NSNumber numberWithInteger:0];
}

Изначально казалось, что это работает как шарм, но затем я переключился на другой набор тестовых клипов, и затем буферизация снова была очень медленной (проигрыватель буферизации помог, но не достаточно). Кажется, что loadedTimeRange s для активов, загруженных в буферный проигрыватель, не соответствует loadedTimeRange для тех же активов внутри AVMutableComposition: Даже после loadedTimeRange s для каждого элемента, загруженного в буферный проигрыватель, указывает на то, что весь ресурс был буферизован, видео основного игрока продолжало заикаться (в то время как аудио воспроизводилось до конца). Опять же, воспроизведение было плавным после перемотки, когда основной проигрыватель прошел все клипы один раз.

Я надеюсь, что ответ на этот вопрос, каким бы он ни был, окажется полезным в качестве отправной точки для других разработчиков iOS, пытающихся реализовать этот базовый вариант использования. Спасибо!

Изменить: так как я опубликовал этот вопрос, я сделал следующий обходной путь для этого. Надеюсь, это спасет любого, кто столкнется с этой головной болью.

Я закончил тем, что поддерживал двух буферных игроков (оба AVPlayer s) которые начали буферизовать первые два клипа, переходя к небуферизованному клипу с самым низким индексом после их loadedTimeRanges указал, что буферизация для их текущего клипа была выполнена. Я сделал логику приостановить / отменить воспроизведение на основе буферизованных клипов, и loadedTimeRanges из буферных игроков, плюс небольшой запас. Это требовало нескольких переменных бухгалтерского учета, но не было слишком сложным.

Вот как инициализировались буферизирующие плееры (здесь я опускаю логику учета):

- (void)startBufferingClips
{
  _bufferingPlayerItemA = [AVPlayerItem playerItemWithAsset:_clipAssets[0] 
                               automaticallyLoadedAssetKeys:@[@"tracks"]];
  _bufferingPlayerA = [AVPlayer playerWithPlayerItem:_bufferingPlayerItemA];
  _currentlyBufferingIndexA = [NSNumber numberWithInt:0];
  if ([_clipAssets count] > 1) {
    _bufferingPlayerItemB = [AVPlayerItem playerItemWithAsset:_clipAssets[1] 
                                 automaticallyLoadedAssetKeys:@[@"tracks"]];
    _bufferingPlayerB = [AVPlayer playerWithPlayerItem:_bufferingPlayerItemB];
    _currentlyBufferingIndexB = [NSNumber numberWithInt:1];
    _nextIndexToBuffer = [NSNumber numberWithInt:2];
  } else {
    _nextIndexToBuffer = [NSNumber numberWithInt:1];
  }
}

Кроме того, мне нужно было убедиться, что видео и аудио треки не были объединены, так как они были добавлены в AVMutableComposition поскольку это явно мешало буферизации (возможно, они не регистрировали те же видео / аудио дорожки, что и те, что загружали буферирующие плееры, и, таким образом, не получали новые данные). Вот код, где AVMutableComposition построен из массива NSAsset s:

- (AVPlayerItem*)playerItemForAssets:(NSMutableArray *)assets
{
  AVMutableComposition* composition = [AVMutableComposition composition];
  AVMutableCompositionTrack *compVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo
                                                                   preferredTrackID:kCMPersistentTrackID_Invalid];
  AVMutableCompositionTrack *compAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio
                                                                   preferredTrackID:kCMPersistentTrackID_Invalid];
  CMTime timeOffset = kCMTimeZero;
  for (AVAsset* asset in assets) {
    CMTimeRange editRange = CMTimeRangeMake(CMTimeMake(0, 600), asset.duration);
    NSError *editError;

    NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
    NSArray *audioTracks = [asset tracksWithMediaType:AVMediaTypeAudio];

    if ([videoTracks count] > 0) {
    AVAssetTrack *videoTrack = [videoTracks objectAtIndex:0];
      [compVideoTrack insertTimeRange:editRange
                              ofTrack:videoTrack
                               atTime:timeOffset
                                error:&editError];
    }

    if ([audioTracks count] > 0) {
      AVAssetTrack *audioTrack = [audioTracks objectAtIndex:0];
      [compAudioTrack insertTimeRange:editRange
                              ofTrack:audioTrack
                               atTime:timeOffset
                                error:&editError];
    }

    if ([videoTracks count] > 0 || [audioTracks count] > 0) {
      timeOffset = CMTimeAdd(timeOffset, asset.duration);
    }
  }
  AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:composition];
  return playerItem;
}

При таком подходе буферизация при использовании AVMutableComposition для основного плеера работает красиво и быстро, по крайней мере, в моей настройке.

0 ответов

Другие вопросы по тегам