ios video avfoundation avplayer

ios - avplayer windows



Mantener un buen rendimiento de desplazamiento al usar AVPlayer (6)

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 usando AVPlayer y AVPlayerLayer . Desafortunadamente, el rendimiento de desplazamiento es terrible. Parece que AVPlayer , AVPlayerItem y AVPlayerLayer hacen mucho de su trabajo en el hilo principal. Están constantemente sacando cerraduras, esperando semáforos, etc., lo que bloquea el hilo principal y provoca graves caídas del marco.

¿Hay alguna forma 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 intenté construir un reproductor de video simple usando AVSampleBufferDisplayLayer . Usando eso, puedo asegurarme de que todo suceda fuera del hilo principal, y puedo alcanzar ~ 60 fps mientras me desplazo y reproduzco video. Desafortunadamente, ese método tiene un nivel mucho más bajo y no proporciona cosas como la reproducción de audio y la eliminación del tiempo fuera de la caja. ¿Hay alguna forma de obtener un rendimiento similar con AVPlayer ? Prefiero usar eso.

Editar: después de analizar esto más, no parece que sea posible lograr un buen rendimiento de desplazamiento al usar AVPlayer . Crear un AVPlayer y asociarse con una instancia de AVPlayerItem inicia un montón de trabajo que trampolines en el hilo principal donde luego espera en semáforos e intenta adquirir un montón de bloqueos. La cantidad de tiempo que esto detiene el hilo principal aumenta dramáticamente a medida que aumenta el número de videos en la vista de desplazamiento.

AVPlayer dealloc también parece ser un gran problema. Dealloc''ing a AVPlayer también intenta sincronizar un montón de cosas. Nuevamente, esto se vuelve extremadamente malo a medida que creas más jugadores.

Esto es bastante deprimente y hace que AVPlayer sea ​​casi inutilizable para lo que estoy tratando de hacer. Bloquear el hilo principal de esta manera es algo muy aficionado, por lo que es difícil creer que los ingenieros de Apple hubieran cometido este tipo de error. De todos modos, espero que puedan arreglar esto pronto.


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

1) Almacene todas sus celdas en una NSMapTable (de ahora en adelante, solo accederá a un objeto de celda desde 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) Agregue este método a su subclase 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 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.sharedAppDelegate.assetsFetchResults[indexPath.item]]; [[NSOperationQueue mainQueue] addOperation:invOp]; }); return (AssetPickerCollectionViewCell *)[self.cellCache objectForKey:indexPath]; }

Por cierto, así es como llenaría 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 videos que son locales (y no en iCloud), que es lo que supongo, ya que está buscando un desplazamiento suave:

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


Construya su AVPlayerItem en una cola en segundo plano tanto como sea posible (algunas operaciones que tiene que hacer en el hilo principal, pero puede realizar operaciones de configuración y esperar que las propiedades de video se carguen en las colas en segundo plano; lea los documentos con mucho cuidado). Esto implica bailes vudú con KVO y realmente no es divertido.

El hipo ocurre mientras el AVPlayer está esperando que el estado del AVPlayerItemStatusReadyToPlay convierta en AVPlayerItemStatusReadyToPlay . Para reducir la duración del problema, desea hacer todo lo posible para acercar el AVPlayerItem a AVPlayerItemStatusReadyToPlay en un subproceso en segundo plano antes de asignarlo al AVPlayer .

Ha pasado un tiempo desde que implementé esto, pero IIRC los bloques de subprocesos principales se deben a que las AVURLAsset subyacentes del AVURLAsset están cargadas de forma diferida, y si no las carga usted mismo, se cargan ocupadas en el subproceso principal cuando el AVPlayer quiere jugar.

Consulte la documentación de AVAsset, especialmente lo relacionado con AVAsynchronousKeyValueLoading . Creo que necesitábamos cargar los valores de duration y tracks antes de usar el activo en un AVPlayer para minimizar los bloques de subprocesos principales. Es posible que también tengamos que caminar a través de cada una de las pistas y hacer AVAsynchronousKeyValueLoading en cada uno de los segmentos, pero no recuerdo el 100%.


He jugado con todas las respuestas anteriores y descubrí que son verdaderas solo hasta cierto límite.

La forma más fácil y sencilla que me funcionó hasta ahora es que el código que asigna su AVPlayerItem a su instancia de AVPlayer en un hilo de fondo. Noté que asignar AVPlayerItem al reproductor en el hilo principal (incluso después de que el objeto AVPlayerItem esté listo) siempre AVPlayerItem su rendimiento y velocidad de fotogramas.

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


No sé si esto ayudará, pero aquí hay un código que estoy usando para cargar videos en la cola de fondo que definitivamente ayuda con el bloqueo del hilo principal (Disculpas si no compila 1: 1, abstraje de una base de código más grande I estoy trabajando en):

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 observa AsyncDisplayKit de Facebook (el motor detrás de las fuentes de Facebook e Instagram), puede reproducir videos en su mayor parte en subprocesos en segundo plano utilizando su AVideoNode . Si subnoda eso en un ASDisplayNode y agrega displayNode.view a cualquier vista que esté desplazando (tabla / colección / desplazamiento), puede lograr un desplazamiento perfectamente fluido (solo asegúrese de crear el nodo y los activos y todo eso en un hilo de fondo) . El único problema es cuando se cambia el elemento de video, ya que esto se fuerza en el hilo principal. Si solo tiene unos pocos videos en esa vista en particular, ¡está bien 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) }) })


avplayer crear una vista de alimentación horizontal con avplayer en cada celda.

  1. Almacenamiento en búfer: cree un administrador para que pueda precargar (almacenar en búfer) los videos. La cantidad de AVPlayers que desea almacenar en búfer depende de la experiencia que esté buscando. En mi aplicación administro solo 3 AVPlayers , por lo que un jugador se está jugando ahora y los jugadores anteriores y siguientes se almacenan en el búfer. Todo lo que el administrador de almacenamiento en búfer está haciendo es administrar que el video correcto se esté almacenando en un punto determinado

  2. Celdas reutilizadas: deje que TableView / CollectionView reutilice las celdas en cellForRowAtIndexPath: todo lo que tiene que hacer es después de que dequqe la celda pasarle su reproductor correcto (solo le doy al buffer una indexPath en la celda y él devuelve la correcta)

  3. AVPlayer KVO''s - Cada vez que el administrador de almacenamiento en búfer recibe una llamada para cargar un nuevo video para almacenar en búfer, AVPlayer crea todos sus activos y notificaciones, solo llámalos así:

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

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

Avísame si necesitas ayuda o más explicaciones

Buena suerte :)