Looping video in iOS cause a small pause/flash (MPMoviePlayerController & AVPlayer)

1.4k views Asked by At

I'm really going crazy with my welcome view controller.

I have a video in background in continuos loop but every solution that I used causes a small pause/flash when the video is finished and loop.

I use two solution: MPMoviePlayerController and AVPlayer from AVFoundation but I got the same result, a small white flash when video is looped for replay.

My MPMoviePlayerController solution (I prefer a fix for this)

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"welcome_video" withExtension:@"mp4"];

    self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:videoURL];

    self.moviePlayer.controlStyle = MPMovieControlStyleNone;
    self.moviePlayer.scalingMode = MPMovieScalingModeAspectFill;

    self.moviePlayer.view.frame = self.view.frame;
    [self.view insertSubview:self.moviePlayer.view atIndex:0];

    [self.moviePlayer prepareToPlay];

    // Loop video
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loopVideo) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayer];
}

- (void)loopVideo
{
    [self.moviePlayer play];
}

My AVPlayer solution

 (void)viewDidLoad
{
    [super viewDidLoad];

    NSError *sessionError = nil;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&sessionError];
    [[AVAudioSession sharedInstance] setActive:YES error:&sessionError];

    //Set up player
    NSURL *movieURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"welcome_video" ofType:@"mp4"]];
    AVAsset *avAsset = [AVAsset assetWithURL:movieURL];
    AVPlayerItem *avPlayerItem =[[AVPlayerItem alloc]initWithAsset:avAsset];
    self.avplayer = [[AVPlayer alloc]initWithPlayerItem:avPlayerItem];
    AVPlayerLayer *avPlayerLayer =[AVPlayerLayer playerLayerWithPlayer:self.avplayer];
    [avPlayerLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    [avPlayerLayer setFrame:[[UIScreen mainScreen] bounds]];
    [self.movieView.layer addSublayer:avPlayerLayer];

    //Config player
    [self.avplayer seekToTime:kCMTimeZero];
    [self.avplayer setVolume:0.0f];
    [self.avplayer setActionAtItemEnd:AVPlayerActionAtItemEndNone];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerItemDidReachEnd:)
                                                 name:AVPlayerItemDidPlayToEndTimeNotification
                                               object:[self.avplayer currentItem]];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerStartPlaying)
                                                 name:UIApplicationDidBecomeActiveNotification object:nil];

}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self.avplayer pause];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self.avplayer play];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}

- (void)playerItemDidReachEnd:(NSNotification *)notification 
{
    AVPlayerItem *p = [notification object];
    [p seekToTime:kCMTimeZero];
}

- (void)playerStartPlaying
{
    [self.avplayer play];
}

What's wrong with these implementation? I really try different fixes found on this site but nothing seems to work.

Any suggestions?

1

There are 1 answers

0
Sti On BEST ANSWER

Hm.. I might have an idea for the AVPlayer-approach.

The AVPlayer has this method:

- (void)setRate:(float)rate
           time:(CMTime)itemTime
     atHostTime:(CMTime)hostClockTime

This means that you can specify WHEN you want the AVPlayer to start at kCMTimeZero. I have not actually used it, but I have an idea for how it can work.

You need to know exactly the moment you start the video the first time. I see you have your own -(void)playerStartPlaying which is called by the notificationCenter on didBecomeActive. I suggest using this method for app variations of [player play];, so put

[self playerStartPlaying];

inside viewDidAppear instead of [self.avplayer play];. It might be good enough.

If you here manage to find the device's hostClockTime, and add the length of the video, you should end up with the exact time when you want it to start from scratch. I am not testing any of this, and I'm typing from head, so you need to understand what I'm doing, and fix it yourself.

- (void)playerStartPlaying
{
    //The device's hostClockTime. Basically a number indicating how long the device has been powered on.
    CMTime hostClockTime = CMClockGetHostTimeClock;

    //A CMTime indicating when you want the video to play the next time.
    CMTime nextPlay = CMTimeAdd(hostClockTime, self.avplayer.currentItem.duration);
    /* I don't know if that was correct or not, but you'll find out */

    //Start playing if we're not already playing. There might be an avplayer.isPlaying or something, I don't know, this is probably working as well..
    if(self.avplayer.rate != 1.0)
        [self.avplayer play];

    //Tell the player to restart the video at the correct time.
    [self.avplayer setRate:1.0 time:kCMTimeZero atHostTime:nextPlay];
}

You'll have to remove the entire AVPlayerItemDidPlayToEndTimeNotification-thing. When the video has reached the end, it's already too late. What we're doing now is telling it when to play the second turn when we start the first. We want nothing to happen when didPlayToEndTime is fired, we're handling it manually.

So, if you understand what I have done above, you'll also notice that the video only will play twice. We tell the video to play, at the same time as we tell it to replay at time = now+videoLength. When that replay is done, nothing happens. It simply reaches end. To fix this, you'll need to somehow call -(void)playerStartPlaying at the same time as the setRate:time:atHostTime is executed on the AVPlayer. I guess you could start an NSTimer or dispatch_time and let it execute he method in exactly nextPlay-amount of time, but that would kinda defeat the purpose of this thing. Maybe not. You could try different stuff out. You probably CAN do this with some success, but I suggest finding a way to register for when the player started from the start. Maybe you can observe the rate or something, I don't know.. If you want to try it with a delayed method, you can try this:

double delayInSeconds = CMTimeGetSeconds(self.avplayer.currentItem.duration); //or something
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self playerStartPlaying];
});

Just keep in mind that this is some recursive shit, so even if you pause the video, this will still keep calling after that duration. In that case I suggest making a BOOL paused; and cancel the execution of the entire playerStartPlaying if it's set to YES. ..and of course set paused = YES; whenever you want to pause, next to wherever you say [player pause];

If this actually works, but you still get flashes, I know there are several ways to improve this. For instance, the CMTimeAdd() should probably be using some kind of synchronization-tool to make sure the times add up using the correct timeScale etc.

I have now spent way too much time writing this, and it might not even work. I have no idea. Good luck and good night.