MPMoviePlayer - multiple notifications - ios

I'm trying to observe playback ended notifications so I can loop the video:
#property (nonatomic, strong) MPMoviePlayerController *moviePlayer;
...
self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:theURL];
self.moviePlayer.controlStyle = MPMovieControlStyleNone;
[self.moviePlayer prepareToPlay];
self.moviePlayer.shouldAutoplay = YES;
[[NSNotificationCenter defaultCenter] addObserver: self selector:#selector(moviePlayBackDidFinish:) name: MPMoviePlayerPlaybackStateDidChangeNotification
object: self.moviePlayer];
- (void)moviePlayBackDidFinish:(NSNotification *)note {
if (note.object == self.moviePlayer) {
NSInteger reason = [[note.userInfo objectForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] integerValue];
if (reason == MPMovieFinishReasonPlaybackEnded) {
NSLog(#"THIS HAPPENS FOUR TIMES every time the movie ends");
[self.moviePlayer play];
}
}
}
As noted in the comment, I get this notification 4 times every time the video ends. The player still loops, but I don't like that I'm telling it to play 4 times.
Also, if I use MPMoviePlayerPlaybackDidFinishNotification instead of MPMoviePlayerPlaybackStateDidChangeNotification I only get notified once. However in that case the movie doesn't loop. This is all because the following doesn't work at all:
self.moviePlayer.repeatMode = MPMovieRepeatModeOne;
So my question is why do I get 4 MPMovieFinishReasonPlaybackEnded notifications every time the movie ends? Also is there a more foolproof way to loop videos?

Related

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

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.

multiple instances of the same sound on iOS

i'm making a bubble popping game and hit a bit of a snag :/
i have a sound (pop.mp3), this sound needs to play EVERY time one of the bubbles are tapped
all the bubbles are buttons that call the same method (-(IBACTION)bubblePop)
i initialised an AVAudioPlayer in the Viewdidload method
and call it's play function inside the bubblePop method
this clearly did not work however because the bubble's pop sound only plays one at a time, it doest overlap like i would expect it to
does anybody know how i can resolve this?
Extra info
if i initialise the audio player inside bubblePop i get no sound at all
You should initialize AVPlayer inside bubblePop and keep strong reference to it.
Try this:
#interface ViewController()
#property (strong, nonatomic) NSMutableDictionary *players;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.players = [[NSMutableDictionary alloc] init];
}
- (void)bubblePop {
NSURL *URL = your sound URL;
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:URL];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(itemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
self.players[playerItem] = player;
[player play];
}
-(void)itemDidFinishPlaying:(AVPlayerItem *)sender {
[self.players removeObjectForKey:sender];
}
#end

iOS 7 MPMoviePlayerController seek forward button brings the video to the End and displays Black screen

I am facing an issue with MPMoviePlayerController in iOS 7. I enter the fullscreen and then click (just a single tap) on seek forward button (>>|) , and the video playback ends and gives a black screen with a text "Loading" on the header.
I registered notification for "MPMoviePlayerPlaybackStateDidChangeNotification".
**[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerPlaybackStateDidChange:)
name:MPMoviePlayerPlaybackStateDidChangeNotification
object:self.player];**
It does not get fired on a single click of seek forward button.
Also on registration of "MPMoviePlayerPlaybackDidFinishNotification"
**[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerPlaybackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:nil];**
I get "MPMovieFinishReasonPlaybackEnded" event fired on that single click of seek forward button.
Any one knows the reason why? Is this a bug in apple?
I need to either stop this behavior of showing a black screen on single click , or just disable single click of seek forward button so that nothing happens.
Any one knows how to achieve this?
I fixed this by removing the MPMoviePlayer object completely, setting it to nil, removing it from it's superview and re-adding it using the original video Url. Code below:
- (void)addPlayerForUrl:(NSURL *)url {
self.player = [[MPMoviePlayerController alloc] initWithContentURL:url];
self.player.view.frame = self.videoView.bounds;
self.player.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.player.controlStyle = MPMovieControlStyleDefault;
[self.videoView insertSubview:self.player.view atIndex:0];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerLoadStateDidChangedNotification:)
name:MPMoviePlayerReadyForDisplayDidChangeNotification
object:self.player];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerPlaybackStateDidChangeNotification:)
name:MPMoviePlayerPlaybackStateDidChangeNotification
object:self.player];
}
#pragma mark - Notifications
- (void)moviePlayerLoadStateDidChangedNotification:(NSNotification *)notification {
self.isVideoPreloaded = YES;
self.videoPlayButton.hidden = YES;
self.photoImageView.hidden = YES;
self.videoLoadingImageView.hidden = YES;
}
- (void)moviePlayerPlaybackStateDidChangeNotification:(NSNotification *)notification {
NSURL *url = self.player.contentURL;
switch (self.player.playbackState) {
case MPMoviePlaybackStateSeekingBackward:
case MPMoviePlaybackStateSeekingForward:
break;
case MPMoviePlaybackStatePlaying:
self.videoPlayButton.hidden = YES;
if (!self.isVideoPreloaded) {
self.videoLoadingImageView.hidden = NO;
[self.videoLoadingImageView startAnimating];
} else {
self.videoLoadingImageView.hidden = YES;
}
break;
case MPMoviePlaybackStatePaused:
case MPMoviePlaybackStateStopped:
self.videoPlayButton.hidden = NO;
self.videoLoadingImageView.hidden = YES;
[self.player endSeeking];
[self.player.view removeFromSuperview];
[self.player setFullscreen:NO];
self.player = nil;
[self addPlayerForUrl:url];
break;
default:
break;
}
}
Notice how I keep the NSURL, right before the switch statement in the moviePlayerPlaybackStateDidChangeNotification. That way, I can re-initialize and re-add the MPMoviePlayer object.
Btw, my mpmovieplayer is on a tableviewCell if you're wondering. Hope this helps and let me know if you have questions. Good luck!
MPMoviePlayerLoadStateDidChangeNotification will be called when you single tap on the fast-forward or rewind button. You should check the loadState and just give it the path to your video and prepareToPlay again.
- (void)moviePlayerLoadStateChanged:(NSNotification *)notification {
MPMoviePlayerController *moviePlayer = notification.object;
MPMovieLoadState loadState = moviePlayer.loadState;
if(loadState == MPMovieLoadStateUnknown) {
moviePlayer.contentURL = [NSURL fileURLWithPath:videoPath]
[moviePlayer prepareToPlay];
}
.....
}
The reason you're getting MPMovieFinishReasonPlaybackEnded is because playback reached the end of the video (sorry if this is obvious). So it seem's your seek forward actions are seeking all the way to the end of the video. You can check the playback state with MPMoviePlaybackStateSeekingForward.
A quick solution could be to create your own forward button that seek's ahead by a specified time (ie. 5 seconds). But perhaps this isn't the functionality you're looking for.

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