I am using AVCaptureMovieFileOutput to record videos and I want to add a UIProgressView to represent how much time there is left before the video stops recording.
I set a max duration of 15 seconds:
CMTime maxDuration = CMTimeMakeWithSeconds(15, 50);
[[self movieFileOutput] setMaxRecordedDuration:maxDuration];
I can't seem to find if AVCaptureMovieFileOutput has a callback for when the video is recording or for when recording begins. My question is, how can I get updates on the progress of the recording? Or if this isn't something that is available, how can I tell when recording begins in order to start a timer?
Here is how I was able to add a UIProgressView
recording is a property of AVCaptureFileOutput which is extended by AVCaptureMovieFileOutput
I have a variable movieFileOutput of type AVCaptureMovieFileOutput that I am using to capture data to a QuickTime movie.
#property (nonatomic) AVCaptureMovieFileOutput *movieFileOutput;
I added an observer to the recording property to detect a change in recording.
[self addObserver:self
forKeyPath:#"movieFileOutput.recording"
options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
context:RecordingContext];
Then in the callback method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
I created a while loop to be executed in the background, then I made sure to dispatch updates to the view on the main thread like this:
dispatch_async([self sessionQueue], ^{ // Background task started
// While the movie is recording, update the progress bar
while ([[self movieFileOutput] isRecording]) {
double duration = CMTimeGetSeconds([[self movieFileOutput] recordedDuration]);
double time = CMTimeGetSeconds([[self movieFileOutput] maxRecordedDuration]);
CGFloat progress = (CGFloat) (duration / time);
dispatch_async(dispatch_get_main_queue(), ^{ // Here I dispatch to main queue and update the progress view.
[self.progressView setProgress:progress animated:YES];
});
}
});
Related
I am trying to use the native player (AVPlayer) to reproduce a live stream on iOS. However, I have trouble resuming the playback. When I stop the playback and resume it after few seconds, the playback starts from the moment I paused instead of reproducing the current (last) sample of the live stream.
Is there a way to get the last sample, o configure AVPlayer to reproduce from last sample when tapping on Play Button?
My solution is based on denying user to keep the player paused. This is, destroying the player each time playback is resumed. And creating a new instance each time, playback is intended to be resumed.
According to Apple recommendation, the only solution to know if the AVPlayer was stopped is to add KVO. This is:
- (void)setupPlayer {
AVPlayer *player = [AVPlayer playerWithURL:streamURL];
AVPlayerViewController *playerViewController = [[AVPlayerViewController alloc] init];
playerViewController.player = player;
self.playerViewController = playerViewController;
[self configureConstraintsForView:self.playerViewController.view]; //Add Player to View
[self setupObservers];
[player play];
}
- (void)setupObservers {
[self.playerViewController.player addObserver:self
forKeyPath:#"rate"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context
if ([keyPath isEqualToString:#"rate"] && self.playerViewController.player.rate == CGPointZero.x) {
[self.playerViewController.view removeFromSuperview];
[self.playerViewController.player removeObserver:self forKeyPath:#"rate"];
[self.playerViewController.player pause];
self.playerViewController = nil;
}
}
Then, when user wants to re-engage the player, just call -(void)setupPlayer which start the playback from the last live sample.
In my app at some point the user can invoke video playing with tapping on an UI element, and then the following code segment will be executed:
self.loadingView.frame = _frameWhereItShouldBeLocated;
[self.loadSpinner startAnimating]; // self.loadSpinner is an UIActivityIndicatorView
self.loadingView.hidden = FALSE;
AVPlayer *player = [AVPlayer playerWithURL:fileUrl]; // fileUrl is where is the video file is hosted, which is not a local path
if ([player.currentItem.asset isPlayable])
{
if (!self.playerController) {
self.playerController = [[AVPlayerViewController alloc] init];
self.playerController.transitioningDelegate = self;
self.playerController.modalPresentationStyle = UIModalPresentationCustom;
}
self.playerController.player = player;
self.playerController.showsPlaybackControls = TRUE;
[self.navigationController presentViewController:self.playerController animated:YES completion:nil];
[self.playerController.player play];
}
I expect the loading view will be visible immediately after user's tap, and then after some time, the player controller is then presented and play the video. However, it happens that there is a significant delay before the loading view become visible.
This is what I expected:
User tap -> loading view shown -> (some time for loading the video, etc) -> play video
Instead this is what I've got:
User tap -> (significant time delay) -> loading view shown -> play video
After some debugging I found that the delay is caused by the [player.currentItem.asset isPlayable] call, i.e. the loading view only become visible after the call is returned. I tried to put the segment below the display of loading view in a dispatch_async call but it makes no different.
Is there anyway to handle this to make it behaves as expected?
Thanks a lot!
Insted of if ([player.currentItem.asset isPlayable])
check this
if ((player.rate != 0) && (player.error == nil)) {
// player is playing
}
Or
You can add notification for rate property like below
[player addObserver:self
forKeyPath:#"rate"
options:NSKeyValueObservingOptionNew
context:NULL];
Then check if the new value for observed rate is zero, which means that playback has stopped for some reason, like reaching the end or stalling because of empty buffer.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:#"rate"]) {
float rate = [change[NSKeyValueChangeNewKey] floatValue];
if (rate == 0.0) {
// Playback stopped
} else if (rate == 1.0) {
// Normal playback
} else if (rate == -1.0) {
// Reverse playback
}
}
}
For rate == 0.0 case, to know what exactly caused the playback to stop, you can do the following checks:
if (self.player.error != nil) {
// Playback failed
}
if (CMTimeGetSeconds(self.player.currentTime) >=
CMTimeGetSeconds(self.player.currentItem.duration)) {
// Playback reached end
} else if (!self.player.currentItem.playbackLikelyToKeepUp) {
// Not ready to play, wait until enough data is loaded
}
Based on above condition hide your indicator view.
And load your indicator view on main thread.
dispatch_async(dispatch_get_main_queue(), ^{
//load spinner
});
I have a collection view with an AVPlayer inside the cell and the AVPlayer starts playing the AVPlayerItem in a loop when
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
gets called. this works well but the problem is that after the AVPlayer is playing the item a few times the video is no longer shown but i can hear its sound.
I also add an observer for the value #"playbackBufferFull" for each item that is played like that:
[item addObserver:self forKeyPath:#"playbackBufferFull" options:NSKeyValueObservingOptionNew context:nil];
i noticed that when the video stops the observer method of the value #"playbackBufferFull" gets called, first of all i would like to know what causes the buffer the get full, the second and most important is how can i resume the AVPlayer when the video stops;
i tried calling [cell.videoPlayer play]; and to replace the item with a new one and it didnt work, the observer method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if ([object isKindOfClass:[AVPlayerItem class]] && [keyPath isEqualToString:#"playbackBufferFull"])
{
//this method is get called when the video stop showing but i can still hear it
//how can i resume the video?
}
}
my solution is :
first add observe for AVPlayerItemPlaybackStalledNotification in viewDidLoad or ...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemPlaybackStalledNotification
object:self.avPlayer.currentItem];
-(void)playerItemDidReachEnd:(NSNotification*)noti
{
//thisisn't good way but i can't find the best way for detect best place for resume again.
NSLog(#"\n\n give Error while Streaminggggg");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.avPlayer play];
});
}
BUT
maybe you can find the best way to call play method again for resume!
please check do you get AVPlayerStatusReadyToPlay keypatch ?
if you get , you can call play method there.
please notify me about the result
I'm using AVAudioPlayer instances in my project to play both background music clips and voice audio clips in sequence. I have to sync the audio with the animations displayed. It works great except in a scenario (i.e) after an interruption such as a call. If the interruption begins the animation will get paused , and if ends it will be resumed, audio should be in the same way so that it could be synced perfectly.
I have used delegate methods of AVAudioPlayer which get called during an interruption,
- (void) audioPlayerBeginInterruption: (AVAudioPlayer *) player {
[self pauseAllPlayers];
}
- (void) audioPlayerEndInterruption: (AVAudioPlayer *) player withOptions:(NSUInteger) flags{
[self playAllPlayers];
}
-(void)pauseAllPlayers
{
if(musicPlayer1)
[musicPlayer1 pause];
if(musicPlayer2)
[musicPlayer2 pause];
// ... paused all players the similar way
}
-(void)playAllPlayers
{
if(musicPlayer1)
[musicPlayer1 play];
// .. played all players the similar way
}
I have also set delegate to self. It works fine , but the audio clip not getting resumed from the same state after interruption, it plays the clip from the beginning. I have also tried the following:
1)
- (void)endInterruptionWithFlags:(NSUInteger)flags {
if (flags) {
if (AVAudioSessionInterruptionFlags_ShouldResume) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1), dispatch_get_main_queue(), ^{
[self playAllPlayers];
});
}
}
}
2)
- (void) audioPlayerEndInterruption: (AVAudioPlayer *) player withOptions:(NSUInteger) flags{
[NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(resumeBG) userInfo:nil repeats:NO];
[musicPlayer1 play];
}
-(void) resumeBG
{
[musicPlayer2 play]; // This is the player that plays background music.
}
3)
- (void) audioPlayerBeginInterruption: (AVAudioPlayer *) player {
[self stopAllAudioPlayers];
}
- (void) audioPlayerEndInterruption: (AVAudioPlayer *) player withOptions:(NSUInteger) flags{
[self playAllPlayers];
}
-(void) stopAllAudioPlayers
{
if(musicPlayer1)
[musicPlayer1 stop];
if(musicPlayer2)
[musicPlayer2 stop];
//...
}
But these are not working. I need to resume both BG and voice audio clips from the same state when the interruption begins. Is there a way to do that with AVAudioPlayer itself?
EDIT:
Based on Duncan C's answer I have tried the following:
- (void) audioPlayerBeginInterruption: (AVAudioPlayer *) player {
[self storeTime];
}
- (void) audioPlayerEndInterruption: (AVAudioPlayer *) player withOptions:(NSUInteger) flags{
[self resumeAtTime];
}
- (void) storeTime
{
player1Time = [player1 currentTime];
player2Time = [player2 currentTime];
}
-(void) resumeAtTime
{
[player1 playAtTime:player1Time];
[player2 playAtTime:player2Time];
}
I have also tried using (player1.deviceCurrentTime + player1Time) in resumeAtTime function. Is that the correct way to do so? Also I have tried using player.currentTime/player1.rate
The docs don't make it clear if the play method plays a sound from the beginning or continues after a pause. It just says that it plays the sound, which suggests playing from the beginning. Your findings suggest that that is what it does.
Here's what I would do. Use the currentTime method to get the playback point for your sound(s) before pausing them.
Then use playAtTime to resume each sound at the time it left off.
I'm having some trouble with registering WHEN the player is starting to play external videos (over internet) using AVPlayer. Please read the question before suggesting solutions.
I initialize the player like this:
player = [[AVPlayer alloc] initWithURL:[[NSURL alloc] initWithString:#"http://example.com/video.mp4"]];
playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[playerLayer setFrame:[videoView bounds]];
[videoView.layer addSublayer:playerLayer];
This adds the player to the view correctly. I have added the following two lines of code to keep track of when the player is ready, and what the status/rate is;
[player addObserver:self forKeyPath:#"rate" options:0 context:nil];
[player addObserver:self forKeyPath:#"status" options:0 context:nil];
These two line will call the method - (void)observeValueForKeyPath:.... when something changes with the status or the rate of the AVPlayer.
So far it looks like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
//To print out if it is 'rate' or 'status' that has changed:
NSLog(#"Changed: %#", keyPath);
if ([keyPath isEqualToString:#"rate"]) //If rate has changed:
{
if ([player rate] != 0) //If it started playing
{
NSLog(#"Total time: %f", CMTimeGetSeconds([[player currentItem] duration]));
// This NSLog is supposed to print out the duration of the video.
[self setControls];
// This method (setControls) is supposed to set play/pause-buttons
// as well as labels for the current and total time of the current video.
}
}
else if ([keyPath isEqualToString:#"status"]) // If the status changed
{
if(player.status == AVPlayerStatusReadyToPlay) //If "ReadyToPlay"
{
NSLog(#"ReadyToPlay");
[player play]; //Start the video
}
}
}
The state of the AVPlayer changes to readyToPlay almost immediately after initializing it, and I then call [player play]. When this happens, the rate changes to 1.00000, meaning it's actually playing at that rate, but the video is now just starting to buffer, not playing. The screen is black, and it takes a couple of seconds, and then it starts playing. The rate, however, indicates it starts playing before it does. The rate stays at 1.00000, not going down to 0 when start-buffering, which makes it very difficult for me to know when the player has enough information to start setting the controls (I.E time stamps etc).
The NSLog() printing out the duration of the video above prints out nan (Not A Number), which leads me to think that the item isn't ready to be played, however, the rate stays at 1.0000 until it has buffered a while, then it will actually play, still with rate at 1.0000.
It does, however, get called twice. The rate "changes" to 1.0000 twice without being anything else in between. In neither calls, the duration of the video is an available variable.
My goal is to fetch the current and total timestamp of the video as fast as possible (I.E 0:00/3:52). This will also be used to register the scrubbing of a slider (for fast-forward etc.).
These values are not ready when the player notifies me it's playing at a rate of 1.0000, twice. If I manually click "play" after a second or so (and call [player play]), then it's working. How can I register to know when the video is ready, not just 'ready to get ready'?
See addBoundaryTimeObserverForTimes:queue:usingBlock: on AVPlayer and this example from Apple.
AVPlayer *player = [AVPlayer playerWithURL:[NSURL URLWithString:#"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"]];
[player play];
// Assumes a property: #property (strong) id playerObserver;
// Cannot use kCMTimeZero so instead use a very small period of time
self.playerObserver = [player addBoundaryTimeObserverForTimes:#[[NSValue valueWithCMTime:CMTimeMake(1, 1000)]] queue:NULL usingBlock:^{
//Playback started
[player removeTimeObserver:self.playerObserver];
}];
I think the nearest you'll get to is to observe player.currentItem.playbackLikelyToKeepUp