使用AVPlayer时如何保持良好的滚动性能

74

我正在开发一个应用程序,其中有一个 Collection View,Collection View 的单元格可以包含视频。现在我正在使用 AVPlayerAVPlayerLayer 来显示视频。不幸的是,滚动性能非常糟糕。似乎 AVPlayerAVPlayerItemAVPlayerLayer 大部分工作都在主线程上完成。它们不断地锁定、等待信号量等,这会阻塞主线程并导致严重的帧数下降。

有没有办法告诉 AVPlayer 在主线程上做得少一些?到目前为止,我尝试过的所有方法都没有解决问题。

我还尝试使用 AVSampleBufferDisplayLayer 构建一个简单的视频播放器。使用它,我可以确保所有操作都在主线程之外进行,并且在滚动和播放视频时可以实现 ~60fps 的流畅度。不幸的是,该方法要低级得多,而且默认情况下不提供诸如音频播放和时间拖动之类的功能。有没有办法让 AVPlayer 实现类似的性能?我更愿意使用它。

编辑: 经过进一步调查,似乎在使用 AVPlayer 时无法实现良好的滚动性能。创建一个 AVPlayer 并将其与一个 AVPlayerItem 实例关联会触发一堆工作,在主线程上跳跃,并等待信号量和尝试获取一堆锁。这个过程会导致主线程阻塞的时间随着滚动视图中视频数量的增加而急剧增加。

AVPlayer dealloc 也似乎是一个巨大的问题。dealloc 一个 AVPlayer 也会尝试同步一堆东西,随着你创建更多的播放器,情况变得极其糟糕。

这很令人沮丧,也使得AVPlayer几乎无法用于我想要做的事情。像这样阻塞主线程是一件非常业余的事情,难以相信苹果工程师会犯这种错误。无论如何,希望他们能尽快修复这个问题。


3
“使用AVPlayer时,好像无法实现良好的滚动性能。”这并不正确。许多应用程序都依赖于AVFoundation在滚动式信息流中播放媒体。例如Vine / Facebook / Instagram 都使用AVPlayer在信息流中播放媒体。虽然这很棘手,但下面Damian概述的方法是开始实现良好滚动性能的好方法。 - Andy Poes
2
Vine、Facebook和Instagram在滚动视频时都相当卡顿。它们都有明显的帧率下降。Instagram的性能最好,但我也很难找到一个屏幕上同时播放许多长时间、高清晰度视频的场景。看起来它们也没有解决这个问题,尽管它们都拥有优秀的工程师和大量的资源。我相信AVPlayer是这里的问题所在。如果你不相信我,启动Instruments并查看主线程被阻塞的频率。 - Antonio
2
@RomanTruba,很遗憾,使用AVPlayer没有性能解决方案。正如我在我的编辑部分所述,性能问题固有于AVFoundation的实现中,你无法对此做任何事情。如果你想在某种滚动视图中播放视频,并且不想丢失大量帧,则唯一真正的方法是从头开始构建自己的视频播放器,完全绕过AVPlayer。或者,你可以等待iOS 10并希望苹果公司到那时已经解决了这个问题。 - Antonio
1
有没有办法告诉AVPlayer在主线程上停止做那么多事情?到目前为止,我尝试过的一切都没有解决这个问题...可以使用自己的上下文创建自己的调度队列。 - James Bush
3
值得一提的是,我们通过以下方法实现了良好的滚动性能:a)拥有可重用播放器和播放器图层池,b)提前设置播放器(和图层),例如预先设置在视图范围内外1个单位长度,以及最重要的c)预先将播放器图层添加到窗口中并隐藏它。AVPlayerLayer有很多内部设置,在可能可见之前不会发生;也就是说,它必须存在于窗口内,即使只是一个运行循环。通过这样做,我们在快速添加/删除播放器时几乎没有主线程阻塞。 - CIFilter
显示剩余22条评论
6个回答

27

尽可能在后台队列中构建您的AVPlayerItem(有些操作必须在主线程上执行,但您可以在后台队列上执行设置操作和等待视频属性加载 - 仔细阅读文档)。这涉及到使用KVO进行巫术般的舞蹈,并且真的不好玩。

AVPlayer等待AVPlayerItem的状态变为AVPlayerItemStatusReadyToPlay时,会出现中断。为了减少中断的长度,您要尽可能多地在后台线程上将AVPlayerItem接近AVPlayerItemStatusReadyToPlay,然后再将其分配给AVPlayer

实际实现此功能已经有一段时间了,但如果我没记错的话,主线程块是由于懒加载的底层AVURLAsset属性引起的,如果您不自己加载它们,则在AVPlayer想要播放时它们会在主线程上繁忙加载。

