2015-05-21 6 views
43

Я работаю над приложением, где есть просмотр коллекции, а ячейки представления коллекции могут содержать видео. Прямо сейчас я показываю видео, используя AVPlayer и AVPlayerLayer. К сожалению, производительность прокрутки ужасная. Похоже, AVPlayer, AVPlayerItem и AVPlayerLayer делают большую часть своей работы над основной нитью. Они постоянно вынимают замки, ждут семафоров и т. Д., Которые блокируют основную нить и вызывают сильные капли кадров.Поддержание хорошей производительности прокрутки при использовании AVPlayer

Есть ли способ сообщить AVPlayer, чтобы прекратить делать так много вещей в основной теме? До сих пор ничто из того, что я пробовал, не решило проблему.

Я также попытался создать простой видеоплеер, используя AVSampleBufferDisplayLayer. Используя это, я могу убедиться, что все происходит с основного потока, и я могу достичь ~ 60 кадров в секунду при прокрутке и воспроизведении видео. К сожалению, этот метод намного ниже, и он не обеспечивает такие функции, как воспроизведение звука и очистка времени из коробки. Есть ли способ получить аналогичную производительность с AVPlayer? Я бы скорее использовал это.

Edit: Посмотрев на это больше, это не выглядит как это можно добиться хорошей производительности прокрутки при использовании AVPlayer. Создание AVPlayer и сопоставление с помощью экземпляра AVPlayerItem запускает кучу работы, которая батутно по основному потоку, где он затем ждет семафоров и пытается получить кучу замков. Количество времени, которое этот киоск на основной поток увеличивается довольно резко, поскольку количество видеороликов в scrollview увеличивается.

AVPlayer dealloc также представляется огромной проблемой. Dealloc'ing AVPlayer также пытается синхронизировать кучу вещей. Опять же, это становится очень плохо, поскольку вы создаете больше игроков.

Это довольно удручающее, и это делает AVPlayer почти непригодным для использования в том, что я пытаюсь сделать. Блокировка основной нити, подобной этой, является такой любительской штукой, что трудно поверить, что инженеры Apple допустили такую ​​ошибку. В любом случае, надеюсь, они могут исправить это в ближайшее время.

+3

«это не выглядит Как я t можно добиться хорошей производительности прокрутки при использовании AVPlayer « Это просто не так. Существует множество приложений, которые ретранслируют AVFoundation для воспроизведения медиафайлов в прокручиваемых фидах. Vine/Facebook/Instagram используют AVPlayer для воспроизведения медиафайлов в каналах. Это очень сложно, но то, что дамиан, о котором идет речь ниже, является хорошим началом для его продвижения. –

+2

Vine, Facebook и Instagram на самом деле все довольно нестабильно при прокрутке видео. Есть некоторые очень заметные капли для всех из них. Instagram имеет лучшую производительность, но у меня также возникли проблемы с поиском экрана с большим количеством длинных видео высокой четкости в одно и то же время. Похоже, что они также не решили проблему, хотя у них у всех отличные инженеры и тонны ресурсов. Я уверен, что проблема с AVPlayer. Если вы мне не верите, запустите инструменты и посмотрите, как часто основной поток блокируется. – Antonio

+0

Другим хорошим примером является Склад. Это отличное приложение с удивительной прокруткой, и, очевидно, это сделали люди, которые знали, что они делают. Даже еще, создайте статью в хранилище и заполните ее кучей видео и попробуйте прокрутить страницу. Это очень изменчиво. Я уверен, что проблема здесь в AVPlayer. Нет никакого способа заставить его остановить блокировку основного потока. Даже если есть какой-то волшебный способ заставить AVPlayer вести себя, это непростительно плохой дизайн API, поскольку он не должен принимать смехотворные усилия, чтобы получить не ужасную производительность прокрутки. – Antonio

ответ

16

Постройте свой AVPlayerItem в фоновом режиме как можно больше (некоторые операции, которые вы должны выполнять в основном потоке, но вы можете выполнять операции настройки и ждать, пока свойства видео загружаются в фоновых очередях - внимательно прочитайте документы) , Это включает в себя Voodoo танцы с KVO и действительно не весело.

Икота происходит, пока AVPlayer ждет статуса AVPlayerItem, чтобы стать AVPlayerItemStatusReadyToPlay. Чтобы уменьшить длину икоты, вы хотите сделать столько, сколько сможете, чтобы довести AVPlayerItem ближе к AVPlayerItemStatusReadyToPlay на фоновом потоке, прежде чем назначать его AVPlayer.

