40

I have a UITableView containing a number of videos to play when scrolling. As the cells in the tableView are being re-used, I only instantiate one AVPlayer for each row, ever. When a cell is re-used I simply change the PlayerItem of the cell's player by calling [self.player replaceCurrentItemWithPlayerItem:newItem];. This is currently being indirectly called inside tableView:cellForRowAtIndexPath. When scrolling down, there is a noticeable lag when the re-using occurs. With a process of elimination, I have concluded that the lag is caused by replaceCurrentItemWithPlayerItem, before it's even starting to play. When removing this single line of code (preventing the player from getting a new video) the lagging disappears.

What I've tried to fix it:

I have a custom UITableViewCell for playing these videos, and I've created a method inside these to initialize with the new information from an object. I.E, in cellForRowAtIndexPath: i call [cell initializeNewObject:newObject]; to perform the following method:

//In CustomCell.m
-(void)initializeNewObject:(CustomObject*)newObject
{ /*...*/
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        AVPlayerItem *xPlayerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:newObject.url]];
        AVPlayer *dummy = self.player;
        [dummy replaceCurrentItemWithPlayerItem:xPlayerItem];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.player = dummy;
            playerItem = xPlayerItem;
        }
    }/*...*/
}

When running this, I get the same result as if I completely removed the call for replacing the item. Apparently, this function can't be threaded. I'm not completely sure what I expected from this. I would imagine I need a clean copy of the AVPlayer for this to work, but after searching a bit, I found several comments stating that replaceCurrentItemWithPlayerItem: can not be called in a separate thread, which makes no sense to me. I know that UI-elements should never be handled in other threads than main/UI-thread, but I would never imagine replaceCurrentItemWithPlayerItem to fall under this category.

I'm now looking for a way to change the item of an AVPlayer without the lag, but can't find any. I'm hoping I've understood the threading of this function wrong, and that someone would correct me..

EDIT: I've now been informed that this call is already threaded, and that this shouldn't really happen. However, I see no other explanation.. Below is my cellForRowAtIndexPath:. It is inside a custom UITableView with delegates set to self (so self == tableView)

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CustomCell *cell = [self dequeueReusableCellWithIdentifier:kCellIdentifier];
    if(!cell)
        cell = [[[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil] objectAtIndex:0];

    //Array 'data' contains all objects to be shown. The objects each have titles, url's etc.
    CustomVideoObject *currentObject = [data objectAtIndex:indexPath.row];
    //When a cell later starts playing, I store a pointer to the cell in this tableView named 'playing'
    //After a quick scroll, the dequeueing cell might be the cell currently playing - resetting
    if(playing == cell)
        playing = nil;
    //This call will insert the correct URL, title, etc for the new video, in the custom cell
    [cell initializeNewObject:currentObject];
    return cell;
}