查看AVAsset文档,特别是关于AVAsynchronousKeyValueLoading的内容。我认为我们需要在将资产用于AVPlayer之前加载durationtracks的值,以最小化主线程块。可能我们还需要遍历每个曲目并对每个片段进行AVAsynchronousKeyValueLoading,但我不100%确定。


5
非常感谢您提供的建议!您能详细说明一下“尽可能在后台队列上构建 AVPlayerItem”吗?我尝试在后台线程上创建 AVPlayerItem 并加载其资源的轨道,但它的状态始终是 AVPlayerItemStatusUnknown。似乎它只有在与 AVPlayer 相关联,并且该 AVPlayer 连接到 AVPlayerLayer 之后才会转换为 AVPlayerItemStatusReadyToPlay。我相信我在这里做错了什么。 - Antonio
1
嘿@Antonio,我实际上使用几个AVPlayer实例成功创建了这个,它非常好用。如果你仍然有兴趣完成这个项目,我很乐意进一步详细说明。 - YYfim
1
这个答案与苹果的文档相反,其中 AVPlayer 是在状态变为 AVPlayerItemStatusReadyToPlay 之前使用刚创建的 AVPlayerItem 构建的。事实上,在先前构建 AVPlayer 的情况下,我从未达到过那种状态。 - Gobe
1
异步密钥加载的内容与从文件下载示例缓冲区毫无关联。完全不相关。您不需要进行任何与多线程编程有关的工作。如果您真正需要在多个提示和线程之间分配任务,只需使用资产读取器读取样本缓冲区,然后在AVSampleBufferDisplayLayer中显示它们即可。 - James Bush
这绝对是正确的答案。“有可能我们还必须遍历每个轨道,并对每个片段进行AVAsynchronousKeyValueLoading” - 是的。我遇到了一个日志信息:“主线程被同步属性查询阻塞,该属性尚未加载(PreferredTransform),用于HTTP(S)资产。如果该资产从慢速网络读取,这可能是个问题。”通过异步加载资产的preferredTransform,然后是轨道,然后是每个轨道的preferredTransform,问题得到了解决。瞧,苹果的《异步加载媒体数据》。 - undefined
显示剩余9条评论

20

不知道这是否有帮助 - 但这是我用于在后台队列中加载视频的一些代码,这绝对有助于减少主线程阻塞(如果无法一一编译,请见谅,因为我是从我正在工作的一个更大的代码库中抽象出来的):

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
}

你在创建 AVMutableComposition 时为什么要迭代?我没有看到 timeRange 有任何区别。你不会重复使用 AVPlayer 吗?我在努力解决滚动性能问题,所以每一个提示都像是一种祝福 :) - Josip B.
@JosipB。我只是迭代使视频循环播放。您可以看到它在时间组合.持续时间处增加,因此每次迭代时,都会更新组合.持续时间。 - Andy Poes
1
太棒了,AndyPoes!我已经为此苦思冥想十多个小时了... - Tal Zion
1
嗨@AndyPoes,我正在播放自动视频,同时用户滚动集合视图。播放操作似乎会阻塞UI,即使它被包装在后台队列中,AVFoundation也会返回主线程来处理播放操作。你有什么建议或提示来解决这个问题吗?Instagram拥有非常流畅的滚动体验。我真的很想知道他们是如何做到的。 - manonthemoon
1
糟糕的回答。苹果已经为您完成了在后台队列中加载视频的工作,因此不存在这样的事情。即使他们没有这样做,所有这些都只会在一个线程上发出您调用的AVFoundation类方法,而实际的类可能会将它们放回另一个线程。他们并不愚蠢;他们知道加载视频的最有效方法。即使如此,视频仍然一次加载一个样本缓冲区,因此即使您可以在主队列上排队加载操作,UI和其他任何内容也永远不会受到影响,因为您不会一次性加载整个视频...从来没有。 - James Bush
显示剩余3条评论

8
如果您研究Facebook的 AsyncDisplayKit(Facebook和Instagram Feed背后的引擎),您可以使用他们的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)
            })
        })