Прошло некоторое время с тех пор, как я на самом деле реализовал это, но IIRC основные блоки потока вызваны тем, что базовые свойства AVURLAsset загружены лениво, и если вы не загружаете их самостоятельно, они загружаются основной нить, когда AVPlayer хочет играть.

Ознакомьтесь с документацией по AVAsset, особенно с информацией о товаре около AVAsynchronousKeyValueLoading. Я думаю, нам нужно было загрузить значения для duration и tracks, прежде чем использовать актив на AVPlayer, чтобы свести к минимуму блоки основного потока.Возможно, нам также пришлось пройти через каждый из треков и сделать AVAsynchronousKeyValueLoading на каждом из сегментов, но я не помню 100%.

+5

Большое спасибо за советы! Можете ли вы подробно остановиться на «построении' AVPlayerItem' в фоновой очереди, насколько это возможно ». Я попытался создать «AVPlayerItem» в фоновом потоке и загружать дорожки своего актива, однако его статус всегда является AVPlayerItemStatusUnknown. Похоже, что он не переходит на AVPlayerItemStatusReadyToPlay, пока он не связан с AVPlayer, и что AVPlayer подключен к AVPlayerLayer. Я уверен, что я делаю что-то не так. – Antonio

+0

@ Антонио Эй, тебе удалось это понять? Я также пытаюсь создать это – YuviGr

+0

Hey @YuviGr, я посмотрел на это намного больше, и похоже, что вы ничего не можете сделать. AVPlayer и AVFoundation собираются заблокировать основной поток, и нет хорошего способа обойти это. Это действительно удручает, но это архитектурная проблема, которую способны только люди в Apple. Честно говоря, единственный способ одновременного воспроизведения видео и плавной прокрутки - реализовать свой собственный видеопроигрыватель с нуля, минуя почти все AVFoundation. Либо это, либо ждать год для iOS 10, и надеюсь, что Apple к этому времени соберет его вместе. – Antonio

9

Не знаю, поможет ли это, но вот какой-то код, который я использую для загрузки видео в фоновом режиме, что определенно помогает с блокировкой основного потока (извинения, если он не компилирует 1: 1, я отвлекся от больше кода базы я работаю):

func loadSource() { 
    self.status = .Unknown 

    let operation = NSBlockOperation() 
    operation.addExecutionBlock {() -> Void in 
    // create the asset 
    let asset = AVURLAsset(URL: self.mediaUrl, options: nil) 
    // load values for track keys 
    let keys = ["tracks", "duration"] 
    asset.loadValuesAsynchronouslyForKeys(keys, completionHandler: {() -> Void in 
     // Loop through and check to make sure keys loaded 
     var keyStatusError: NSError? 
     for key in keys { 
      var error: NSError? 
      let keyStatus: AVKeyValueStatus = asset.statusOfValueForKey(key, error: &error) 
      if keyStatus == .Failed { 
       let userInfo = [NSUnderlyingErrorKey : key] 
       keyStatusError = NSError(domain: MovieSourceErrorDomain, code: MovieSourceAssetFailedToLoadKeyValueErrorCode, userInfo: userInfo) 
       println("Failed to load key: \(key), error: \(error)") 
      } 
      else if keyStatus != .Loaded { 
       println("Warning: Ignoring key status: \(keyStatus), for key: \(key), error: \(error)") 
      } 
     } 
     if keyStatusError == nil { 
      if operation.cancelled == false { 
       let composition = self.createCompositionFromAsset(asset) 
       // register notifications 
       let playerItem = AVPlayerItem(asset: composition) 
       self.registerNotificationsForItem(playerItem) 
       self.playerItem = playerItem 
       // create the player 
       let player = AVPlayer(playerItem: playerItem) 
       self.player = player 
      } 
     } 
     else { 
      println("Failed to load asset: \(keyStatusError)") 
     } 
    }) 

    // add operation to the queue 
    SomeBackgroundQueue.addOperation(operation) 
} 

func createCompositionFromAset(asset: AVAsset, repeatCount: UInt8 = 16) -> AVMutableComposition { 
    let composition = AVMutableComposition() 
    let timescale = asset.duration.timescale 
    let duration = asset.duration.value 
    let editRange = CMTimeRangeMake(CMTimeMake(0, timescale), CMTimeMake(duration, timescale)) 
    var error: NSError? 
    let success = composition.insertTimeRange(editRange, ofAsset: asset, atTime: composition.duration, error: &error) 
    if success { 
     for _ in 0 ..< repeatCount - 1 { 
      composition.insertTimeRange(editRange, ofAsset: asset, atTime: composition.duration, error: &error) 
     } 
    } 
    return composition 
} 
+0

Почему вы повторяли при создании AVMutableComposition? Я не вижу разницы во времени. Разве вы не используете AVPlayer повторно? Я борюсь с прокручивающими выступлениями, поэтому каждый намек похож на благословение :) –

