【问题标题】:Maintaining good scroll performance when using AVPlayer使用 AVPlayer 时保持良好的滚动性能
【发布时间】:2015-08-02 12:24:28
【问题描述】:

我正在开发一个应用程序,其中有一个集合视图,集合视图的单元格可以包含视频。现在我正在使用AVPlayerAVPlayerLayer 显示视频。不幸的是,滚动性能很糟糕。似乎AVPlayerAVPlayerItemAVPlayerLayer 在主线程上做了很多工作。他们不断地取出锁,等待信号量等,这阻塞了主线程并导致严重的帧丢失。

有没有办法告诉AVPlayer 停止在主线程上做这么多事情?到目前为止,我尝试过的任何方法都无法解决问题。

我还尝试使用AVSampleBufferDisplayLayer 构建一个简单的视频播放器。使用它我可以确保一切都发生在主线程之外,并且在滚动和播放视频时我可以达到约 60fps。不幸的是,该方法的级别要低得多,并且它不提供开箱即用的音频播放和时间擦除等功能。有什么方法可以与AVPlayer 获得类似的性能?我宁愿使用它。

编辑: 在对此进行了更多研究之后,使用AVPlayer 似乎不可能实现良好的滚动性能。创建AVPlayer 并与AVPlayerItem 实例关联会启动一堆工作,这些工作会跳到主线程上,然后等待信号量并尝试获取一堆锁。随着滚动视图中视频数量的增加,主线程停止的时间会急剧增加。

AVPlayer dealloc 似乎也是个大问题。处理AVPlayer 也尝试同步一堆东西。同样,随着您创建更多玩家,这会变得非常糟糕。

这非常令人沮丧,它使AVPlayer 几乎无法用于我正在尝试做的事情。像这样阻塞主线程是一件很业余的事情,所以很难相信苹果工程师会犯这种错误。不管怎样,希望他们能尽快解决这个问题。

【问题讨论】:

  • "使用 AVPlayer 似乎无法实现良好的滚动性能" 这根本不是这样的。有许多应用程序依靠 AVFoundation 来播放滚动提要中的媒体。 Vine / Facebook / Instagram 都使用 AVPlayer 来播放 feed 中的媒体。它非常棘手,但下面概述的 damian 是一个很好的开始。
  • Vine、Facebook 和 Instagram 在滚动过去的视频时实际上都相当不稳定。所有这些都有一些非常明显的丢帧。 Instagram 的性能最好,但我也很难找到同时播放大量高清长视频的屏幕。看起来他们也没有解决问题,尽管他们都有优秀的工程师和大量的资源。我很确定 AVPlayer 是这里的问题。如果您不相信我,请启动 Instruments 并查看主线程被阻塞的频率。
  • 有什么方法可以告诉 AVPlayer 停止在主线程上做这么多事情吗?到目前为止,我尝试过的任何方法都无法解决问题...使用您自己的上下文创建您自己的调度队列。
  • 对于它的价值,我们能够通过以下方式获得良好的滚动性能:a) 拥有可重复使用的播放器池和播放器层,b) 提前设置播放器(和层)(例如,+/ - 1 个屏幕外视图),以及,最重要的是,c)将播放器层添加到窗口,隐藏,以及提前。 AVPlayerLayer 有很多内部设置,直到可以想象的可见时才会发生;也就是说,它必须存在于一个窗口中,即使它只是用于一个运行循环。这样做,我们在快速添加/删除玩家时几乎不会阻塞主线程。
  • 我不确定我是否对此发表了评论;但是,使用资产读取器加载样本缓冲区,然后在 AVSampleBufferDisplayLayer 中显示样本缓冲区会比拥有多个播放器对象更好。这是因为样本缓冲区是在单独的线程上出售的,Core Animation 也是如此。走这条路可以消除多线程编程中的猜测,并为您提供比您自己提供的更明智的解决方案。

标签: ios video avfoundation avplayer


【解决方案1】:

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

AVPlayer 正在等待AVPlayerItems 状态变为AVPlayerItemStatusReadyToPlay 时,就会出现问题。为了尽可能减少打嗝的长度,您可以在将AVPlayerItem 分配给AVPlayer 之前,在后台线程上使AVPlayerItemStatusReadyToPlay 更接近AVPlayerItemStatusReadyToPlay

