Подтвердить что ты не робот

Поддержание хорошей производительности прокрутки при использовании AVPlayer

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

Не знаю, поможет ли это - но вот некоторый код, который я использую для загрузки видео в фоновую очередь, который определенно помогает с блокировкой основного потока (извиняюсь, если он не компилируется 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 createCompositionFromAsset(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
}

Ответ 3

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

        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)
            })
        })

Ответ 4

Здесь представлено рабочее решение для отображения "видеостены" в 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];
}

Ответ 5

Я поиграл со всеми ответами выше и обнаружил, что они верны только до определенного предела.

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

Swift 4

ех.

let mediaUrl = //your media string
let player = AVPlayer()
let playerItem = AVPlayerItem(url: mediaUrl)

DispatchQueue.global(qos: .default).async {
    player.replaceCurrentItem(with: playerItem)
}

Ответ 6

Я полагаю, AVQueuePlayer полезен для этой проблемы.

Ответ 7

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

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

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

  • avplayer KVO - каждый раз, когда менеджер буферизации получает вызов для загрузки нового видео, чтобы буфер AVPlayer создавал все его активы и уведомления, просто назовите их так:

//player

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 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 - это вид, который вы хотите добавить в плеер

Сообщите мне, если вам нужна помощь или дополнительные объяснения

Удачи:)