+0

@JosipB. Я просто повторил цикл видео. Вы можете видеть, что он добавляет во время композиции.duration, поэтому каждый раз, когда он повторяется, изменяется композиция.duration. –

+0

Отличный ответ @ AndyPoes! Я ломаю голову над этим более 10 часов ... –

-1

мне удалось создать горизонтальную подачу подобного зрения с avplayer в каждой ячейке сделал это так:

  1. Буферизация - создать менеджер, чтобы вы может предварительно загрузить (буфер) видео. Сумма AVPlayers, которую вы хотите сохранить, зависит от опыта, который вы ищете. В моем приложении я управляю только 3 AVPlayers, поэтому один игрок сейчас играет, а предыдущие & следующие игроки буферизуются. Все менеджер буферизации делают это управление, что правильное видео буферизации в любой заданной точке

  2. Бывших клетки - Пусть TableView/CollectionView повторно клетки в cellForRowAtIndexPath: все, что вам нужно сделать, это после того, как вы dequqe клетки пройти мимо него это правильный игрок (я просто дать буферизации в indexPath на клетку, и он возвращает правильный)

  3. AVPlayer КВО в - Каждый раз, когда менеджер буферизации получает вызов, чтобы загрузить новое видео в буфере AVPlayer создать весь его активы и уведомления, просто назовите их так:

// игрок

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 
    self.videoContainer.playerLayer.player = self.videoPlayer; 
    self.asset = [AVURLAsset assetWithURL:[NSURL URLWithString:self.videoUrl]]; 
    NSString *tracksKey = @"tracks"; 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     [self.asset loadValuesAsynchronouslyForKeys:@[tracksKey] 
            completionHandler:^{       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 
              NSError *error; 
              AVKeyValueStatus status = [self.asset statusOfValueForKey:tracksKey error:&error]; 

              if (status == AVKeyValueStatusLoaded) { 
               self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset]; 
               // add the notification on the video 
               // set notification that we need to get on run time on the player & items 
               // a notification if the current item state has changed 
               [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:contextItemStatus]; 
               // a notification if the playing item has not yet started to buffer 
               [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:contextPlaybackBufferEmpty]; 
               // a notification if the playing item has fully buffered 
               [self.playerItem addObserver:self forKeyPath:@"playbackBufferFull" options:NSKeyValueObservingOptionNew context:contextPlaybackBufferFull]; 
               // a notification if the playing item is likely to keep up with the current buffering rate 
               [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:contextPlaybackLikelyToKeepUp]; 
               // a notification to get information about the duration of the playing item 
               [self.playerItem addObserver:self forKeyPath:@"duration" options:NSKeyValueObservingOptionNew context:contextDurationUpdate]; 
               // a notificaiton to get information when the video has finished playing 
               [NotificationCenter addObserver:self selector:@selector(itemDidFinishedPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem]; 
               self.didRegisterWhenLoad = YES; 

               self.videoPlayer = [AVPlayer playerWithPlayerItem:self.playerItem]; 

               // a notification if the player has chenge it's rate (play/pause) 
               [self.videoPlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:contextRateDidChange]; 
               // a notification to get the buffering rate on the current playing item 
               [self.videoPlayer addObserver:self forKeyPath:@"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:contextTimeRanges]; 
              } 
             }); 
            }]; 
    }); 
}); 

где: videoContainer - это вид вы хотите, чтобы добавить игрока в

Позвольте мне знать, если вам нужна помощь или больше объяснений

удачи:)

+0

Спасибо за это, но я действительно не понимаю, как это могло бы реально решить проблемы производительности, которые я наблюдал с AVFoundation. Какова ваша производительность прокрутки? Я уверен, что вы все равно будете бросать кучу кадров, когда вы свяжете AVPlayer с AVPlayerItem. Разве это не так? Если возможно, можете ли вы загрузить небольшой скринкаст о том, как выглядит производительность прокрутки? – Antonio

+0

Я использую пейджинг, включенный в виде коллекции, и когда начинается воспроизведение видео, и никаких прокруток не происходит, я связываю все элементы и активы буферизованных игроков. – YuviGr

+1