2
@Gregg 你真的试过这个吗?ASVideoNode只是AVPlayerLayer的包装器。它不是魔法,也不会为视频播放带来任何滚动性能优势。 - Antonio
1
嘿@Antonio,你看过https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Listings/ReadMe_txt.html吗?它提供了一个很好的示例,展示如何使用loadValuesAsync和KVO播放视频。如果您能够在用户不滚动时完成所有主线程任务,我相信您可以实现您的目标。 - chourobin
当Core Animation和Core Media已经为您提供更好的队列/线程选择时,走这条路没有意义。为什么不使用资产阅读器加载示例缓冲区,然后在示例缓冲区显示层中显示它呢?对于这两个方面,Apple已经为您做出了完美的线程和提示选择...而且,您不必使用某人的工具包来使它们工作。 - James Bush
@JamesBush,您能否请提供您博客的直接链接,并附上演示AVPlayer解决方法的示例代码?这将是非常有帮助的。 - Bhavin Kansagara
1
@BhavinKansagara,这正是我所做的,直到有些嫉妒心理的人删除了它。这里有一个视频和源代码链接:http://www.mediafire.com/download/ivecygnlhqxwynr/VideoWallCollectionView.zip https://youtu.be/7QlaO7WxjGg。有人非常嫉妒我在这里的存在,太可悲了。无论如何,享受这段代码吧。 - James Bush
显示剩余7条评论

0
这是一个关于在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集合填充Photos应用程序中视频文件夹中的所有视频:

// 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];
}

重用并不等同于唯一。它如何知道要重用什么?那只是重用视图;我的其他对象与视图无关。当您交换视图时,仍然保留旧的AVPlayerItem/AVPlayer等。要获取新的AVPlayerItem/AVPlayer等,必须通过将这些对象分配给特定索引处的特定单元格来进行区分。当我没有以这种方式做时,滚动很平滑,只有最初加载的单元格会出现,其余都是它们的副本。 - James Bush
我修改了我的评论,以更好地回答你关于将正确的视频映射到单元格的问题。简而言之,我不再这样做了。当我确保资产与预期单元格匹配时,我使用资产标识符设置单元格的标签。然后,我只需检查单元格标签和资产标识符是否匹配,然后再进行下一步操作;这是一个接一个的调用。这可以避免我曾经遇到的问题。另一个我成功使用的解决方案是通过标记号引用单元格中的所有视图。永远不要用名称引用。并且,每次使用它们时,我总是重新引用它们。 - James Bush
@John 抱歉,我不知道;但是翻译应该不难。 - James Bush
@JamesBush,你能帮我解决这个问题吗?https://dev59.com/463la4cB1Zd3GeqPOo0m? - Rahul Vyas

-1

我成功地使用avplayer在每个单元格中创建了一个类似于水平滚动的视图,方法如下:

  1. 缓冲 - 创建一个管理器,以便您可以预加载(缓冲)视频。您想要缓冲的AVPlayers数量取决于您要寻找的体验。在我的应用程序中,我仅管理3个AVPlayers,因此现在正在播放一个播放器,前一个和后一个播放器正在缓冲。所有缓冲管理器要做的就是在任何给定点上管理正确的视频正在被缓冲

  2. 重复使用单元格 - 让TableView / CollectionViewcellForRowAtIndexPath:中重复使用单元格,您所要做的就是在您取消队列的单元格之后将其传递给它的正确播放器(我只是给缓冲区在单元格上设置了一个indexPath,然后他返回正确的播放器)

  3. AVPlayer KVO's - 每次缓冲管理器接到调用以加载新视频进行缓冲的电话时,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'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];
                                          }
                                      });
                                  }];
    });
});

其中:

videoContainer - 是您想要将播放器添加到的视图

如果您需要任何帮助或更多解释,请告诉我

祝你好运 :)


我在集合视图中使用分页功能,当视频开始播放且没有滚动发生时,我将所有的项目和缓冲播放器的资产关联起来。 - YYfim
2
所以直到页面加载完成之前,您实际上没有做任何工作?因为这可以解释您如何获得可接受的用户体验。虽然您仍在不断地阻塞主线程,但由于没有任何滚动动画来中断,所以并不会被察觉。这听起来正确吗? - Antonio
2
哪一部分不准确?主线程停顿150毫秒会导致很多帧丢失,这将破坏同时发生的任何滚动视图动画。你还没有找到防止这种情况的方法,对吗? - Antonio
Antonio:请看我上面提交的答案;使用那段代码,我可以同时显示高质量的15个视频,而且播放流畅。此外,我还可以轻松滚动到另外15个视频,没有任何卡顿。 - James Bush
1
这并不是什么。这绝对没有解决任何问题的价值。你所描述的并没有反映在那段代码里。 - James Bush
显示剩余3条评论

-1

我已经尝试了以上所有答案,并发现它们只在某种程度上是正确的。

目前为止对我来说最简单和最有效的方法是,在后台线程将代码分配给AVPlayer实例时,将代码分配给AVPlayerItem。 我注意到,即使在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)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接