自从我真正实现它已经有一段时间了,但是 IIRC 主线程阻塞是因为底层 AVURLAsset 的属性是延迟加载的,如果你不自己加载它们,它们就会被繁忙加载当AVPlayer 想玩时在主线程上。

查看 AVAsset 文档,尤其是 AVAsynchronousKeyValueLoading 周围的内容。我认为我们需要在 AVPlayer 上使用资产之前加载 durationtracks 的值以最小化主线程块。有可能我们还必须遍历每条轨道并在每个段上执行AVAsynchronousKeyValueLoading,但我不记得 100%。

【讨论】:

  • 非常感谢您的提示!您能否详细说明“尽可能在后台队列上构建AVPlayerItem”。我尝试在后台线程上创建AVPlayerItem 并加载其资产的轨道,但其状态始终为 AVPlayerItemStatusUnknown。在它与 AVPlayer 关联之前,它似乎不会转换到 AVPlayerItemStatusReadyToPlay,并且 AVPlayer 连接到 AVPlayerLayer。我确定我在这里做错了什么。
  • @Antonio 嘿,你设法弄清楚了吗?我也在尝试创建这个
  • 嘿@Antonio 我实际上设法使用一些 AVPlayer 实例来创建它,它的效果很棒,如果您仍然有兴趣完成它,我将很乐意进一步详细说明
  • 这个答案与Apple的文档相反,其中AVPlayer是在状态变为AVPlayerItemStatusReadyToPlay之前用刚刚创建的AVPlayerItem构造的。事实上,如果我之前没有构建过AVPlayer,我从来没有达到那个状态。
  • 他异步键加载的东西与从文件下载样本缓冲区无关。完全不相关。您不需要做任何与多线程编程相关的工作。如果您确实需要在多个线索和线程之间分解任务,只需使用资产阅读​​器读取样本缓冲区,然后将它们显示在 AVSampleBufferDisplayLayer 中。
【解决方案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
}

【讨论】:

  • 创建AVMutableComposition时为什么要迭代?我看不出时间范围有什么不同。你不重用AVPlayer吗?我正在为滚动表演而苦苦挣扎,所以每一个提示都像是一种祝福:)
  • @JosipB。我只是迭代制作视频循环。你可以看到它是在composition.duration的时候添加的,所以每次迭代,composition.duration都会更新。
  • 很好的答案@AndyPoes!我已经为此烦恼了 10 多个小时......
  • 嗨@AndyPoes,当用户滚动收藏视图时,我正在自动播放视频。播放动作似乎阻塞了 UI,即使它被包装在后台队列中,AVFoundation 也会返回到主线程继续播放动作。你有什么建议,提示来解决这个问题吗? Instagram 具有非常流畅的滚动体验。我真的很想知道他们是怎么做到的。
  • @manonthemoon 发生这种情况的原因有几个:(1) 音频成本高昂。如果您从视频中删除音轨,您会注意到对主线程的点击较少。 (2) 更多兆字节 = 更多处理。更小/更短的视频加载和播放性能更好。 (3) 仅在必要时播放。不要在视频一出现就尝试播放它们。滚动速度的阈值回放。 (4) 暂停视频而不是停止视频。拆除视频的成本可能很高,因此只有在重复使用单元格时才拆除视频。 (5) 释放模式大大提高了播放效果!
【解决方案3】:

我设法在每个单元格中使用avplayer 创建了一个水平提要视图,这样做是这样的:

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

  2. 重用单元格 - 让 TableView / CollectionView 重用 cellForRowAtIndexPath: 中的单元格,你所要做的就是在你 dequqe 单元格传递给他之后,它是正确的播放器(我只是给缓冲一个 indexPath on单元格,他返回正确的单元格)

  3. AVPlayer KVO's - 每次缓冲管理器接到调用以加载新视频以缓冲 AVPlayer 时,都会创建他的所有资产和通知,只需像这样调用它们:

// 播放器

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 - 是您想要添加播放器的视图

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

祝你好运:)

