Mantener un buen performance de desplazamiento al usar AVPlayer

Estoy trabajando en una aplicación donde hay una vista de colección, y las celdas de la vista de colección pueden contener video. En este momento estoy mostrando el video con AVPlayer y AVPlayerLayer . Desafortunadamente, el performance de desplazamiento es terrible. Parece que AVPlayer , AVPlayerItem y AVPlayerLayer hacen mucho de su trabajo en el hilo principal. Están constantemente sacando candados, esperando en semáforos, etc., lo que bloquea el hilo principal y provoca caídas severas.

¿Hay alguna manera de decirle a AVPlayer que deje de hacer tantas cosas en el hilo principal? Hasta ahora, nada de lo que he intentado ha resuelto el problema.

También traté de build un reproductor de video simple usando AVSampleBufferDisplayLayer . Con eso puedo asegurarme de que todo sucede fuera del hilo principal, y puedo lograr ~ 60 fps mientras navego y reproduzco video. Desafortunadamente, ese método es mucho más bajo, y no proporciona cosas como la reproducción de audio y el lavado de time fuera de la caja. ¿Hay alguna forma de get un performance similar con AVPlayer ? Prefiero usar eso.

Editar: después de search más en esto, no parece que sea posible lograr un buen performance de desplazamiento al usar AVPlayer . Crear un AVPlayer y asociarse con una instancia de AVPlayerItem inicia un montón de trabajo que trampolina en el hilo principal donde luego espera en los semáforos e intenta adquirir un montón de lockings. La cantidad de time que esto detiene el hilo principal aumenta considerablemente a medida que aumenta la cantidad de videos en el scrollview.

AVPlayer Dealloc también parece ser un gran problema. Dealloc'ing un AVPlayer también trata de sincronizar un montón de cosas. Una vez más, esto se pone extremadamente malo a medida que creas más jugadores.

Esto es bastante deprimente, y hace que AVPlayer sea ​​casi inutilizable por lo que bash hacer. Bloquear el hilo principal de esta manera es tan aficionado a hacerlo que es difícil creer que los ingenieros de Apple hubieran cometido este tipo de error. De todos modos, es de esperar que puedan solucionar esto pronto.

Cree su AVPlayerItem en una queue de background tanto como sea posible (algunas operaciones tiene que hacer en el hilo principal, pero puede realizar operaciones de configuration y esperar a que las properties de video se carguen en las queues de background, lea los documentos con mucho cuidado). Esto implica bailes vudú con KVO y realmente no es divertido.

El hipo sucede mientras AVPlayer está esperando que el estado de AVPlayerItemStatusReadyToPlay convierta en AVPlayerItemStatusReadyToPlay . Para networkingucir la longitud de los contratimes, desea hacer todo lo posible para acercar el AVPlayerItem a AVPlayerItemStatusReadyToPlay en un hilo de background antes de asignarlo al AVPlayer .

Ha pasado un time desde que realmente implementé esto, pero IIRC los bloques de subprocesss principales son causados ​​porque las AVURLAsset subyacentes de AVURLAsset están cargadas perezosamente, y si no las cargas tú mismo, se cargan ocupadas en el hilo principal cuando el AVPlayer quiere jugar.

Consulte la documentation de AVAsset, especialmente las cosas relacionadas con AVAsynchronousKeyValueLoading . Creo que necesitábamos cargar los valores de duration y tracks antes de usar el recurso en un AVPlayer para minimizar los bloques de hilos principales. Es posible que también hayamos tenido que recorrer cada una de las pistas y hacer AVAsynchronousKeyValueLoading en cada uno de los segmentos, pero no recuerdo el 100%.

No sé si esto ayudará, pero aquí hay un código que estoy usando para cargar videos en la queue de background que definitivamente ayuda con el locking del subprocess principal (disculpas si no comstack 1: 1, abstraí de una base de código más grande I Estoy trabajando):

 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 } 

Si miras el AsyncDisplayKit de Facebook (el motor detrás de las fonts de Facebook y Instagram), puedes reproducir el video en su mayor parte en subprocesss de background usando su AVideoNode . Si lo subnoda a un ASDisplayNode y agrega displayNode.view a cualquier vista en la que se desplace (tabla / colección / desplazamiento) puede lograr un desplazamiento perfectamente suave (solo asegúrese de crear el nodo y los resources y todo eso en un subprocess de background) . El único problema es cuando se cambia el elemento de video, ya que esto se impone al hilo principal. ¡Si solo tiene algunos videos en esa vista en particular, puede usar este método!

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

Aquí hay una solución de trabajo para mostrar un "muro de video" en UICollectionView:

1) Almacene todas sus celdas en un NSMapTable (de ahora en adelante, solo accederá a un object de celda desde el NSMapTable):

 self.cellCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsWeakMemory valueOptions:NSPointerFunctionsStrongMemory capacity:AppDelegate.shanetworkingAppDelegate.assetsFetchResults.count]; for (NSInteger i = 0; i < AppDelegate.shanetworkingAppDelegate.assetsFetchResults.count; i++) { [self.cellCache setObject:(AssetPickerCollectionViewCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]] forKey:[NSIndexPath indexPathForItem:i inSection:0]]; } 

2) Agregue este método a su subclass 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) Llame al método anterior desde su delegado de UICollectionView de esta manera:

 - (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.shanetworkingAppDelegate.assetsFetchResults[indexPath.item]]; [[NSOperationQueue mainQueue] addOperation:invOp]; }); return (AssetPickerCollectionViewCell *)[self.cellCache objectForKey:indexPath]; } 

Por cierto, así es como llenarías una colección PHFetchResult con todos los videos en la carpeta Video de la aplicación Fotos:

 // 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 desea filtrar los videos que son locales (y no en iCloud), que es lo que asumí, ya que está buscando un desplazamiento suave:

 // Filter videos that are stonetworking 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]; } 

Me las avplayer para crear una vista horizontal como vista con avplayer en cada celda:

  1. Almacenamiento en búfer: cree un administrador para que pueda cargar (almacenar) los videos. La cantidad de AVPlayers que desea almacenar depende de la experiencia que está buscando. En mi aplicación gestiono solo 3 AVPlayers , por lo que se está reproduciendo un jugador ahora y los jugadores anteriores y siguientes están siendo almacenados en el búfer. Todo lo que está haciendo el administrador de almacenamiento en búfer es gestionar que el video correcto se almacene en un búfer en un punto dado

  2. Células reutilizadas: permita que TableView / CollectionView reutilice las celdas en cellForRowAtIndexPath: todo lo que tiene que hacer es después de que la célula le pase el jugador correcto (solo le doy al búfer un índicePath en la celda y él devuelve el correcto)

  3. AVPlayer KVO's: cada vez que el administrador de búfer recibe una llamada para cargar un nuevo video para proteger al AVPlayer, cree todos sus activos y notifications, solo llámelos de esta manera:

// jugador

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

donde: videoContainer – es la vista a la que desea agregar el reproductor

Avísame si necesitas ayuda o más explicaciones.

Buena suerte 🙂