Question Maintenir de bonnes performances de défilement lors de l'utilisation d'AVPlayer


Je travaille sur une application où il y a une vue de collection et les cellules de la vue de collection peuvent contenir de la vidéo. En ce moment je montre la vidéo en utilisant AVPlayer et AVPlayerLayer. Malheureusement, la performance de défilement est terrible. Il semble que AVPlayer, AVPlayerItem, et AVPlayerLayer faire beaucoup de leur travail sur le fil conducteur. Ils sortent constamment les serrures, attendent les sémaphores, etc. qui bloquent le fil principal et provoquent des chutes de trames sévères.

Y a-t-il un moyen de dire AVPlayer arrêter de faire autant de choses sur le fil conducteur? Jusqu'à présent, rien de ce que j'ai essayé n'a résolu le problème.

J'ai aussi essayé de construire un simple lecteur vidéo en utilisant AVSampleBufferDisplayLayer. En utilisant cela, je peux m'assurer que tout se passe hors du thread principal, et je peux atteindre ~ 60fps tout en faisant défiler et lire la vidéo. Malheureusement, cette méthode est beaucoup moins sophistiquée et ne fournit pas de fonctionnalités telles que la lecture audio et le nettoyage rapide. Est-il possible d'obtenir des performances similaires avec AVPlayer? Je préférerais beaucoup l'utiliser.

Modifier: Après avoir examiné cela plus, il ne semble pas possible d'obtenir de bonnes performances de défilement lors de l'utilisation AVPlayer. Créer un AVPlayer et en associant avec un AVPlayerItem l'instance lance un tas de travail qui trampoline sur le thread principal où il attend ensuite des sémaphores et essaie d'acquérir un tas de verrous. La durée pendant laquelle le thread principal est bloqué augmente considérablement au fur et à mesure que le nombre de vidéos dans la vue de défilement augmente.

AVPlayer dealloc semble également être un énorme problème. Deallocing un AVPlayer essaie également de synchroniser un tas de choses. Encore une fois, cela devient extrêmement grave lorsque vous créez plus de joueurs.

C'est assez déprimant, et ça fait AVPlayer presque inutilisable pour ce que j'essaie de faire. Bloquant le thread principal comme celui-ci est une chose tellement amateur, il est difficile de croire que les ingénieurs d'Apple auraient fait ce genre d'erreur. Quoi qu'il en soit, j'espère qu'ils pourront résoudre ce problème rapidement.


47
2018-05-21 02:22


origine


Réponses:


Construisez votre AVPlayerItem dans une file d'attente en arrière-plan autant que possible (certaines opérations doivent être effectuées sur le thread principal, mais vous pouvez effectuer des opérations de configuration et attendre que les propriétés vidéo se chargent sur les files d'attente en arrière-plan). Cela implique des danses vaudou avec KVO et ce n'est vraiment pas amusant.

Les hoquets se produisent alors que le AVPlayer attend la AVPlayerItems statut pour devenir AVPlayerItemStatusReadyToPlay. Pour réduire la longueur du hoquet, vous voulez faire le maximum pour amener le AVPlayerItem plus proche de AVPlayerItemStatusReadyToPlay sur un fil de fond avant de l'assigner à la AVPlayer.

Cela fait un moment que je ne l'ai pas réellement implémenté, mais les blocs de thread principaux d'IIRC sont dus au fait que le sous-jacent AVURLAssetles propriétés sont chargées paresseusement, et si vous ne les chargez pas vous-même, elles sont chargées sur le thread principal lorsque la AVPlayer vouloir jouer.

Consultez la documentation AVAsset, en particulier les informations AVAsynchronousKeyValueLoading. Je pense que nous devions charger les valeurs pour duration et tracksavant d'utiliser l'actif sur un AVPlayer pour minimiser les blocs de fil principaux. Il est possible que nous devions aussi parcourir chacune des pistes et faire AVAsynchronousKeyValueLoading sur chacun des segments, mais je ne me souviens pas de 100%.


19
2018-05-21 14:14



Je ne sais pas si cela peut aider - mais voici un code que j'utilise pour charger des vidéos sur une file d'attente en arrière-plan qui aide certainement à bloquer le thread principal (Apologies si elle ne compile pas 1: 1) travaille sur):

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
}

9
2017-07-14 22:57



Si vous regardez dans Facebook AsyncDisplayKit (le moteur derrière les flux Facebook et Instagram), vous pouvez rendre la vidéo pour la plupart sur des threads de fond en utilisant leur AVideoNode. Si vous sous-composez cela dans un ASDisplayNode et ajouter le displayNode.view à n'importe quelle vue que vous faites défiler (table / collection / scroll) vous pouvez obtenir un défilement parfaitement régulier (assurez-vous simplement que le noeud et les ressources sont créés et tout cela sur un thread d'arrière-plan). Le seul problème est lorsque vous changez l'élément vidéo, car cela se force sur le thread principal. Si vous n'avez que quelques vidéos sur cette vue, vous pouvez utiliser cette méthode!

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

7
2018-03-12 18:37



Voici une solution de travail pour afficher un "mur vidéo" dans un UICollectionView:

1) Stockez toutes vos cellules dans un NSMapTable (à partir de maintenant, vous n’accéderez qu’à un objet de la 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) Ajoutez cette méthode à votre sous-classe 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) Appelez la méthode ci-dessus depuis votre délégué UICollectionView de la manière suivante:

- (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];
}

En passant, voici comment remplir une collection PHFetchResult avec toutes les vidéos du dossier Vidéo de l'application Photos:

// 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;
}

Si vous voulez filtrer les vidéos locales (et non iCloud), c'est ce que je suppose, vu que vous recherchez le défilement fluide:

// 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
2017-07-13 21:13



Je parviens à créer un flux horizontal comme la vue avec avplayer dans chaque cellule l'a-t-il fait ainsi:

  1. Mise en mémoire tampon - créez un gestionnaire pour pouvoir précharger (mettre en mémoire tampon) les vidéos. La quantité de AVPlayers vous voulez tamponner dépend de l'expérience que vous recherchez. Dans mon application, je gère seulement 3 AVPlayersAinsi, un joueur est en train de jouer et les joueurs précédents et suivants sont mis en tampon. Tout ce que fait le gestionnaire de la mémoire tampon consiste à gérer la mise en mémoire tampon de la vidéo à un moment donné.

  2. Cellules réutilisées - Laissez le TableView / CollectionView réutiliser les cellules dans cellForRowAtIndexPath: tout ce que vous avez à faire, c'est après que la cellule lui ait passé, il a le bon joueur (je donne juste un indexPath sur la cellule et il retourne le bon)

  3. AVPlayer KVO - Chaque fois que le gestionnaire de mémoire tampon reçoit un appel pour charger une nouvelle vidéo afin de mettre en mémoire tampon l'AVPlayer pour créer tous ses actifs et notifications, appelez-les comme suit:

// joueur

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

où: videoContainer - est la vue que vous souhaitez ajouter au lecteur

Faites-moi savoir si vous avez besoin d'aide ou plus d'explications

Bonne chance :)


-1
2018-01-22 07:24