The initializeNewObject that is currently "working", however lagging (this is inside CustomCell.m:

-(void)initializeNewObject:(CustomObject*)o
{
    //If this cell is being dequeued/re-used, its player might still be playing the old file
    [self.player pause];
    self.currentObject = o;
    /*
    //Setting text-variables for title etc. Removed from this post, but by commenting them out in the code, nothing improves.
    */

    //Replace the old playerItem in the cell's player
    NSURL *url = [NSURL URLWithString:self.currentObject.url];
    AVAsset *newAsset = [AVAsset assetWithURL:url];
    AVPlayerItem *newItem = [AVPlayerItem playerItemWithAsset:newAsset];
    [self.player replaceCurrentItemWithPlayerItem:newItem];

    //The last line above, replaceCurrentItemWithPlayerItem:, is the 'bad guy'.
    //When commenting that one out, all lag is gone (of course, no videos will be playing either)
    //The lag still occurs even if I never call [self.player play], which leads me
    //to believe that nothing after this function can cause the lag

    self.isPlaying = NO;
}

The lag is happening at the exact same place each time. When scrolling fast down we can see that the short lag happens when the top part of the bottom-most cell reaches the center of the screen. I guess this doesn't mean anything to you, but it is clear that the lag is happening at the exact same place every time, namely when a new cell is dequeueing. After changing the playerItem of the AVPlayer, I do nothing. I do not start to play the video until later. replaceCurrentItemWithPlayerItem: is causing this noticeable lag. The documentation states that it's changing the item in another thread, but something in that method is holding up my UI.

I was told to use Time Profiler in Instruments to find out what it is, but I have no idea how to do that. By running the profiler, and scrolling once in a while, this is the result(image). The peaks of each graph-group is the lag I am talking about. The one single extreme peak is (I think) when I had scrolled to the bottom and tapped the status bar to scroll to the top. I do not know how to interpret the result. I searched the stack for replace, and found the bastard. It's called here from within Main Thread (at the top), which makes sense, with a "running time" of 50ms, which I have no idea to think of. The relevant proportion of the graph is of a timespan of about 1 minute and a half. In comparison, when commenting out that single line and running Time Profiler again, the graph's peaks are significantly lower (Highest peak at 13%, compared to what's on the image, which is probably around 60-70%).

I don't know what to look for..

Sti
  • 8,275
  • 9
  • 62
  • 124
  • 3
    I have the same issue. Have you fixed it somehow? – user1561346 Jan 11 '15 at 18:20
  • 2
    @user1561346 No.. :/ I decided to only start playback when the scrolling had stopped instead. That way I can scroll past many videos (with correct thumbnails) without the video ever being loaded, and only loads when scrolling stops over it. Not really what I wanted, but better than the lag. Let me know if you find out anything! – Sti Jan 11 '15 at 19:25
  • @Sti - how do you get and set video thumbnail? – Robert Constantinescu Oct 29 '15 at 15:32
  • 1
    Not sure that replaceCurrentItemWithPlayerItem by itself is the issue. I've tried retaining all AVPlayers (1 per cell) and simply swapping the AVPlayerLayer's player for each cell. So, never calling replaceCurrentItemWithPlayerItem as each AVPlayer is only given one AVPlayerItem. The lag is still there and occurs even when replacing the AVPlayerLayer's player to another existing AVPlayer. – William Rust Nov 19 '15 at 20:11
  • @Sti Did you find any solution for this? I am also stuck with same problem. Can you help. – Ajay Kumar Jan 24 '17 at 05:01
  • @AjayKumar I never found out. As I said in the comment above, I ended up starting each video when scrolling stops. – Sti Jan 24 '17 at 07:54
  • Did You try using several players so You can preload videos more easily? And maybe I missed, whats the size of the cell, is it fullscreen? – Georgi Boyadzhiev Feb 07 '17 at 20:10
  • As far as I know `replaceCurrentItemWithPlayerItem` is already decoding the video and shows the first frame even when you are not playing the item. This seems to happen on the main thread. My approach for this was to only use a single AVPlayer and attaching its output to the cells playerLayers while also making the table/collectionview super performant (rasterizing, no corner radius, minimal offscreen rendering). Some things in the UIKit world just have to happen on the main thread so you have to try to keep it free (in the end I switched to ASDK) – fruitcoder Mar 28 '17 at 15:36
  • How you are setting ur reuseIdentifier for cell??? you are trying to get it by reuseidentifier but not setting here? – Surbhi Garg May 17 '17 at 12:13
  • How many rows on a screen for your case???because for each row intialliase method will be called, May be when you scroll, this method is called for more than one rows, and pausing and replacing occurs multiple times. – Surbhi Garg May 17 '17 at 12:15

3 Answers3

1

If you do some basic level of profiling, I think you can narrow down the problem. For me, I had a similar issue where replaceCurrentItemWithPlayerItem was blocking the UI thread. I resolved it by examining my code to find out which line was taking time. For me the AVAsset loading was taking time. So I used the loadValuesAsynchronouslyForKeys method of AVAsset to resolve my issue.

Hence can you try the following:

-(void)initializeNewObject:(CustomObject*)o
{
    //If this cell is being dequeued/re-used, its player might still be playing the old file
    [self.player pause];
    self.currentObject = o;
    /*
    //Setting text-variables for title etc. Removed from this post, but by commenting them out in the code, nothing improves.
    */

    //Replace the old playerItem in the cell's player
    NSURL *url = [NSURL URLWithString:self.currentObject.url];
    AVAsset *newAsset = [AVAsset assetWithURL:url];
    [newAsset loadValuesAsynchronouslyForKeys:@[@"duration"] completionHandler:^{
        AVPlayerItem *newItem = [AVPlayerItem playerItemWithAsset:newAsset];
        [self.player replaceCurrentItemWithPlayerItem:newItem];
    }];


    //The last line above, replaceCurrentItemWithPlayerItem:, is the 'bad guy'.
    //When commenting that one out, all lag is gone (of course, no videos will be playing either)
    //The lag still occurs even if I never call [self.player play], which leads me
    //to believe that nothing after this function can cause the lag

    self.isPlaying = NO;
}
manishg
  • 9,520
  • 1
  • 16
  • 19
  • Thanks for your reply, however, if you read what I noted in the bounty tooltip "Doing KVO on the AVAsset, AVPlayer, and AVPlayerItem does not help with the lag occured with replaceCurrentItemWithPlayerItem. A solution for doing this smoothly or an alternative method is welcomed." I have tried all the above mentioned and even further, it doesnt help tho :( –  Feb 08 '17 at 15:32
0

In your CustomCell.m you should use thumbnails to display an image for the specific cell video, you could add the thumbnail to a button inside your cell, then build a custom delegate in CustomCell.h that will be called when you tap on the newly created button something like:

@class CustomCell;

@protocol CustomCellDelegate <NSObject>

- (void)userTappedPlayButtonOnCell:(CustomCell *)cell;

@end

also in your your .h add the action for the button:

@interface CustomCell : UITableViewCell

@property (strong, nonatomic) IBOutlet UIButton *playVideoButton;

 - (IBAction)didTappedPlayVideo;
 - (void)settupVideoWithURL:(NSString *)stringURL;
 - (void)removeVideoAndShowPlaceholderImage:(UIImage *)placeholder;

@end

also in "didTappedPlayVideo" you do the next:

- (void)didTappedPlayVideo{

[self.delegate userTappedPlayButtonOnCell:self];

}

the method: settupVideoWithURL is used to init you player and the method: removeVideoAndShowPlaceholderImagewill stop the video and make the AVPlayerItem and AVPlayer nil.

Now when the user taps a cell you send the call back to the UIViewController where you have the tableView, and in the delegate method you should do the next:

    - (void)userTappedPlayButtonOnCell:(CustomCell *)cell{

    //check if this is the first time when the user plays a video
    if(self.previousPlayedVideoIndexPath){
          CustomCell *previousCell = (CustomCell *)[tableView cellForRowAtIndexPath:self.previousPlayedVideoIndexPath];
          [previousCell removeVideoAndShowPlaceholderImage: [UIImage imageNamed:@"img.png"]];
    }
    [cell settupVideoWithURL:@"YOURvideoURL"];
    self.previousPlayedVideoIndexPath = cell.indexPath;
    }

P.S.:

On older devices(almost all of them until the new iPad Pro) is not indicated to have multiple instances of AVPlayer. Also hope that this chunks of code can guide or help you in your quest :)

Laur Stefan
  • 1,519
  • 5
  • 23
  • 50
-1

You should profile your app using the time profiler to see where the lagging is actually occurring.

The documentation for replaceCurrentItemWithPlayerItem: clearly states that it executes asynchronously, so it shouldn't be the source of your main queue jumpiness.

The item replacement occurs asynchronously; observe the currentItem property to find out when the replacement will/did occur.

Documentation here

If you still can't figure it out, post more code, and in particular at least your cellForRowAtIndexPath method.

UPDATE

As per @joey's comment below, the previous content of this answer is no longer valid. The documentation no longer states that the method is asynchronous, so it may not be.

Dima
  • 23,484
  • 6
  • 56
  • 83
  • Thank you! However `replaceCurrentItemWithPlayerItem` is the only logical explanation for my lag for me.. I can't seem to find it. See updated question for more! (with link to picture) – Sti Aug 18 '14 at 16:41
  • Try that again and check `Invert Call Tree` on the left side and get rid of the filter you have on the word "replace". – Dima Aug 18 '14 at 17:13
  • I did that now, and limited the result in 'time' to only reflect the one second when scrolling. I'm not sure how to interpret this. Does it say that 'something' took 10ms to resolve? 10ms doesn't sound like much, but I guess it's noticeable when the entire screen is scrolling and suddenly stopping for any period of time longer than 1 FPS-unit. The result: http://i.imgur.com/YPmPazv.png I expanded the topmost item `objc_msgSend`. By further expanding the `[AVPlayerItem..]`-line within, I get back to Main Thread through the same way as shown in the original image. All of them 1ms/1.1%.. – Sti Aug 18 '14 at 18:44
  • @Dima your link is no longer valid. The documentation here: https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVPlayer_Class/#//apple_ref/occ/instm/AVPlayer/replaceCurrentItemWithPlayerItem: says "The item replacement occurs immediately, the item becomes the player’s currentItem." and suggests that the replacement is NOT asynchronous. – jjjjjjjj Mar 31 '16 at 07:07
  • @Dima I see you are active answering questions on SO. I awarded you the bounty since I didnt get any solutions on this one. –  Feb 09 '17 at 18:12