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

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?

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.

Related

MPMoviePlayerController stop/freezes video when beginSeekingForward is pressed only on iOS 8+

I am using the MPMoviePlayerController to play some streaming videos of type .m3u from a url. They video player launches perfectly and the video starts playing perfectly as well, but as soon as I press the Seek Forward or Next or beginSeekingForward button the video complete stops/freezes. After that I can click Done to dismiss the player or scrub the progress bar and it will come back to play to video. But the I can't click on the play/pause or at least seems not to do anything.
I have spent quite sometime looking around online for answer or at least a hint so I can tackle this issue in the right direction but not luck. So, I really hope someone could help with this. Btw, this only happens on iOS 8+, currently I am testing it in iOS 8.1.
This is how I create the player and load the video to it.
NSURL *videoURL = [NSURL URLWithString:self.video.flvurl];
self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL: videoURL];
self.moviePlayer.controlStyle = MPMovieControlStyleFullscreen;
self.moviePlayer.movieSourceType = MPMovieSourceTypeStreaming;
self.moviePlayer.shouldAutoplay = NO;
self.moviePlayer.fullscreen = YES;
self.moviePlayer.repeatMode = YES;
self.moviePlayer.view.backgroundColor = [UIColor blackColor];
self.moviePlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.moviePlayer.view.frame = self.containerView.frame;
[self.view addSubview:self.moviePlayer.view];
[self.moviePlayer prepareToPlay];
[self.moviePlayer play];
I have tried using multiple different notifications to see if I can catch this within one of them, but not luck at all. I have tested all of these notifications so far.. I have added this to the self.moviePlayer and also the self.moviePlayer.view not luck in either of them.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(willResignActive:)
name:UIApplicationWillResignActiveNotification
object:self.moviePlayer.view];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoPlaybackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:self.moviePlayer.view];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoStartedPlaying:)
name:MPMoviePlayerNowPlayingMovieDidChangeNotification
object:self.moviePlayer.view];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoLoadState:)
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.moviePlayer.view];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoPlaybackState:)
name:MPMoviePlayerPlaybackStateDidChangeNotification
object:self.moviePlayer.view];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoPlaybackUserInfoKey:)
name:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey
object:self.moviePlayer.view];
Well, if you need more relevant code please let me know. I really need some help with this.. Thanks in advance.
This code worked for me, but just to get rid of freezing. Sign up for the MPMoviePlayerLoadStateDidChangeNotification, like you did above. In the callback method, I simply set the movie players contentURL to the original URL I had initialized it with at the start, and set the movie player to play again.
The functionality of this is when you tap the NEXT or BACK buttons in fullscreen, the video simply starts playing again from the start. That may not be ideal since the user might not expect this, but at least it will stop it from freezing. I'm currently trying to find a better workaround.
- (void) videoLoadState:(NSNotification *)notification
{
MPMovieLoadState loadState = moviePlayer.loadState;
if(loadState == MPMovieLoadStateUnknown)
{
NSLog(#"Movie Load state is unknown");
moviePlayer.contentURL = currentlyPlayingVideoURL;
[moviePlayer prepareToPlay];
}
}
If you check the value of MPMoviePlaybackState in this method, it will always be MPMoviePlaybackStateStopped when NEXT or BACK are pressed, so I'm not sure how to handle it...

AVPlayerItemDidPlayToEndTimeNotification not called

I have a video on a screen that must be played with an infinity loop.
So I wrote :
self.player = [AVPlayer playerWithPlayerItem:avItem];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[_player currentItem]];
[_player play];
And I fallback method is :
- (void)playerItemDidReachEnd:(NSNotification *)notification {
[_player.currentItem seekToTime:kCMTimeZero];
[_player play];
}
It works well but sometimes it seems that my fallback is not called. The video is freezing at the end and never played again. It's happening randomly ...
Do you have an idea ?
You can just set a boundary time observer.
- (id) setupBoundaryEndWith:(NSArray*)array
{
__weak MY_AVPlayer* weakself = self;
return [self addBoundaryTimeObserverForTimes:array queue:NULL usingBlock:^{
[weakself stopAVPlayerAndLoopOrTriggerNextTrack];
}];
}
[_player currentItem] is null, since you haven't started playing yet. I would suggest either to explicitly add the AVPlayerItem (ideally) or register to notifications after starting the playback (reverse the 2 last lines).
I have not tried the 2-nd solution, so it might not work, if it takes some time to start the playback. If that is the case, I suggest setting an NSTimer to trigger a second later and then register.

MPMoviePlayerController spams 'Received memory warning'

