AVPlayer 사용시 우수한 스크롤 성능 유지
컬렉션보기가 있고 컬렉션보기의 셀에 비디오가 포함될 수있는 응용 프로그램에서 작업 중입니다. 지금은 AVPlayer
및을 사용하여 비디오를 표시 하고 AVPlayerLayer
있습니다. 불행히도 스크롤 성능이 끔찍합니다. 그것은 것 같아 AVPlayer
, AVPlayerItem
그리고 AVPlayerLayer
주 스레드에서 자신의 작품을 많이 할. 그들은 지속적으로 잠금을 해제하고, 주 스레드를 차단하고 심각한 프레임 드롭을 일으키는 세마포어 등을 기다리고 있습니다.
AVPlayer
메인 스레드에서 그렇게 많은 일을 그만두 라고 말할 수 있는 방법이 있습니까? 지금까지 내가 시도한 어떤 것도 문제를 해결하지 못했습니다.
나는 또한 AVSampleBufferDisplayLayer
. 이를 사용하여 모든 것이 메인 스레드에서 발생하는지 확인할 수 있으며 스크롤하고 비디오를 재생하는 동안 ~ 60fps를 달성 할 수 있습니다. 불행히도 그 방법은 훨씬 낮은 수준이며 오디오 재생 및 시간 스크러빙과 같은 기능을 제공하지 않습니다. 비슷한 성능을 얻을 수있는 방법이 AVPlayer
있습니까? 차라리 그것을 사용하고 싶습니다.
편집 : 이것을 자세히 살펴본 후 .NET을 사용할 때 우수한 스크롤 성능을 얻을 수없는 것 같습니다 AVPlayer
. 인스턴스를 생성 AVPlayer
하고 연결 AVPlayerItem
하면 메인 스레드에서 트램폴린을 수행 한 다음 세마포어를 대기하고 많은 잠금을 획득하려고 시도하는 많은 작업이 시작됩니다. 이로 인해 메인 스레드가 지연되는 시간은 스크롤 뷰의 비디오 수가 증가함에 따라 상당히 증가합니다.
AVPlayer
dealloc도 큰 문제인 것 같습니다. Dealloc'ing은 AVPlayer
또한 많은 것을 동기화하려고 시도합니다. 다시 말하지만, 더 많은 플레이어를 만들면 극도로 나빠집니다.
이것은 꽤 우울하고 AVPlayer
내가하려는 일에 거의 사용할 수 없게 만듭니다 . 이와 같이 메인 스레드를 차단하는 것은 매우 아마추어적인 일이므로 Apple 엔지니어가 이런 종류의 실수를했을 것이라고 믿기 어렵습니다. 어쨌든, 곧이 문제를 해결할 수 있기를 바랍니다.
AVPlayerItem
가능한 한 백그라운드 큐에서 빌드하십시오 (일부 작업은 메인 스레드에서 수행해야하지만 설정 작업을 수행하고 백그라운드 큐에서 비디오 속성이로드되기를 기다릴 수 있습니다. 문서를 매우주의 깊게 읽으십시오). 이것은 KVO와의 부두 춤을 포함하며 정말 재미 있지 않습니다.
딸꾹질은가 s 상태가 될 AVPlayer
때까지 기다리는 동안 발생합니다 . 육성하는만큼 당신이 할 수있는 당신이 원하는 딸꾹질의 길이를 줄이려면 가까이에 받는 사람을 지정하기 전에 백그라운드 스레드에 .AVPlayerItem
AVPlayerItemStatusReadyToPlay
AVPlayerItem
AVPlayerItemStatusReadyToPlay
AVPlayer
실제로 이것을 구현 한 지 오래되었지만 기본 스레드 블록이 기본 스레드 블록이 발생하는 이유는 기본 AVURLAsset
의 속성이 지연로드 되기 때문이며 직접로드하지 않으면 기본 스레드에서로드됩니다. AVPlayer
놀고 싶어.
AVAsset 문서, 특히 AVAsynchronousKeyValueLoading
. 나는 우리가 값을 읽어들이는 데 필요한 생각 duration
하고 tracks
온 자산을 사용하기 전에 AVPlayer
메인 스레드 블록을 최소화 할 수 있습니다. 우리가 각 트랙을 걷거나 각 AVAsynchronousKeyValueLoading
구간을 밟아야 할 수도 있지만 100 %는 기억 나지 않습니다.
이것이 도움이 될지 모르겠습니다.하지만 메인 스레드 차단에 확실히 도움이되는 백그라운드 큐에 비디오를로드하는 데 사용하는 코드가 있습니다 (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
}
Facebook의 AsyncDisplayKit (Facebook 및 Instagram 피드의 엔진)을 살펴보면 AVideoNode를 사용하여 백그라운드 스레드에서 대부분의 비디오를 렌더링 할 수 있습니다 . ASDisplayNode에 하위 노드를 지정하고 스크롤하는 뷰 (테이블 / 컬렉션 / 스크롤)에 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)
})
})
다음은 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];
}
나는 위의 모든 답변을 가지고 놀았고 그들이 특정 한계에 대해서만 사실이라는 것을 알았습니다.
Easiest and the simplest way that worked for me so far is that the code you assign your AVPlayerItem
to your AVPlayer
instance in a background thread. I noticed that assigning the AVPlayerItem
to the player on the main thread (even after AVPlayerItem
object is ready) always takes a toll on your performance and frame rate.
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)
}
I manage to create a horizontal feed like view with avplayer
in each cell did it like so:
Buffering - create a manager so you can preload (buffer) the videos. The amount of
AVPlayers
you want to buffer depends on the experience you are looking for. In my app i manage only 3AVPlayers
, so one player is being played now and the previous & next players are being buffered. All the buffering manager is doing is managing that the correct video is being buffered at any given pointReused cells - Let the
TableView
/CollectionView
reuse the cells incellForRowAtIndexPath:
all you have to do is after you dequqe the cell pass him it's correct player (i just give the buffering an indexPath on the cell and he returns the correct one)AVPlayer
KVO's - Every time the buffering manager gets a call to load a new video to buffer the AVPlayer create all of his assets and notifications, just call them like so:
// 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'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];
}
});
}];
});
});
where: videoContainer - is the view you want to add the player to
Let me know if you need any help or more explanations
Good luck :)
ReferenceURL : https://stackoverflow.com/questions/30363502/maintaining-good-scroll-performance-when-using-avplayer
'Development Tip' 카테고리의 다른 글
페이지 매김을위한 ProgressBar가있는 Endless RecyclerView (0) | 2020.12.31 |
---|---|
auto x {3}가 initializer_list를 추론하는 이유는 무엇입니까? (0) | 2020.12.31 |
AMP HTML은 무엇이며 프레임 워크 / 도구 X와 어떻게 어울리나요? (0) | 2020.12.31 |
Xcode에서 스토리 보드 파일에 대한 불필요한 편집을 피하는 방법은 무엇입니까? (0) | 2020.12.31 |
언제 개인 클래스를 정적으로 만들고 싶습니까? (0) | 2020.12.31 |