在 UICollectionView 上使用 AVPlayer 和 AVPlayerLayer 显示多个视频

Posted

技术标签:

【中文标题】在 UICollectionView 上使用 AVPlayer 和 AVPlayerLayer 显示多个视频【英文标题】:Display multiple videos using AVPlayer and AVPlayerLayer on UICollectionView 【发布时间】:2017-06-17 09:59:21 【问题描述】:

我正在使用水平滚动的 UICollectionView Cell 上的 AVPlayer 和 AVPlayerLayer 类在全屏上显示“n”个视频的应用程序。我在 UIScrollView 上设置了分页。因为我只需要播放当前位置的玩家,否则“上一个和下一个”单元格玩家应该处于“暂停”状态。

我正在获取 url 格式的视频。所以最初我需要下载,然后它会开始播放。所以每个人都不需要从 URL 下载视频。所以我决定将下载的视频缓存在本地。所以我下载并将其存储在本地文件中。然后我从本地加载视频,如果它存在于本地文件中。

问题:

(一)。 “播放和暂停”在其相关索引上无法正常工作。最初播放“0”索引。当我此时滚动到下一个索引时,当前索引播放器应该正在播放,下一个索引播放器应该处于“暂停”状态。同步不正确。

(b)。在下一个播放器开始播放之前,我可以看到上一个播放器的视频剪辑。

(c)。所有观看的音频同时在后台播放以及在释放播放器时播放。 (我的意思是,我可以在关闭一些视图或视图控制器后听到)

(d) 需要维护内存泄漏问题。

下面是我尝试的源码方式,

- (void)setVideoLoading:(NSString *)videoUrl

    NSString *urlString = videoUrl;

    SHAREDATA.videoUrl = [NSURL URLWithString:urlString];

    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:SHAREDATA.videoUrl options:nil];

    NSArray *requestedKeys = @[@"playable"];

    //Tells the asset to load the values of any of the specified keys that are not already loaded.
    [asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^

        dispatch_async(dispatch_get_main_queue(), ^

            AVPlayerItem *item = [AVPlayerItem playerItemWithAsset: asset];

            if (self.avPlayer)
            
                /* Remove existing player item key value observers and notifications. */

                [self.avPlayer removeObserver:self forKeyPath:@"status" context:nil];

                [[NSNotificationCenter defaultCenter] removeObserver:self
                                                                name:AVPlayerItemDidPlayToEndTimeNotification
                                                              object:self.avPlayer.currentItem];
            

            self.avPlayer = [AVPlayer playerWithPlayerItem:item];

            self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer];

            if (_typeOfContentMode == UIViewContentModeScaleAspectFit) 

                self.avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
            
            else if (_typeOfContentMode == UIViewContentModeScaleAspectFill)
            
                self.avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
            


            self.avPlayerLayer.frame = CGRectMake(0, 0, _videoBackgroundView.frame.size.width, _videoBackgroundView.frame.size.height);

            [_videoBackgroundView.layer addSublayer:self.avPlayerLayer];

            self.avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;

            self.avPlayer.muted = NO;

            [self.avPlayer addObserver:self
                                forKeyPath:@"status"
                                   options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                                   context:nil];
        );
    ];

这里是KVO方法。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context 

    if (object == self.avPlayer && [keyPath isEqualToString:@"status"]) 

        if (self.avPlayer.status == AVPlayerStatusReadyToPlay) 

            [_videoProgressView setProgress:0 animated:NO];

            [self setVideoPlayEnabled];

            [self setAutoUpdateTimer];

            [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:[self.avPlayer currentItem]];

            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(playerItemDidReachEnd:)
                                                         name:AVPlayerItemDidPlayToEndTimeNotification
                                                       object:[self.avPlayer currentItem]];
        
        else if (self.avPlayer.status == AVPlayerStatusFailed ||
                 self.avPlayer.status == AVPlayerStatusUnknown) 

            [SVProgressHUD dismiss];

            if (self.avPlayer.rate>0 && !self.avPlayer.error)
            
                [self.avPlayer setRate:0.0];

                [self.avPlayer pause];
            
        
    


- (void)playerItemDidReachEnd:(NSNotification *)notification 

    [SVProgressHUD dismiss];

    AVPlayerItem *p = [notification object];

    [p seekToTime:kCMTimeZero completionHandler:^(BOOL finished)
                  
         [self.avPlayer play];
     ];

我从“cellForItemAtIndexPath”传递 url,我在“didEndDisplayingCell”上暂停当前播放器。

然后在“scrollViewDidEndDecelerating”中,我刚刚检查了我的 customCell 类的其他全局实例(在全局变量上声明)并暂停该单元格当前播放器。

