Maintenir de bonnes performances de défilement lors de L'utilisation D'AVPlayer

je travaille sur une application où il y a une vue collection, et les cellules de la vue collection peuvent contenir de la vidéo. En ce moment, j'affiche 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 thread principal. Ils sortent constamment les écluses, attendent les sémaphores,etc. qui bloque le fil principal et provoque de graves chutes de cadre.

Est-il façon de dire AVPlayer arrêter de faire autant de choses sur le fil principal? Pour l'instant rien, j'ai essayé 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 fil principal, et je peux atteindre ~60fps tout en défilant et en jouant la vidéo. Malheureusement, cette méthode est beaucoup plus bas niveau, et il ne fournit pas des choses comme la lecture audio et le temps frotter hors de la boîte. Est-il possible d'obtenir similaire la performance AVPlayer? Je préfère l'utiliser.

Edit: Après avoir examiné cela plus, il ne semble pas qu'il soit possible d'atteindre de bonnes performances de défilement en utilisant AVPlayer. La création d'un AVPlayer et en s'associant avec un AVPlayerItem l'instance déclenche un tas de travaux qui piétinent le fil principal où elle attend ensuite sur des sémaphores et tente d'acquérir un tas de serrures. La quantité de temps cela décroche le fil principal augmente tout à fait dramatiquement, le nombre de vidéos dans le scrollview augmente.

AVPlayer dealloc semble aussi être un énorme problème. Dealloc require AVPlayer essaie aussi de synchroniser un tas de trucs. Encore une fois, cela devient extrêmement mauvais que vous créez plus de joueurs.

C'est assez déprimant, et il fait AVPlayer presque inutilisable pour ce que j'essaie de faire. Bloquer le fil principal comme ceci est une telle chose d'amateur à faire, il est donc difficile de croire que les ingénieurs Apple aurait fait cela genre d'erreur. De toute façon, j'espère qu'ils pourront réparer ça bientôt.

47
demandé sur Antonio 2015-05-21 05:22:41

5 réponses

Construisez votre AVPlayerItem dans une file d'attente de fond autant que possible (certaines opérations que vous devez faire sur le thread principal, mais vous pouvez faire des opérations de configuration et attendre que les propriétés vidéo se chargent sur les files d'attente de fond - lire les docs très attentivement). Cela implique des danses vaudou avec KVO et est vraiment pas amusant.

le hoquet se produit pendant que le AVPlayer est en attente pour le AVPlayerItem s statut à devenir AVPlayerItemStatusReadyToPlay. Pour réduire la longueur du hoquet vous voulez faire comme beaucoup que vous pouvez pour apporter de l' AVPlayerItem plus proche de AVPlayerItemStatusReadyToPlay sur un thread d'arrière-plan avant de l'affecter à l' AVPlayer.

Cela fait longtemps que je n'ai pas implémenté cela, mais IIRC les blocs de thread principaux sont causés parce que le sous-jacent AVURLAsset'propriétés sont chargement paresseux, et si vous ne chargez pas en vous-même, ils get busy-chargé sur le thread principal lorsque l' AVPlayer veut jouer.

consultez la documentation D'AVAsset, surtout les choses autour de AVAsynchronousKeyValueLoading. Je pense que nous avons besoin pour charger les valeurs de duration et tracks avant d'utiliser l'actif sur une AVPlayer pour minimiser les blocs de thread principaux. Il est possible que nous ayons aussi dû marcher à travers chacune des pistes et faire AVAsynchronousKeyValueLoading sur chacun des segments, mais je ne me souviens pas 100%.

19
répondu damian 2016-09-22 08:52:19

Ne sais pas si cela va aider, mais voici un code que j'utilise pour charger les vidéos sur le fond de la file d'attente qui aide vraiment avec le thread principal blocage (toutes mes Excuses si il n'est pas compilé 1:1, je l'ai puisée dans une plus grande base de code, je 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 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
}
9
répondu Andy Poes 2018-09-18 20:25:34

Si vous regardez dans Facebook AsyncDisplayKit (le moteur derrière Facebook et Instagram feeds) vous pouvez rendre la vidéo pour la plupart sur les threads d'arrière-plan en utilisant leur AVideoNode. Si vous sous-nœud dans un ASDisplayNode et ajouter le displayNode.vue vers n'importe quelle vue que vous faites défiler (table/collection/scroll) vous pouvez obtenir un défilement parfaitement lisse (assurez-vous juste que le créer le noeud et les actifs et tout cela sur un fil d'arrière-plan). Le le seul problème est d'avoir la modification de l'élément vidéo, car cela se force sur le fil principal. Si vous n'avez que quelques vidéos sur cette vue particulière, 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
répondu Gregg 2016-03-12 18:45:40

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

1) Stockez toutes vos cellules dans une NSMapTable (à partir de Maintenant, vous n'aurez accès 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) Ajouter cette méthode à votre UICollectionViewCell sous-classe:

- (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 à partir de votre délégué UICollectionView de cette façon:

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

au fait, voici comment vous pourriez peupler un PHFetchResult collection avec toutes les vidéos dans le dossier de la Vidéo de l'app 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 des vidéos qui sont locales( et pas dans iCloud), ce qui est ce que je suppose, vu que vous recherchez un scrolling en douceur:

// 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
répondu James Bush 2016-07-15 06:23:42

j'arrive à créer un flux horizontal comme view avec avplayer dans chaque cellule fait comme suit:

  1. Buffering-créer un gestionnaire pour que vous puissiez précharger (buffer) les vidéos. Le montant de l' AVPlayers vous voulez tampon dépend de l'expérience que vous recherchez. Dans mon application Je ne gère que 3 AVPlayers, donc un joueur est joué maintenant et les joueurs précédents et suivants sont tamponnés. Tout ce que fait le gestionnaire de buffering est de s'assurer que la bonne vidéo est tamponnée à un moment donné

  2. cellules réutilisées-laissez le TableView/CollectionView réutiliser les cellules cellForRowAtIndexPath: tout ce que vous avez à faire, c'est après que vous lui avez demandé la passe de la cellule c'est le bon joueur (je donne juste au tampon un indexPath sur la cellule et il renvoie le bon)

  3. AVPlayer KVO's-chaque fois que le gestionnaire de buffering reçoit un appel pour charger une nouvelle vidéo pour buffer L'AVPlayer créer tous ses actifs et notifications, il suffit de les appeler comme donc:

// 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'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 voulez ajouter le lecteur à

Laissez-moi savoir si vous avez besoin d'aide ou de plus amples explications

Bonne chance :)

-1
répondu YuviGr 2016-01-22 07:24:37