Медленная буферизация при потоковой передаче нескольких удаленных видео с помощью 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
для основного плеера работает красиво и быстро, по крайней мере, в моей настройке.