I'm adding a MPMoviePlayerController as background view and while it plays I get spams of the log-message 'Received memory warning'. I don't now why, and maybe there is a workaround or a better solution.
Here is my code:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[[self navigationController]setNavigationBarHidden:YES animated:YES];
[moviePlayer play];
}
- (void) viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
[moviePlayer pause];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
//self.view.backgroundColor = [UIColor appStyleLightOrangeColor];
//Add Video playback
NSBundle *bundle = [NSBundle mainBundle];
NSString *moviePath = [bundle pathForResource:#"happy-female-friends-smartphon" ofType:#"m4v"];
NSURL *movieURL = [NSURL fileURLWithPath:moviePath];
moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:movieURL];
moviePlayer.controlStyle = MPMovieControlStyleNone;
moviePlayer.shouldAutoplay = YES;
moviePlayer.repeatMode = MPMovieRepeatModeOne;
moviePlayer.fullscreen = YES;
moviePlayer.movieSourceType = MPMovieSourceTypeFile;
moviePlayer.scalingMode = MPMovieScalingModeAspectFill;
//set the frame of movie player
moviePlayer.view.frame = self.view.bounds;
[self.view insertSubview:moviePlayer.view atIndex:0];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appBecameActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[self performSelector:#selector(animationCode) withObject:nil afterDelay:0.1f];
}
-(void)appBecameActive{
[moviePlayer play];
}
The log says it all. You are receiving a printed warning that you are using too much memory and your app will shutdown if you do not free up space. You need to take immediate steps to lower your memory line, so don't just shrug off those warnings. Yeah, your app might not crash immediately, but it is often a sign of a much bigger problem in your code setup.
Run the Allocations Instrument in XCode to see where the bulk of your memory is being used. I'd first check the size of that m4v video also. You should be streaming the video if it is a significant size. Additionally, ensure that you are not leaking memory using the Leaks instrument. But once again, when you receive the didReceiveMemoryWarning callback, take immediate action. Either pick up the notification in the AppDelegate or subscribe to the UIApplicationDidReceiveMemoryWarningNotification and release items/viewControllers that can be recreated later.
Here's the Memory Management Guide if you want to consult.
Thannks first to #tdevoy for your advices.
I finally got rid of the warning. It was the file type! I had to convert it to .3gpand now it works much smoother and without warnings.
On strange thing is I even use now 4mb more memory than before. But everything works great..

MPMoviePlayerController plays only when called twice. Only occurs in iOS 4