但是scrollViewDidEndDecelerating的主要目的是获取当前可见单元格。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

    if (self.lastPlayingCell) 

        [self.lastPlayingCell.avPlayer pause];
    

    // Play/Pause Video
    CGRect visibleRect = (CGRect).origin = self.videoCollectionView.contentOffset, .size = self.videoCollectionView.bounds.size;

    CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));

    _visibleIndexPath = [self.videoCollectionView indexPathForItemAtPoint:visiblePoint];

    DetailsCollectionCell *cell = (DetailsCollectionCell *)[self.videoCollectionView cellForItemAtIndexPath:_visibleIndexPath];

    if (![cell isEqual: self.lastPlayingCell]) 

        [self.avPlayer play];

        self.lastPlayingCell = cell;//assigned to current visible cell to lastplayingcell instance. 
    

我在这里通知一件事,我只是将 AVPlayer 显示为 UIView 类的子视图。当我点击视频缩略图时(UITableView 上的已发布视频信息列表在它的前一个视图上)我的意思是 UITable 没有选择功能,我只是通过移动它的“Y”位置值来显示 UIView(就像一个当前视图控制器)。

所以我可以像在“Facebook messanger 应用程序相机访问视图”中那样移动该视图的“上下”方向。

所以当我关闭视图时,我需要释放 AVPlayer,停止所有观看视频的背景音频,并删除注册的观察者类,如下所示。

- (void)setRemoveRegisteredObserversAndPlayerRelatedElements

    [self.avPlayer removeObserver:self forKeyPath:@"status" context:nil];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:[self.avPlayer currentItem]];

    [self.avPlayerLayer.player pause];

    [self.avPlayer pause];

    self.avPlayer = [AVPlayer playerWithURL:[NSURL URLWithString:@""]];

    [self.avPlayerLayer removeFromSuperlayer];

    self.avPlayerLayer.player = nil;

    self.avPlayerLayer = nil;

    self.avPlayer = nil;

解决这些问题并在当前索引上流畅播放视频、停止音频和处理内存泄漏的任何想法。

任何人分享或提出一些建议来实现这一目标。

cellForItemAtIndexPath

- (UICollectionViewCell *) collectionView: (UICollectionView *) collectionView cellForItemAtIndexPath: (NSIndexPath *) indexPath

    videoDetailsCollectionCell *videoDetailCell = [collectionView dequeueReusableCellWithReuseIdentifier:kVideoDetailsCollectionCell forIndexPath:indexPath];

    NSDictionary *publishedVideoObject = [self.videoDetailArray objectAtIndex:indexPath.row];

    [videoDetailCell loadVideoURL:publishedVideoObject];

    return videoDetailCell;

我已经从 ZOWVideoPlayer 库文件中添加了所有的类文件。

最新编辑:

#pragma mark - Load video url for their current index cell
- (videoDetailsCollectionCell *)videoCellFor:(UICollectionView *)collectionView withIndex:(NSIndexPath *)indexPath

    videoDetailsCollectionCell * videoCell = [collectionView dequeueReusableCellWithReuseIdentifier:kCustomVideoCell forIndexPath:indexPath];

    //[videoCell.videoView.videoPlayer resume];

    return videoCell;

**在 CellForItemAtIndexPath 方法中**

 NSDictionary *publicationObject = [videoDetailArray objectAtIndex:indexPath.row];

    videoDetailsCollectionCell * cell = [self videoCellFor:collectionView withIndex:indexPath];

    cell.mediaUrl = [NSString replaceEmptyStringInsteadOfNull:[NSString stringWithFormat:@"%@",publicationObject.video]];

    return cell;

然后在 scrollViewDidScroll 中,我是 暂停上次播放的视频,正如你之前提到的。

然后在 scrollViewDidEndDecelerating 我按照下面的答案部分,除了使用 UICollectionView 而不是 IndexedCollectionView 类来检索页数信息。

最后我停止了所有播放的视频,如下所示,

  - (void)setPauseTheLastViewedPlayerAudio 

        // Pause last played videos
        if (self.lastPlayedVideo) 

            [self.lastPlayedVideo mute];

            [self.lastPlayedVideo stopVideoPlay];
        

        _videoDetailArray = nil;

        [_videoCollectionView reloadData];
    

因为我应该使用 UIPanGestureRecognizer 关闭用手指从上到下移动的玩家视图。

【问题讨论】:

【参考方案1】:

前段时间我也在为同样的问题苦苦挣扎。 我解决此问题的方法与您的方法略有不同。

让我逐步解释

我已经在视图控制器中声明了一个播放器的全局实例,它包含当前的视频单元播放器实例。

@property (nonatomic, strong) InstagramVideoView *sharedVideoPlayer; // Share video player instance

视频播放器对象是为每个单元格创建的,它们还保存每个视频播放器的视频 URL。

