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.
-
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 3AVPlayers
, 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 -
Celdas reutilizadas: deje que
TableView
/CollectionView
reutilice las celdas encellForRowAtIndexPath:
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) -
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 :)