Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Display multiple videos using AVPlayer and AVPlayerLayer on UICollectionView

I'm working an application displaying "n" number of videos on full screen using AVPlayer & AVPlayerLayer classes on UICollectionView Cell with horizontal scroll. I was set paging enabled on UIScrollView. Because I need to play only current position player otherwise the "Previous & next" cell players should be in "pause" status.

I'm getting videos in url format. So initially I need to download and after that it will start play. So each and every no need to download videos from URL. So I decided to cache that downloaded video on local. So I down load and store it on local file. Then I load video from local If it exist on local file.

Issues :

(a). "Play & pause" were not working properly on their related index. Initially "0" the index was played. When I scroll to next Index at this moment current index player is should be playing, the next index player should be in "Pause" status. It's not syncing correctly.

(b). I can see the previous player video clip until the next player start to play.

(c). All viewed audio playing in background simultaneously as well as when release the player also. (I mean, I can hearing after dismiss some views or viewControllers)

(d) Need to maintain memory leak out problems.

Here below What I tried the way of source,

- (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];
        });
    }];

Here below the KVO method.

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

I pass url from "cellForItemAtIndexPath", I pause current player on "didEndDisplayingCell".

Then In "scrollViewDidEndDecelerating" I just checked additional global instance of my customCell class(Declared on global variable) and pause that cell current player.

But main purpose on scrollViewDidEndDecelerating, I'm getting current visible cell.

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

One thing I informing here, I just display AVPlayer as a subview of UIView class. When I click on video thumb image (list of published videos info on UITableView on it's previous view)I mean UITable didselect function, I just display the UIView with moving it's "Y" position value (like a present a view controller).

So I can moving that view "Top & down" direction like in "Facebook messanger app camera access view".

So When I dismiss the view, I need to release the AVPlayer, stop audio on background of all viewed videos and remove the registered observer classes like below.

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

Any Idea to solve those issues and playing video smoothly on their current index, stop the audio and handle memory leaks.

Any one share or give some suggestion to achieve this one.

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

I have added all of the class files from ZOWVideoPlayer library file.

Latest Edit:

#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;
}

**In CellForItemAtIndexPath method **

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

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

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

    return cell;

Then In scrollViewDidScroll I was Pause last played videos as you mentioned before.

Then In scrollViewDidEndDecelerating I followed like below answer part except using UICollectionView Instead of IndexedCollectionView class for retrieving page count info.

And finally I was stopped all played videos like below,

  - (void)setPauseTheLastViewedPlayerAudio {

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

            [self.lastPlayedVideo mute];

            [self.lastPlayedVideo stopVideoPlay];
        }

        _videoDetailArray = nil;

        [_videoCollectionView reloadData];
    }

Because I should dismiss the player view moving with finger from Top to down direction using UIPanGestureRecognizer.

like image 958
Ram Avatar asked Jun 17 '17 09:06

Ram


1 Answers

I was struggling with the same issue some time ago. My approach to solving this issue is little different than yours.

let me explain in steps

  • 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
    

End result - enter image description here

Note - For smooth video playing, the player cache the video in temp memory. let me know if you have issue in any of the following steps.

like image 108
Pawan Rai Avatar answered Nov 08 '22 03:11

Pawan Rai