Display multiple videos using AVPlayer and AVPlayerLayer on UICollectionView
Asked Answered
F

1

9

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.

Fiertz answered 17/6, 2017 at 9:59 Comment(0)
I
6

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.

Interregnum answered 17/6, 2017 at 12:16 Comment(24)
Thanks a lot of your Tips. But one clarification where I need to call this method "[self videoCellFor:collectionView withIndex:indexPath]". And also this one "// Pause last played videos" you have mentioned on above where I need to call it. Could you explain me.Fiertz
So I don't need to pass media url on "cellForItemAtIndexPath" during the scroll ? because I can see you're passed media url on "scrollViewDidEndDecelerating".Fiertz
"[self videoCellFor:collectionView withIndex:indexPath]" return a video cell, which has the video player object in it. "// Pause last played videos" in "scrollViewDidScroll", I am playing video only when scroll stops scrolling & center cell is a video cell.Interregnum
Could you share your all player related class ? I will modify related to my requirement.Fiertz
Code is from private from a private projet, that is still in development. for right now i cant share code with you. you should try to implement it by your self, let me know if you need help at any points. this is the video player library i am using in my project for video caching ZOWVideoPlayerInterregnum
thanks a lot. I will try to show the videos using the above video caching library ZOWVideoPlayer. Once it's finished let me know you.Fiertz
Did you set "InstagramVideoView" as a subview to "CommonVideoCollectionViewCell" contentView with constraint ? because why I'm asking this I have set like above, I can see only empty player because this view (ZOWVideoPlayerLayerContainerView) frame was not updating. everything is 0.Fiertz
"InstagramVideoView" is just an outlet with constraints, I have added the code for "CommonViewCollectionViewCell" in the answer. nothing goes inside implementation of "CommonViewCollectionViewCell".Interregnum
Thanks for your above tip. I really want to know, this line "[self videoCellFor:collectionView withIndex:indexPath]", Where you called it. I kow it's returned video cell. but I can't assume with Where I used this liay I know what you have managed?Fiertz
could you see my latest edition on my question like in the line below "cellForItemAtIndexPath". I was passing current index url to AVPlayer (defined on UICollectionCell class). That's why I'm asking where I need to call "videoCellFor:" method. Kindly explain with detail it's maybe help full for me.Fiertz
@Fiertz i have added code for "videoCellFor". nothing special this function does. its simply return the video cell object. attach the code for "loadVideoURL", lets see if you are doing something wrong there.Interregnum
okay sure. I know that method returns video cell as I already informed on my latest comment. But try to understand, I need where I can call that return type method or In which method I should call like this "CommonVideoCollectionViewCell *cell = [self videoCellFor:collectionView withIndex:indexPath];" this is my question. I hope this time yow will understand my requirement.Fiertz
@Fiertz following function is called from "cellForItemAtIndexPath" method. see the edit.Interregnum
thanks for your response. Yes I assume you would be called that function on "cellForItemAtIndexPath", but my assumption is 50 % only. I think may be you called that method your customization function. So that's why I'm asking that. But better way, earlier I was asked like "I need to call that video cell return method on cellForItem" for your clearance. revert it that's my mistake.Fiertz
How to manage this class or what I need to add on this class "IndexedCollectionView" ?. I need to add collectionView for each and every index ? Kindly explain me.Fiertz
How to manage to show the first index video should be play ? because right now, when I was open the player view the first index video was not playing well. I can see like this response from "Console log" - "stoncle debug : attempt to play video from nil video player". But When I scroll to see the next videos, I can see it. the player was working fine. How to handle this scenario ? kindly explain me. Note : Could you check my "Latest Edit" section What I have modified it.Fiertz
now everything working perfect except one, that is I scroll to see next video, I can hear already viewed video audio only during the new video download progress. After download finished it played the downloaded video well. I have followed your instructions like "lastPlayed video pause and mute" on "ScrollDidScroll" delegate. What I need to rectify that situation? Kindly explain me.Fiertz
I can see already viewed AVPlayerItem, when I see the new one. How to remove this player layer ?Fiertz
For this, I'm using a placeholder image for video cell. you can see in the gif that as soon user start scrolling, the global player pause the current video and when scroll ends deaccelerating, collection view reloads replace the video view with the placeholder image.Interregnum
@pawan, Is it possible to make the InstagramVideoView to full screen and then again minimise it on cell again?Bagpipes
@Bagpipes yes it's quite possible. on click of cell open a controller with the current player like video URL, the current time(so it seems like resume from where you clicked for zooming). you can customize view controller presentation to make it appear like a video zoom button click. that's how I have done it.Interregnum
while self.sharedVideoPlayer?.playVideo(with: URL(string: videoCell.mediaUrl)) at scrollViewDidEndDecelerating, the callback goes to ZOWVideoView class. Here self.videoPlayer is nil always in my case. Thus video is not playing. What can be the possible root cause of this problemBagpipes
@Bagpipes As you can see just above these line, there is self.sharedVideoPlayer = videoPlayer; which make sure the videoplayer is not nil. do check if videoPlayer is not nil before calling self.sharedVideoPlayer?.playVideo(with: URL(string: videoCell.mediaUrl)) line.Interregnum
Can anyone suggest me here: #54745859Nitza

© 2022 - 2024 — McMap. All rights reserved.