コレクションビューがあり、コレクションビューのセルにビデオを含めることができるアプリケーションに取り組んでいます。現在、AVPlayer
とAVPlayerLayer
を使用してビデオを表示しています。残念ながら、スクロールのパフォーマンスはひどいです。 AVPlayer
、AVPlayerItem
、およびAVPlayerLayer
は、メインスレッドで多くの作業を行っているようです。メインスレッドをブロックし、深刻なフレームドロップを引き起こしているロックを常に取り出し、セマフォを待機しています。
メインスレッドでこれほど多くのことをやめるようにAVPlayer
に指示する方法はありますか?これまでのところ、私が試したことは何も問題を解決していません。
また、AVSampleBufferDisplayLayer
を使用して簡単なビデオプレーヤーを構築してみました。それを使用して、すべてがメインスレッドで発生することを確認でき、ビデオのスクロールと再生中に最大60fpsを達成できます。残念ながら、この方法ははるかに低いレベルであり、オーディオ再生や箱から出してスクラブするようなものは提供しません。 AVPlayer
で同様のパフォーマンスを得る方法はありますか?むしろそれを使いたいです。
編集:これをさらに調べた後、AVPlayer
を使用しているときに、良好なスクロールパフォーマンスを達成できるようには見えません。 AVPlayer
を作成してAVPlayerItem
インスタンスに関連付けると、一連の作業が開始され、メインスレッドでトランポリンがメインスレッドでセマフォを待機し、一連のロックの取得を試みます。これがメインスレッドを停止させる時間は、スクロールビュー内のビデオの数が増加するにつれて非常に劇的に増加します。
AVPlayer
deallocも大きな問題のようです。 AVPlayer
の割り当て解除も、多くのものの同期を試みます。繰り返しますが、より多くのプレーヤーを作成すると、これは非常に悪くなります。
これは非常に憂鬱であり、AVPlayer
が私がしようとしていることに対してほとんど使用できなくなります。このようにメインスレッドをブロックするのは非常にアマチュアなことなので、Appleエンジニアがこの種の間違いを犯したとは思えません。とにかく、すぐに修正できることを願っています。
バックグラウンドキューでAVPlayerItem
を可能な限り構築します(メインスレッドで実行する必要がある操作もありますが、セットアップ操作を実行して、ビデオプロパティがバックグラウンドキューにロードされるのを待つことができます-ドキュメントを注意深く読んでください)。これには、KVOを使用したブードゥーダンスが含まれますが、実際には楽しくありません。
AVPlayer
がAVPlayerItem
sステータスがAVPlayerItemStatusReadyToPlay
になるのを待っている間にしゃっくりが起こります。しゃっくりの長さを短くするには、AVPlayerItem
に割り当てる前に、バックグラウンドスレッドでAVPlayerItemStatusReadyToPlay
をAVPlayer
に近づけるようにしてください。
これを実際に実装してからしばらく経ちましたが、IIRCのメインスレッドブロックは、基になるAVURLAsset
のプロパティが遅延ロードされるために発生し、自分でロードしない場合、ビジーロードされますAVPlayer
が再生したいときのメインスレッド。
AVAssetのドキュメント、特にAVAsynchronousKeyValueLoading
に関連するものをチェックしてください。メインスレッドブロックを最小化するためにduration
でアセットを使用する前に、tracks
およびAVPlayer
の値をロードする必要があると思います。各トラックを歩いて、各セグメントでAVAsynchronousKeyValueLoading
を実行する必要もありましたが、100%覚えていません。
これが役立つかどうかわからない-ただし、メインスレッドブロッキングに間違いなく役立つバックグラウンドキューにビデオをロードするために使用しているコードがあります(1対1でコンパイルできない場合はおologiesびします。取り組んでいます):
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
}
Facebookの AsyncDisplayKit (FacebookおよびInstagramフィードの背後にあるエンジン)を調べると、AVideoNodeを使用して、バックグラウンドスレッドで大部分のビデオをレンダリングできます。それをASDisplayNodeにサブノード化し、スクロールするビュー(table/collection/scroll)にdisplayNode.viewを追加すると、完全にスムーズなスクロールを実現できます(作成したことを確認してください)ノードとアセット、およびバックグラウンドスレッド上のすべて)。唯一の問題は、ビデオアイテムを変更する場合です。これにより、メインスレッドに強制されるためです。その特定のビューにいくつかのビデオしかない場合は、この方法を使用しても構いません!
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)
})
})
上記のすべての答えをいじってみたところ、特定の制限に対してのみ真実であることがわかりました。
これまでのところ、最も簡単で簡単な方法は、バックグラウンドスレッドで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)
}
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];
}