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

9
我正在开发一个应用程序,使用AVPlayer和AVPlayerLayer类在UICollectionView单元格上水平滚动显示“n”个全屏视频。我在UIScrollView上启用了分页功能,因为我只需要播放当前位置的播放器,否则“上一个和下一个”单元格的播放器应处于“暂停”状态。
我以URL格式获取视频。因此,最初需要下载并开始播放。所以不需要从URL下载每一个视频。因此,我决定将已下载的视频缓存到本地。所以我下载并将其存储在本地文件中。然后,如果本地存在该视频,则从本地加载视频。
问题:
(a) “播放和暂停”在其相关索引上无法正常工作。最初播放的是索引“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方法的代码。
以下是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"中,我只检查了我的自定义单元格类的全局实例(在全局变量上声明),并暂停该单元格当前的播放器。

但是,"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 didselect函数,我会显示UIView并移动其“Y”位置值(就像呈现视图控制器一样)。
因此,我可以像“Facebook Messenger应用程序中的相机访问视图”一样将该视图向“上和下”方向移动。
当我关闭该视图时,我需要释放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个回答

6

我曾经也遇到过同样的问题。我的解决方法与你有些不同。

让我分步解释一下:

  • I have declared a global instance of the player in the view controller that hold the current video cell player instance.

    @property (nonatomic, strong) InstagramVideoView *sharedVideoPlayer; // Share video player instance
    
  • The video player objects are created for each cell & they also hold the video URL for each video player.

    - (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;
    }
    
  • Each time user tries to scroll the collection cell pause the video from the global instance of the video player. this solves the flickering issue for the cell.

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

  • Last step is to find the current video cell in the center of horizontal collection view & play the video from URL we saved in current video cell.

    -(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
    

最终结果 - enter image description here

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


非常感谢您的提示。但是我需要澄清一下,在哪里需要调用这个方法“[self videoCellFor:collectionView withIndex:indexPath]”?还有,您在上面提到的“//暂停最后播放的视频”,我需要在哪里调用它?能否给我解释一下? - Ram
那么在滚动时我不需要在“cellForItemAtIndexPath”中传递媒体URL了吗?因为我可以看到你已经在“scrollViewDidEndDecelerating”中传递了媒体URL。 - Ram
"[self videoCellFor:collectionView withIndex:indexPath]" 返回一个视频单元格,其中包含视频播放器对象。在“scrollViewDidScroll”中,“//暂停上次播放的视频”,只有当滚动停止并且中心单元格是视频单元格时,我才会播放视频。 - Pawan Rai
当在 scrollViewDidEndDecelerating 中调用 self.sharedVideoPlayer?.playVideo(with: URL(string: videoCell.mediaUrl)) 时,回调会到达 ZOWVideoView 类。在我的情况下,self.videoPlayer 始终为 nil。因此视频无法播放。这个问题可能的根本原因是什么? - pkc456
@pkc456 正如您在这些行的上方所看到的,有 self.sharedVideoPlayer = videoPlayer; 这一行,它确保了视频播放器不是空值。在调用 self.sharedVideoPlayer?.playVideo(with: URL(string: videoCell.mediaUrl)) 之前,请确保 videoPlayer 不是空值。 - Pawan Rai
显示剩余19条评论

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