Поддержание хорошей производительности прокрутки при использовании AVPlayer
Я работаю над приложением, где есть представление коллекции, а ячейки представления коллекции могут содержать видео. Сейчас я показываю видео с помощью AVPlayer
и AVPlayerLayer
. К сожалению, производительность прокрутки ужасная. Кажется, что AVPlayer
, AVPlayerItem
и AVPlayerLayer
выполняют большую часть своей работы над основным потоком. Они постоянно вынимают блокировки, ждут семафоров и т.д., Которые блокируют основную нить и вызывают сильные капли кадров.
Есть ли способ сказать AVPlayer
прекратить делать так много вещей в основном потоке? До сих пор ничто из того, что я пробовал, не решило проблему.
Я также попытался создать простой видеоплеер, используя AVSampleBufferDisplayLayer
. Используя это, я могу убедиться, что все происходит с основного потока, и я могу достичь ~ 60 кадров в секунду при прокрутке и воспроизведении видео. К сожалению, этот метод намного ниже, и он не обеспечивает такие функции, как воспроизведение звука и очистка времени из коробки. Есть ли способ получить аналогичную производительность с помощью AVPlayer
? Я бы предпочел использовать это.
Edit:
Посмотрев на это больше, не получается добиться хорошей прокрутки при использовании AVPlayer
. Создание AVPlayer
и связывание с экземпляром AVPlayerItem
запускает кучу работы, которая батутно по основному потоку, где он затем ждет семафоров и пытается получить кучу блокировок. Количество времени, которое это останавливает основной поток, увеличивается довольно резко по мере увеличения количества видеороликов в прокрутке.
AVPlayer
dealloc также кажется огромной проблемой. Dealloc'ing AVPlayer
также пытается синхронизировать кучу вещей. Опять же, это становится очень плохо, поскольку вы создаете больше игроков.
Это довольно удручающе, и он делает AVPlayer
почти непригодным для того, что я пытаюсь сделать. Блокировка основной нити, подобной этой, является такой любительской штукой, что трудно поверить, что инженеры Apple допустили такую ошибку. В любом случае, надеюсь, они могут исправить это в ближайшее время.
Ответы
Ответ 1
Постройте свой AVPlayerItem
в фоновой очереди как можно больше (некоторые операции, которые вы должны выполнять в основном потоке, но вы можете выполнять операции настройки и ждать, пока свойства видео загружаются в фоновых очередях - внимательно прочитайте документы). Это включает в себя Voodoo танцы с KVO и действительно не весело.
Икоты происходят, пока AVPlayer
ожидает, что статус AVPlayerItem
станет AVPlayerItemStatusReadyToPlay
. Чтобы уменьшить длину икоты, вы хотите сделать как можно больше, чтобы AVPlayerItem
приблизиться к AVPlayerItemStatusReadyToPlay
в фоновом потоке, прежде чем назначать его AVPlayer
.
Прошло некоторое время с тех пор, как я на самом деле реализовал это, но IIRC - основные блоки потока, вызванные тем, что базовые свойства AVURLAsset
загружены с лёгкой загрузкой, и если вы не загружаете их самостоятельно, они загружаются на основной поток, когда хочет играть AVPlayer
.
Ознакомьтесь с документацией AVAsset, особенно с файлом вокруг AVAsynchronousKeyValueLoading
. Я думаю, нам нужно было загрузить значения для duration
и tracks
перед использованием актива на AVPlayer
, чтобы свести к минимуму блоки основного потока. Возможно, нам также пришлось пройти через каждую из треков и сделать AVAsynchronousKeyValueLoading
на каждом из сегментов, но я не помню 100%.
Ответ 2
Не знаю, поможет ли это - но вот некоторый код, который я использую для загрузки видео в фоновую очередь, который определенно помогает с блокировкой основного потока (извиняюсь, если он не компилируется 1:1, я абстрагировался от большей базы кода я работаю)
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
}
Ответ 3
Если вы посмотрите в Facebook AsyncDisplayKit (движок за фидами Facebook и Instagram), вы можете отображать видео по большей части в фоновом потоке используя AVideoNode. Если вы подсознаете это в ASDisplayNode и добавьте displayNode.view во все прокрутки, которые вы просматриваете (таблица/коллекция/прокрутка), вы можете добиться совершенно гладкой прокрутки (просто убедитесь, что создайте node и активы и все, что на фоне потока). Единственная проблема заключается в том, чтобы изменить элемент видео, поскольку это заставляет себя на основной поток. Если у вас есть только несколько видеороликов в этом конкретном представлении, вы можете использовать этот метод!
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)
})
})
Ответ 4
Здесь представлено рабочее решение для отображения "видеостены" в UICollectionView:
1) Храните все свои ячейки в NSMapTable (отныне вы будете получать доступ только к объекту ячейки из 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) Добавьте этот метод в свой подкласс 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) Вызовите метод выше из вашего делегата UICollectionView следующим образом:
- (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];
}
Кстати, вот как вы могли бы заполнить коллекцию PHFetchResult всеми видео в папке "Видео" приложения "Фото":
// 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;
}
Если вы хотите отфильтровать видео, которые являются локальными (а не в iCloud), что я бы предположил, видя, что вы ищете гладкую прокрутку:
// 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];
}
Ответ 5
Я поиграл со всеми ответами выше и обнаружил, что они верны только до определенного предела.
Самый простой и простой способ, который до сих пор работал для меня, заключается в том, что код, который вы присваиваете свой AVPlayerItem
своему экземпляру AVPlayer
в фоновом потоке. Я заметил, что назначение AVPlayerItem
плееру в главном потоке (даже после того, как объект AVPlayerItem
готов) всегда влияет на производительность и частоту кадров.
Swift 4
ех.
let mediaUrl = //your media string
let player = AVPlayer()
let playerItem = AVPlayerItem(url: mediaUrl)
DispatchQueue.global(qos: .default).async {
player.replaceCurrentItem(with: playerItem)
}
Ответ 6
Я полагаю, AVQueuePlayer полезен для этой проблемы.
Ответ 7
Мне удалось создать горизонтальный вид, похожий на канал, с avplayer
в каждой ячейке, как это было:
-
Буферизация - создайте менеджера, чтобы вы могли предварительно загрузить (буфер) видео. Количество AVPlayers
, которое вы хотите буферизировать, зависит от опыта, который вы ищете. В моем приложении я управляю только 3 AVPlayers
, поэтому один игрок сейчас играет, а предыдущий и следующий игроки буферизуются. Весь менеджер буферизации выполняет управление тем, что правильное видео буферизуется в любой заданной точке.
-
Повторно используемые ячейки. Пусть TableView
/CollectionView
повторно использует ячейки в cellForRowAtIndexPath:
, все, что вам нужно сделать, - после того, как вы отклоните ячейку, передайте ему правильного игрока (я просто даю буферизацию indexPath на ячейка, и он возвращает правильный)
-
avplayer
KVO - каждый раз, когда менеджер буферизации получает вызов для загрузки нового видео, чтобы буфер AVPlayer создавал все его активы и уведомления, просто назовите их так:
//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 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];
}
});
}];
});
});
где:
videoContainer - это вид, который вы хотите добавить в плеер
Сообщите мне, если вам нужна помощь или дополнительные объяснения
Удачи:)