I have an app for iOS 4.0-5.1 that uses HTTP Live Streaming to play videos. I have a simple setup with a button in a view that starts playing the stream when it is tapped. This works fine in iOS 5 but the button needs to be tapped twice before the stream begins playing in iOS 4. Does anybody know why this is happening and how to make the video play on the first tap of the button?
Here is what I'm doing:
.h
#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>
#interface ViewController : UIViewController{
MPMoviePlayerController *moviePlayer;
}
#property (nonatomic, strong) MPMoviePlayerController *moviePlayer;
-(IBAction)playApple:(id)sender;
#end
.m
-(IBAction)playApple:(id)sender{
if(self.moviePlayer){
self.moviePlayer = nil;
}
//Use Apple's sample stream
NSURL *mediaURL = [NSURL URLWithString:#"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"];
self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:mediaURL];
//Begin observing the moviePlayer's load state.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerLoadStateChanged:)
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.moviePlayer];
[self.moviePlayer setMovieSourceType:MPMovieSourceTypeStreaming];
[self.moviePlayer setShouldAutoplay:NO];//Stop it from autoplaying
[self.moviePlayer prepareToPlay];//Start preparing the video
}
#pragma mark Notification Center
- (void)moviePlayerLoadStateChanged:(NSNotification *)notification{
NSLog(#"State changed to: %d\n", moviePlayer.loadState);
if(self.moviePlayer.loadState == MPMovieLoadStatePlayable){
//if load state is ready to play
[self.view addSubview:[self.moviePlayer view]];
[self.moviePlayer setFullscreen:YES];
[self.moviePlayer play];//play the video
}
}
I can see some possible issues - just try it out and let us know if any or all of this did the trick.
1. loadstate masking
The loadstate should not be directly compared but masked before comparing as it might contain a mixture of multiple states.
MPMovieLoadState
Constants describing the network load state of the movie player.
enum {
MPMovieLoadStateUnknown = 0,
MPMovieLoadStatePlayable = 1 << 0,
MPMovieLoadStatePlaythroughOK = 1 << 1,
MPMovieLoadStateStalled = 1 << 2,
};
typedef NSInteger MPMovieLoadState;
So instead of directly comparing it, as you are, mask it to be on the safe side.
2. view setup
Why not setting up the player view right before starting the playback. I have never seen it the way you do it (not that I was positively sure that this is the problem - still, seems odd to me). Additionally, even though the player will not actually use the view you are assigning (fullscreen mode does it a bit differently), you may want to enhance the view-setup with assigning a proper frame.
All of the above rolled into this new version of your code...
-(IBAction)playApple:(id)sender
{
if(self.moviePlayer)
{
self.moviePlayer = nil;
}
//Use Apple's sample stream
NSURL *mediaURL = [NSURL URLWithString:#"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"];
self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:mediaURL];
//Begin observing the moviePlayer's load state.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerLoadStateChanged:)
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.moviePlayer];
self.moviePlayer.view.frame = self.view.bounds;
[self.view addSubview:[self.moviePlayer view]];
[self.moviePlayer setFullscreen:YES];
[self.moviePlayer setMovieSourceType:MPMovieSourceTypeStreaming];
[self.moviePlayer setShouldAutoplay:NO]; //Stop it from autoplaying
[self.moviePlayer prepareToPlay]; //Start preparing the video
}
- (void)moviePlayerLoadStateChanged:(NSNotification *)notification
{
NSLog(#"State changed to: %d\n", moviePlayer.loadState);
if((self.moviePlayer.loadState & MPMovieLoadStatePlayable) == MPMovieLoadStatePlayable)
{
//if load state is ready to play
[self.moviePlayer play];//play the video
}
}

Problems with MPMoviePlayerController looping

I just want a continuously looping video. I set up the player like this:
self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:someURL];
self.moviePlayer.controlStyle = MPMovieControlStyleNone;
self.moviePlayer.shouldAutoplay = YES;
self.moviePlayer.repeatMode = MPMovieRepeatModeOne;
self.moviePlayer.view.frame = self.container.frame;
[self.container addSubview:self.moviePlayer.view];
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(moviePlayBackDidFinish:) name: MPMoviePlayerPlaybackStateDidChangeNotification
object: self.moviePlayer];
- (void) moviePlayBackDidFinish:(NSNotification*)notification {
NSLog( #"myMovieFinishedCallback: %#", notification );
MPMoviePlayerController *movieController = notification.object;
NSLog( #"player.playbackState = %d", movieController.playbackState );
}
The notification method is simply a hack that someone suggested here: Smooth video looping in iOS
I have two problems. The video looping still is not seamless. There is a very noticeable paused between loops. Second, the video stops looping after an arbitrary number of loops. Typically varies between 2-4 loops. This is obviously a huge problem for my app. Is the player really this buggy or am I doing something wrong?
I have created a complete seamless looping solution for video here seamless-video-looping-on-ios. Feel free to download the example xcode app and try it out for yourself to see my approach in action. I found that MPMoviePlayerController and AVPlayer both fail to work for this type of thing.
I was also unable to get gapless looping using MPMoviePlayerController -- there was always at least 0.5s of black, and the occasional flash of the QuickTime logo.
However, I am able to obtain gapless looping using AVPlayer -- it takes a couple of conditions to achieve, though:
Something about the encoding of my test video clip means that seeking to the beginning always causes a pause of about 0.5s at the start of each loop. Seeking to 1s into the clip with a kCMTimeZero tolerance makes it seamless. Without the explicit zero seek-tolerance, the effect is the same as seeking to the beginning of the clip.
Seeking while not playing is erratic; it causes a hang on my iPhone 4, but not my iPad 3. The two alternative fixes (shown #if'ed below), are to:
wait for the seek to finish before calling play again, or
wait until a specific time (before the end of the clip), then restart playback at the beginning.
The following code implements those two conditions:
self.player = [AVPlayer playerWithURL:url];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.frame = self.view.bounds;
[self.view.layer addSublayer:self.playerLayer];
[self.player seekToTime:CMTimeMakeWithSeconds(1, 1) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[self.player play];
#if 1
[[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self.player seekToTime:CMTimeMakeWithSeconds(1, 1) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
[self.player play];
}];
}];
#endif
#if 0
NSArray *times = [NSArray arrayWithObject:[NSValue valueWithCMTime:CMTimeMake(5, 1)]];
[self.player addBoundaryTimeObserverForTimes:times queue:NULL usingBlock:^{
[self.player seekToTime:CMTimeMakeWithSeconds(1, 1) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}];
#endif

Resources