web-dev-qa-db-fra.com

Maintenir de bonnes performances de défilement lorsque vous utilisez AVPlayer

Je travaille sur une application dans laquelle il existe une vue de collection et où les cellules de la vue de 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 effectuent une grande partie de leur travail sur le fil principal. Ils sortent constamment des verrous, attendent des sémaphores, etc., ce qui bloque le fil principal et provoque de graves pertes d'images.

Existe-t-il un moyen de dire à AVPlayer de ne plus faire tant de choses sur le fil principal? Jusqu'à présent, rien de ce que j'ai essayé n'a résolu le problème.

J'ai également essayé de construire un lecteur vidéo simple en utilisant AVSampleBufferDisplayLayer. En utilisant cela, je peux m'assurer que tout se passe hors du fil principal et que je peux atteindre environ 60 images par seconde lors du défilement et de la lecture de la vidéo. Malheureusement, cette méthode est beaucoup plus basse et ne fournit pas de choses comme la lecture audio et le nettoyage du temps hors de la boîte. Existe-t-il un moyen d'obtenir des performances similaires avec AVPlayer? Je préférerais de beaucoup l'utiliser.

Edit: Après avoir examiné cela de plus près, il n’est pas possible d’obtenir de bonnes performances de défilement lorsque AVPlayer est utilisé. La création d'une AVPlayer et son association à une instance AVPlayerItem lancent un ensemble de travaux qui trampolines sur le thread principal où il attend ensuite des sémaphores et tente d'acquérir un ensemble de verrous. La durée pendant laquelle le fil principal est bloqué augmente de façon considérable à mesure que le nombre de vidéos dans la vue de défilement augmente.

AVPlayer dealloc semble également être un énorme problème. Le traitement d'une AVPlayer tente également de synchroniser un tas de choses. Encore une fois, cela devient extrêmement pénible à mesure que vous créez plus de joueurs.

C'est assez déprimant, et cela rend AVPlayer presque inutilisable pour ce que j'essaie de faire. Bloquer le fil conducteur comme celui-ci est une chose tellement amateur à faire qu'il est difficile de croire que les ingénieurs d'Apple auraient commis ce type d'erreur. Quoi qu'il en soit, j'espère qu'ils pourront résoudre ce problème rapidement.

50
Antonio

Construisez votre AVPlayerItem dans une file d'attente en arrière-plan autant que possible (certaines opérations que vous devez effectuer sur le thread principal, mais vous pouvez effectuer des opérations de configuration et attendre que les propriétés de la vidéo soient chargées dans les files d'attente en arrière-plan - lisez très attentivement la documentation) . Cela implique des danses vaudou avec le KVO et ce n’est vraiment pas amusant.

Le hoquet se produit pendant que la AVPlayer attend que le statut de AVPlayerItems devienne AVPlayerItemStatusReadyToPlay. Pour réduire la longueur du hoquet que vous voulez faire autant que possible, rapprochez le AVPlayerItem de AVPlayerItemStatusReadyToPlay sur un thread d'arrière-plan avant de l'assigner à AVPlayer.

Cela fait longtemps que je n'ai pas implémenté cela, mais les blocs de thread principaux sont générés par IIRC parce que les propriétés sous-jacentes de AVURLAsset sous-jacentes sont chargées paresseuses, et si vous ne les chargez pas vous-même, elles sont occupées par la charge principale fil lorsque le AVPlayer veut jouer.

Consultez la documentation AVAsset, en particulier les informations relatives à AVAsynchronousKeyValueLoading. Je pense que nous devions charger les valeurs pour duration et tracks avant d'utiliser l'actif sur un AVPlayer afin de minimiser les principaux blocs de thread. Il est possible que nous ayons également dû parcourir chacune des pistes et effectuer AVAsynchronousKeyValueLoading sur chacun des segments, mais je ne me souviens pas à 100%.

19
damian

Je ne sais pas si cela aidera - mais voici le code que j'utilise pour charger les vidéos sur la file d'attente d'arrière-plan, ce qui aide définitivement à bloquer les threads principaux (excuses si cela ne compile pas 1: 1, j'ai extrait un fichier de base plus volumineux I 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
}
12
Andy Poes

Si vous regardez dans le AsyncDisplayKit de Facebook (le moteur derrière les flux Facebook et Instagram), vous pouvez rendre la vidéo pour la plupart sur des discussions en arrière-plan en utilisant leur AVideoNode. Si vous sous-noeudz dans un ASDisplayNode et ajoutez le displayNode.view à la vue que vous faites défiler (table/collection/défilement), vous pouvez obtenir un défilement parfaitement lisse (assurez-vous simplement de créer le noeud, les actifs et tout un fil de fond). Le seul problème concerne le changement d’élément vidéo, car cela s’impose au fil principal. Si vous ne disposez que de 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
Gregg

J'ai joué avec toutes les réponses ci-dessus et découvert qu'elles n'étaient vraies que dans une certaine limite.

Le moyen le plus simple et le plus simple qui a fonctionné pour moi jusqu'à présent est que le code que vous affectez à votre AVPlayerItem à votre instance AVPlayer dans un thread en arrière-plan. J'ai remarqué que l'attribution de la variable AVPlayerItem au lecteur sur le thread principal (même après que l'objet AVPlayerItem soit prêt) a toujours un effet négatif sur vos performances et votre cadence. 

Swift 4

ex.

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

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

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

1) Stockez toutes vos cellules dans une table NSMapTable (à partir de maintenant, vous ne pourrez accéder qu’à un objet cellule depuis la table 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 à partir de 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];
}

A propos, voici comment vous pouvez 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 souhaitez filtrer les vidéos locales (et non dans iCloud), c'est ce que je suppose, dans la mesure où vous recherchez un défilement régulier:

// 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
James Bush