Итак, вы фактически не выполняете какую-либо работу до тех пор, пока страница не будет решена? Потому что это объясняет, как вы получаете приемлемый пользовательский интерфейс. Вы по-прежнему останавливаете основной поток, как сумасшедший, но нет никаких анимаций прокрутки для прерывания, поэтому это не заметно. Правильно ли это звучит? – Antonio

6

Если вы посмотрите на Facebook AsyncDisplayKit (движок за фидами Facebook и Instagram), вы можете отображать видео по большей части в фоновом потоке нас их AVideoNode. Если вы подсознаете это в ASDisplayNode и добавьте displayNode.view в любой вид, который вы прокручиваете (таблица/коллекция/прокрутка), вы можете добиться совершенно гладкой прокрутки (просто убедитесь, что создайте узел и активы и все, что на фоне нить). Единственная проблема заключается в том, чтобы изменить элемент видео, поскольку это заставляет себя на основной поток. Если у вас есть только несколько видеороликов в этом конкретном представлении, вы можете использовать этот метод!

 dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), { 
      self.mainNode = ASDisplayNode() 
      self.videoNode = ASVideoNode() 
      self.videoNode!.asset = AVAsset(URL: self.videoUrl!) 
      self.videoNode!.frame = CGRectMake(0.0, 0.0, self.bounds.width, self.bounds.height) 
      self.videoNode!.gravity = AVLayerVideoGravityResizeAspectFill 
      self.videoNode!.shouldAutoplay = true 
      self.videoNode!.shouldAutorepeat = true 
      self.videoNode!.muted = true 
      self.videoNode!.playButton.hidden = true 

      dispatch_async(dispatch_get_main_queue(), { 
       self.mainNode!.addSubnode(self.videoNode!) 
       self.addSubview(self.mainNode!.view) 
      }) 
     }) 
+0

В настоящее время, чтобы использовать ASVideoNode, вам нужно сработать с главной веткой AsyncDisplayKit – Kevin

+0

Установка с pod в порядке, вам просто нужно включить ASVideoNode.h в файл моста заголовка. Но @Kevin верен, ASVideoNode является чрезвычайно новым и, тем самым, официально не поддерживается. – Gregg

+2

@Gregg Вы на самом деле попробовали это? 'ASVideoNode' является просто оболочкой для' AVPlayerLayer'. Это не волшебство, и это не даст вам никаких преимуществ по производительности прокрутки для воспроизведения видео. – Antonio

0

Вот рабочее решение для отображения «видеостена» в UICollectionView:

1) хранить все ваши клетки в NSMapTable (отныне, вы получите доступ только объект ячейки из NSMapTable):

self.cellCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsWeakMemory valueOptions:NSPointerFunctionsStrongMemory capacity:AppDelegate.sharedAppDelegate.assetsFetchResults.count]; 
    for (NSInteger i = 0; i < AppDelegate.sharedAppDelegate.assetsFetchResults.count; i++) { 
     [self.cellCache setObject:(AssetPickerCollectionViewCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]] forKey:[NSIndexPath indexPathForItem:i inSection:0]]; 
    } 

2) Добавить этот метод в ваш UICollectionViewCell подкласса:

- (void)setupPlayer:(PHAsset *)phAsset { 
typedef void (^player) (void); 
player play = ^{ 
    NSString __autoreleasing *serialDispatchCellQueueDescription = ([NSString stringWithFormat:@"%@ serial cell queue", self]); 
    dispatch_queue_t __autoreleasing serialDispatchCellQueue = dispatch_queue_create([serialDispatchCellQueueDescription UTF8String], DISPATCH_QUEUE_SERIAL); 
    dispatch_async(serialDispatchCellQueue, ^{ 
     __weak typeof(self) weakSelf = self; 
     __weak typeof(PHAsset) *weakPhAsset = phAsset; 
     [[PHImageManager defaultManager] requestPlayerItemForVideo:weakPhAsset options:nil 
                resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) { 
                 if(![[info objectForKey:PHImageResultIsInCloudKey] boolValue]) { 
                  AVPlayer __autoreleasing *player = [AVPlayer playerWithPlayerItem:playerItem]; 
                  __block typeof(AVPlayerLayer) *weakPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:player]; 
                  [weakPlayerLayer setFrame:weakSelf.contentView.bounds]; //CGRectMake(self.contentView.bounds.origin.x, self.contentView.bounds.origin.y, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height * (9.0/16.0))]; 
                  [weakPlayerLayer setVideoGravity:AVLayerVideoGravityResizeAspect]; 
                  [weakPlayerLayer setBorderWidth:0.25f]; 
                  [weakPlayerLayer setBorderColor:[UIColor whiteColor].CGColor]; 
                  [player play]; 
                  dispatch_async(dispatch_get_main_queue(), ^{ 
                   [weakSelf.contentView.layer addSublayer:weakPlayerLayer]; 
                  }); 
                 } 
                }]; 
    }); 

    }; play(); 
} 

