web-dev-qa-db-ja.com

AVPlayerを使用しているときの良好なスクロールパフォーマンスの維持

コレクションビューがあり、コレクションビューのセルにビデオを含めることができるアプリケーションに取り組んでいます。現在、AVPlayerAVPlayerLayerを使用してビデオを表示しています。残念ながら、スクロールのパフォーマンスはひどいです。 AVPlayerAVPlayerItem、およびAVPlayerLayerは、メインスレッドで多くの作業を行っているようです。メインスレッドをブロックし、深刻なフレームドロップを引き起こしているロックを常に取り出し、セマフォを待機しています。

メインスレッドでこれほど多くのことをやめるようにAVPlayerに指示する方法はありますか?これまでのところ、私が試したことは何も問題を解決していません。

また、AVSampleBufferDisplayLayerを使用して簡単なビデオプレーヤーを構築してみました。それを使用して、すべてがメインスレッドで発生することを確認でき、ビデオのスクロールと再生中に最大60fpsを達成できます。残念ながら、この方法ははるかに低いレベルであり、オーディオ再生や箱から出してスクラブするようなものは提供しません。 AVPlayerで同様のパフォーマンスを得る方法はありますか?むしろそれを使いたいです。

編集:これをさらに調べた後、AVPlayerを使用しているときに、良好なスクロールパフォーマンスを達成できるようには見えません。 AVPlayerを作成してAVPlayerItemインスタンスに関連付けると、一連の作業が開始され、メインスレッドでトランポリンがメインスレッドでセマフォを待機し、一連のロックの取得を試みます。これがメインスレッドを停止させる時間は、スクロールビュー内のビデオの数が増加するにつれて非常に劇的に増加します。

AVPlayer deallocも大きな問題のようです。 AVPlayerの割り当て解除も、多くのものの同期を試みます。繰り返しますが、より多くのプレーヤーを作成すると、これは非常に悪くなります。

これは非常に憂鬱であり、AVPlayerが私がしようとしていることに対してほとんど使用できなくなります。このようにメインスレッドをブロックするのは非常にアマチュアなことなので、Appleエンジニアがこの種の間違いを犯したとは思えません。とにかく、すぐに修正できることを願っています。

56
Antonio

バックグラウンドキューでAVPlayerItemを可能な限り構築します(メインスレッドで実行する必要がある操作もありますが、セットアップ操作を実行して、ビデオプロパティがバックグラウンドキューにロードされるのを待つことができます-ドキュメントを注意深く読んでください)。これには、KVOを使用したブードゥーダンスが含まれますが、実際には楽しくありません。

AVPlayerAVPlayerItemsステータスがAVPlayerItemStatusReadyToPlayになるのを待っている間にしゃっくりが起こります。しゃっくりの長さを短くするには、AVPlayerItemに割り当てる前に、バックグラウンドスレッドでAVPlayerItemStatusReadyToPlayAVPlayerに近づけるようにしてください。

これを実際に実装してからしばらく経ちましたが、IIRCのメインスレッドブロックは、基になるAVURLAssetのプロパティが遅延ロードされるために発生し、自分でロードしない場合、ビジーロードされますAVPlayerが再生したいときのメインスレッド。

AVAssetのドキュメント、特にAVAsynchronousKeyValueLoadingに関連するものをチェックしてください。メインスレッドブロックを最小化するためにdurationでアセットを使用する前に、tracksおよびAVPlayerの値をロードする必要があると思います。各トラックを歩いて、各セグメントでAVAsynchronousKeyValueLoadingを実行する必要もありましたが、100%覚えていません。

20
damian

これが役立つかどうかわからない-ただし、メインスレッドブロッキングに間違いなく役立つバックグラウンドキューにビデオをロードするために使用しているコードがあります(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
}
14
Andy Poes

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

上記のすべての答えをいじってみたところ、特定の制限に対してのみ真実であることがわかりました。

これまでのところ、最も簡単で簡単な方法は、バックグラウンドスレッドでAVPlayerItemAVPlayerインスタンスに割り当てるコードです。メインスレッドのプレーヤーに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)
}
0
melaka

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];
}
0
James Bush