- (CommonVideoCollectionViewCell *)videoCellFor:(UICollectionView *)collectionView withIndex:(NSIndexPath *)indexPath 
    CommonVideoCollectionViewCell *videoCell = [collectionView dequeueReusableCellWithReuseIdentifier:VideoCollectionViewCellIdentifier forIndexPath:indexPath];
    return videoCell;

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

  CommonVideoCollectionViewCell *cell = [self videoCellFor:collectionView withIndex:indexPath];
  cell.mediaUrl = tempMedia.media;
  return cell;

每次用户尝试滚动集合单元格时,都会暂停视频播放器全局实例中的视频。这解决了单元格的闪烁问题。

// Pause last played videos if (self.sharedVideoPlayer) [self.sharedVideoPlayer pause]; [self.sharedVideoPlayer mute];

最后一步是在水平集合视图的中心找到当前视频单元格并从我们保存在当前视频单元格中的 URL 播放视频。

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 
  if ([scrollView isKindOfClass:[UITableView class]]) 
    return;
  

IndexedCollectionView *pageContentCollectionView = (IndexedCollectionView *)scrollView;
CGPoint centerPoint = CGPointMake(pageContentCollectionView.center.x + pageContentCollectionView.contentOffset.x,
                              pageContentCollectionView.center.y + pageContentCollectionView.contentOffset.y);

NSIndexPath *centerCellIndexPath = [pageContentCollectionView indexPathForItemAtPoint:centerPoint];
NSArray *visibleCells = [pageContentCollectionView visibleCells];
// Get cell for index & pause video playback
UICollectionViewCell *cell = [pageContentCollectionView cellForItemAtIndexPath:centerCellIndexPath];

for (UICollectionViewCell *tempCell in visibleCells) 
if ([tempCell isKindOfClass:[CommonVideoCollectionViewCell class]]) 
    CommonVideoCollectionViewCell *videoCell = (CommonVideoCollectionViewCell *)tempCell;
    InstagramVideoView *videoPlayer = [videoCell videoView];
    // Pause & mute all the video player instance
    [videoCell.videoView setHidden:YES];
    [videoCell.placeholderImageView setHidden:NO];
    [videoPlayer pause];
    [videoPlayer mute];
    // Check if the central cell is present
    if (tempCell == cell) 
        self.sharedVideoPlayer = videoPlayer;// Save played video player instance
        [self.sharedVideoPlayer playVideoWithURL:[NSURL URLWithString:[(CommonVideoCollectionViewCell *)tempCell mediaUrl]]];
        [videoCell.videoView setHidden:NO];
        [videoCell.placeholderImageView setHidden:YES];
        [videoPlayer resume]; // resume video playback
        [videoPlayer mute];
    
  
 

CommonVideoCollectionViewCell

#import<UIKit/UIKit.h>
#import "InstagramVideoView.h"
@interface CommonVideoCollectionViewCell : UICollectionViewCell
@property (weak, nonatomic) IBOutlet InstagramVideoView *videoView;
@property (weak, nonatomic) IBOutlet UIImageView *placeholderImageView;
// Set media url
@property (strong, nonatomic) NSString *mediaUrl;
@end

最终结果 -

注意 - 为了流畅地播放视频,播放器会将视频缓存在临时内存中。如果您在以下任何步骤中遇到问题,请告诉我。

【讨论】:

非常感谢您的提示。但是需要澄清一下,我需要将此方法称为“[self videoCellFor:collectionView withIndex:indexPath]”。还有你上面提到的这个“//暂停上次播放的视频”,我需要调用它。你能解释一下吗? 所以我不需要在滚动期间在“cellForItemAtIndexPath”上传递媒体网址?因为我可以看到您在“scrollViewDidEndDecelerating”上传递了媒体网址。 "[self videoCellFor:collectionView withIndex:indexPath]" 返回一个视频单元格,其中包含视频播放器对象。 "// 暂停上次播放的视频" in "scrollViewDidScroll",我只在滚动停止滚动且中心单元格是视频单元格时播放视频。 你能分享一下你所有玩家相关的课程吗?我将根据我的要求进行修改。 代码来自私人项目的私人项目,该项目仍在开发中。现在我不能和你分享代码。您应该尝试自己实现它,如果您在任何时候需要帮助,请告诉我。这是我在项目中用于视频缓存的视频播放器库ZOWVideoPlayer

以上是关于在 UICollectionView 上使用 AVPlayer 和 AVPlayerLayer 显示多个视频的主要内容,如果未能解决你的问题,请参考以下文章

Expo AV 音频无法在 iOS/iPhone 上播放

我可以在 iOS 上使用 AV Foundation Framework 流式传输 .mp3 链接吗?

在 UICollectionView 上使用 AVPlayer 和 AVPlayerLayer 显示多个视频

静态成员 rx 不能在实例类型 UICollectionView 上使用

UICollectionView 在 UICollectionViewCompositionalLayout 上自动滚动

如何使用 UICollectionView 的 selectItem 在 collectionView 单元格上绘图?