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.
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%.
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
}
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)
})
})
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];
}
j'arrive à créer un flux horizontal comme view avec avplayer
dans chaque cellule fait comme suit:
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 3AVPlayers
, 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écellules réutilisées-laissez le
TableView
/CollectionView
réutiliser les cellulescellForRowAtIndexPath:
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)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 :)