【讨论】:

  • 感谢您这样做,但我真的不明白这将如何真正解决我在 AVFoundation 中观察到的性能问题。你的滚动性能如何?我很确定,每当您将 AVPlayer 与 AVPlayerItem 关联时,您仍然会丢掉一堆帧。不是这样吗?如果可能的话,你能上传一个关于滚动性能的小屏幕录像吗?
  • 我正在使用集合视图中启用的分页,当视频开始播放并且没有发生滚动时,我将所有项目和缓冲播放器的资产关联起来
  • 所以在页面稳定之前您实际上不做任何工作?因为这将解释您如何获得可接受的用户体验。你仍然疯狂地拖延主线程,但没有任何滚动动画可以中断,所以它并不明显。听起来对吗?
  • 哪一部分不准确? 150 毫秒的主线程停顿会导致大量帧丢失,并且会破坏同时发生的任何滚动视图动画。您实际上还没有找到防止这种情况的方法,对吧?
  • 这没什么。这绝对不是什么可以解决任何问题的。你所描述的你所做的并没有反映在那个代码中。
【解决方案4】:

如果您查看 Facebook 的 AsyncDisplayKitFacebook 和 Instagram 供稿背后的引擎),您可以使用他们的 AVideoNode 在后台线程上渲染大部分视频。如果您将其子节点到ASDisplayNode 并将displayNode.view 添加到您正在滚动的任何视图(table/collection/scroll),您可以实现完美平滑的滚动(只需确保它们在后台线程上创建节点和资产以及所有这些)。唯一的问题是更改视频项目时,因为这会强制自己进入主线程。如果您在该特定视图上只有几个视频,则可以使用此方法!

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

【讨论】:

  • 目前,为了使用 ASVideoNode,您需要关闭 AsyncDisplayKit master 分支
  • 从 pod 安装没问题,您只需要在头桥文件中包含 ASVideoNode.h 即可。但@Kevin 是正确的,ASVideoNode 是非常新的,因此尚未得到官方支持。
  • @Gregg 你真的试过这个吗? ASVideoNode 只是 AVPlayerLayer 的包装。这不是魔法,它不会为视频播放带来任何滚动性能优势。
  • 嘿@Antonio,你看过developer.apple.com/library/ios/samplecode/AVPlayerDemo/…吗?它为如何使用 loadValuesAsync 和 KVO 播放视频提供了一个很好的示例。如果您能够在用户不滚动的情况下完成所有主线程任务,我相信您可以实现您的目标。
  • @BhavinKansagara 这正是我所做的,直到某个嫉妒的人删除了它。这是一个视频和源代码的链接:mediafire.com/download/ivecygnlhqxwynr/…youtu.be/7QlaO7WxjGg有人非常嫉妒我在这里的存在。多么可怜。不管怎样,享受代码吧。
【解决方案5】:

这是在 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];
}

【讨论】:

  • 重用不等于唯一。它如何知道要重用什么?那只是重用视图;我的其他对象独立于视图。当您交换视图时,您仍然拥有旧的 AVPlayerItem/AVPlayer/etc。要获得一个新对象,您必须通过将这些对象分配给特定索引处的特定单元格来关联区别。当我不这样做时,滚动很流畅,只有最初加载的单元格会出现,其余的都是它们的副本。
  • 我正在修改我的评论,以更好地回答您关于将正确的视频映射到单元格的问题。简而言之,我不再这样做了。当我确保资产与预期的单元格匹配时,我会使用资产标识符来设置单元格的标签。然后,我只是在继续之前检查以确保单元格标记和资产标识符匹配;这是一个接一个的电话。这可以防止我遇到的问题。我成功使用的另一个解决方案是通过标记号引用单元格中的所有视图。从不叫名字。而且,我每次使用它们时都会对它们进行新的引用。
  • @John 对不起,我没有;但是,翻译应该不会太难。
  • @JamesBush 你能帮我解决这个stackoverflow.com/questions/51787983/… 吗?
【解决方案6】:

我玩弄了上面所有的答案,发现它们只在一定限度内是正确的。

到目前为止,对我来说最简单的方法是在后台线程中将 AVPlayerItem 分配给 AVPlayer 实例的代码。我注意到将AVPlayerItem 分配给主线程上的播放器(即使在AVPlayerItem 对象准备好之后)总是会影响您的性能和帧速率。

斯威夫特 4

例如

let mediaUrl = //your media string
let player = AVPlayer()
let playerItem = AVPlayerItem(url: mediaUrl)

DispatchQueue.global(qos: .default).async {
    player.replaceCurrentItem(with: playerItem)
}

【讨论】:

    猜你喜欢
    • 2012-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-13
    • 1970-01-01
    相关资源
    最近更新 更多