3) Вызвать метод выше от вашего UICollectionView делегата так:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{ 

    if ([[self.cellCache objectForKey:indexPath] isKindOfClass:[AssetPickerCollectionViewCell class]]) 
     [self.cellCache setObject:(AssetPickerCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:indexPath] forKey:indexPath]; 

    dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH), ^{ 
     NSInvocationOperation *invOp = [[NSInvocationOperation alloc] 
             initWithTarget:(AssetPickerCollectionViewCell *)[self.cellCache objectForKey:indexPath] 
             selector:@selector(setupPlayer:) object:AppDelegate.sharedAppDelegate.assetsFetchResults[indexPath.item]]; 
     [[NSOperationQueue mainQueue] addOperation:invOp]; 
    }); 

    return (AssetPickerCollectionViewCell *)[self.cellCache objectForKey:indexPath]; 
} 

Кстати, вот как вы бы заполнить коллекцию PHFetchResult со всеми видео в видео папке приложения Фотографии:

// Collect all videos in the Videos folder of the Photos app 
- (PHFetchResult *)assetsFetchResults { 
    __block PHFetchResult *i = self->_assetsFetchResults; 
    if (!i) { 
     static dispatch_once_t onceToken; 
     dispatch_once(&onceToken, ^{ 
      PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumVideos options:nil]; 
      PHAssetCollection *collection = smartAlbums.firstObject; 
      if (![collection isKindOfClass:[PHAssetCollection class]]) collection = nil; 
      PHFetchOptions *allPhotosOptions = [[PHFetchOptions alloc] init]; 
      allPhotosOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; 
      i = [PHAsset fetchAssetsInAssetCollection:collection options:allPhotosOptions]; 
      self->_assetsFetchResults = i; 
     }); 
    } 
    NSLog(@"assetsFetchResults (%ld)", self->_assetsFetchResults.count); 

    return i; 
} 

Если вы хотите отфильтровать видео, которые являются локальными (а не в ICloud), который является тем, что я бы предположить, видя, как вы ищете гладкой прокрутки:

// Filter videos that are stored in iCloud 
- (NSArray *)phAssets { 
    NSMutableArray *assets = [NSMutableArray arrayWithCapacity:self.assetsFetchResults.count]; 
    [[self assetsFetchResults] enumerateObjectsUsingBlock:^(PHAsset *asset, NSUInteger idx, BOOL *stop) { 
     if (asset.sourceType == PHAssetSourceTypeUserLibrary) 
      [assets addObject:asset]; 
    }]; 

    return [NSArray arrayWithArray:(NSArray *)assets]; 
} 
+0

Какой смысл использовать NSMapTable для всех ваших ячеек просмотра коллекции? Является ли повторное использование ячеек коллекции, которое будет делать кеширование одних и тех же ячеек, делающих это избыточным? Я не понимаю, почему это необходимо. Как заполняется AppDelegate.sharedAppDelegate.assetsFetchResults? Что делать, если вы попытались сыграть кучу видео из вашего пакета приложений или удаленных видеороликов, размещенных в Интернете? – Antonio

+0

Повторное использование не является уникальным. Как он знает, что повторно использовать? Это просто повторное использование взгляда; мои другие объекты не зависят от представления. Когда вы меняете представление, у вас все еще есть старый AVPlayerItem/AVPlayer/etc. Чтобы получить новый, вы должны связать это различие, назначив эти объекты определенной ячейке по определенному индексу. Когда я этого не делал, прокрутка была гладкой, только явные изначально загружались, остальные были дублированы. –

+0

Я исправляю свой комментарий, чтобы лучше ответить на ваш вопрос о отображении правильных видео в ячейки. Короче говоря, я больше этого не делаю. Когда я делаю, чтобы убедиться, что объект соответствует целевой ячейке, нужно использовать идентификатор актива для установки тега ячейки. Затем я просто проверю, чтобы убедиться, что тег ячейки и идентификатор объекта совпадают, прежде чем продолжить; это один звонок за другим. Это предотвращает возникшую у меня проблему. Другим решением, которое я успешно использую, является ссылка на все представления в ячейке по номеру тега. Никогда по имени. И я всегда делаю новые ссылки на них каждый раз, когда я их использую. –

Смежные вопросы