I have a UITableViewCell. It has 2 AVPlayers, 2 AVPlayerItems, and 2 AVPlayerLayers. One AVPlayer and its contents are on the right side of the cell, and one on the left side of the cell. I am using the AVAssetResourceLoader to cache and play videos at the same time. This works great, except the second video does not display. It does cache properly, because after scrolling the tableview, it plays correctly as it should from the cache. But just the AVPlayerLayer does not display the video content as it should.
The AVAssetResourceLoader can not handle 2 simultaneous downloads. Upon first loading the tableview, the video on the left starts to download/play. The code for playing the video on the right side is read immediately after the code for downloading/playing the video on the left side--meaning of course, that the download process has not yet finished. To fix this problem, I have created a timer, like so:
if (self.downloadInProgress) {
// download is in progress. wait to start next download
self.downloadQueueCount++;
NSDictionary *videoSettings = #{#"id": activity2.objectId,
#"activity": activity2, #"cell": videoCell, #"left": #NO}
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self selector:#selector(DownloadNextVideo:)
userInfo:videoSettings repeats:YES];
} else {
[self playStreamingVideoOnRightInCell:videoCell withActivity:activity2];
}
And then the selector method:
- (void)DownloadNextVideo:(NSTimer *)timer {
if (self.downloadInProgress == NO) {
NSString *activityId = [[timer userInfo] objectForKey:#"id"];
self.videoChallengeID1 = activityId;
CompletedTableViewCell *videoCell = (CompletedTableViewCell *)[[timer userInfo]objectForKey:#"cell"];
Activity *activity = [[timer userInfo] objectForKey:#"activity"];
BOOL left = [[timer userInfo] objectForKey:#"left"];
[self.timer invalidate];
if (left) {
[self playStreamingVideoOnLeftInCell:videoCell withActivity:activity];
} else {
[self playStreamingVideoOnRightInCell:videoCell withActivity:activity];
}
}
}
Basically, this works correctly--the AVAssetResourceLoader gets the correct URL, correct activityID, and downloads/caches the correct video--except the video does not play as it downloads. I changed background colors, and I can confirm that the AVPlayerLayer is indeed present in the frame it should be. Just no video shows up.
I tried switching left and right videos and download order--and in that case, the right video played, and the left video didn't play. Basically, whichever video has to use this timer and continually check if a download is in progress or not, is the video that does not display correctly, and that makes me think it is something I am doing wrong with the timer concerning scope/passing variables.
One last detail, in case it may help. If I say the following (instead of the first method I had here):
if (self.downloadInProgress) {
[self playStreamingVideoOnRightInCell:videoCell withActivity:activity2];
}
Then, the playerLayer DOES show video--but it's the wrong video. It shows the same video as in the left side of the cell.
This has been a nightmare to debug, and I am fairly certain at this point that it has something to do with using a timer here to play the video instead of doing it inside the regular scope. Anyone have any ideas what I could be doing wrong?
In order to fix the proper video playing, you need to change the following. You are using the NSNumber value instead of a BOOL for the left value.
Your code:
BOOL left = [[timer userInfo] objectForKey:#"left"];
Should be:
BOOL left = [[[timer userInfo] objectForKey:#"left"] boolValue];
Related
I'm developing an app for Apple Watch using WatchKit and I need to implement a circular progress while recording audio, similar to Apple demos.
I don't know if WatchKit includes something to do it by default so I have created my own images (13 for 12 seconds [209 KB in total]) that I change with a timer.
This is my source code:
Button action to start/stop recording
- (IBAction)recordAction {
NSLogPageSize();
NSLog(#"Recording...");
if (!isRecording) {
NSLog(#"Start!");
isRecording = YES;
_timerStop = [NSTimer scheduledTimerWithTimeInterval:12.0
target:self
selector:#selector(timerDone)
userInfo:nil
repeats:NO];
_timerProgress = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(progressChange)
userInfo:nil
repeats:YES];
} else {
NSLog(#"Stop :(");
[self timerDone];
}
}
Action when timer is finished or if tap again on the button
-(void) timerDone{
NSLog(#"timerDone");
[_timerProgress invalidate];
_timerProgress = nil;
[recordButtonBackground setBackgroundImageNamed:[NSString stringWithFormat:#"ic_playing_elipse_12_%d", 12]];
counter = 0;
isRecording = NO;
}
Method to change progress
- (void)progressChange
{
counter++;
NSLog(#"%d", counter);
NSString *image = [NSString stringWithFormat:#"ic_playing_elipse_12_%d", counter];
[recordButtonBackground setBackgroundImageNamed:image];
NSLog(image);
}
This is a gif showing the bug. It starts to show but change the image randomly until it gets a pair of seconds. (Note: I have already checked that the first images have the right progress)
Update (other solution): using startAnimating
There is a new way to show a circle progress using startAnimatingWithImagesInRange:duration:repeatCount
You need to specify the images range and the duration for your animation. See an example below:
[self.myElement startAnimatingWithImagesInRange:NSMakeRange(0, 360) duration:myDuration repeatCount:1];
I believe this is a side-effect of the way WatchKit interprets image names.
Image names ending in numbers are used to represent frames in an animated image, so "ic_playing_elipse_12_10" is being interpreted as the first frame of the image named "ic_playing_eclipse_12_1" and your 10th image gets displayed when you want it to display your first one.
You should be able to just change your image names so the number isn't the last part of the image name and it will fix it.
I know that this question is answered but I would like to explain better the solution of startAnimating.
[myElement startAnimatingWithImagesInRange:NSMakeRange(imageIdBegin, imageIdEnd) duration:duration repeatCount:repetitions];
In order to use this, you need a suite of images named like you want but with sequential ids. For example: progress_0.png, progress_1.png, progress_2.png, .., progress_n.png
You can decide the begin index and the end index using that values in NSMakeRange(imageIdBegin, imageIdEnd) just indicating the number id (not the full image name)
Of course, you will need to indicate the duration of the animation in seconds, WatchKit will interpolate for you to animate your images between init index and end index.
And finally, you can indicate if you want to repeat this animation (for example for a loading action) or not. If you don't want to repeat, you have to indicate 1.
I hope this can help more people to perform circle progress or another animations inside their AppleWatch applications :)
I am using MPMoviePlayerController to play audio and I want to show the buffered data on slider like this ...
I want to show the buffer data like red section in slider. I have tried to google it. But I didn't get any solution for it.
and How to make slider customize?
Thanks in advance...
Yes We can Show stream data using MPMoviePlayerController by its playableDuration. I am explaining all these steps involved me to make this custom_slider for my customize player...
(You Can direct read step 4 if you only interested in programming part)
These are the steps:
Step 1: (First Design part) i done it using standard Controls...
In Interface Builder place your UISlider immediately on top of your UIProgressView and make them the same size.
On a UISlider the background horizontal line is called the track, the trick is to make it invisible. We do this with a transparent PNG and the UISlider methods setMinimumTrackImage:forState: and setMaximumTrackImage:forState:.
In the viewDidLoad method of your view controller add:
[ProgressSlider setMinimumTrackImage:minImage forState:UIControlStateNormal];
[ProgressSlider setMaximumTrackImage:transparentImage forState:UIControlStateNormal];
[progressBar setTrackImage:maxImage];
[progressBar setProgressImage:streamImage];
where ProgressSlider refers to your UISlider and progressBar to your UIProgressView.
and you can make transparent image programmatically like this.
UIGraphicsBeginImageContextWithOptions((CGSize){512,5}, NO, 0.0f);
UIImage *transparentImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();
you can change the size of it according to your progress view track image height width... I use {512,5} you can use {1,1} for default slider and progress view.
Reference taken form this answer https://stackoverflow.com/a/4570100/3933302
Step 2:
Now I tried to set the track and progress image of UIProgressView. But it was not showing the track and progress image. then i found this solution...to use this library available in github. https://gist.github.com/JohnEstropia/9482567
Now I changed occurrences of UIProgressView to JEProgressView, including those in NIBs and storyboards.
Basically, you'd need to force assigning the images directly to the UIProgressView's children UIImageViews.
The subclass is needed to override layoutSubviews, where you adjust the heights of the imageViews according to the image sizes.
Reference taken from this Answer https://stackoverflow.com/a/22322367/3933302
Step 3:
But now the question is from which will you make the image for slider and progressView. This is explained in this tutorial and You can download the psd also.
http://www.raywenderlich.com/32167/photoshop-tutorial-for-developers-creating-a-custom-uislider
Step 4:
Now come to programming part..
by using the playable duration it is possible to show stream data..(which was my biggest problem)
Using MPMovieLoadStatePlayable we can get when The buffer has enough data that playback can begin, but it may run out of data before playback finishes.
https://developer.apple.com/library/ios/documentation/MediaPlayer/Reference/MPMoviePlayerController_Class/index.html#//apple_ref/c/tdef/MPMovieLoadState
Now in your PlayAudio method...place this code..
-(void) playAudio:(NSURL *) url {
.............
streamTimer= [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(loadStateChange) userInfo:nil repeats:YES];
..........
}
and here is the definition of loadStateChange
-(void)loadStateChange
{
if(self.moviePlayer.loadState == MPMovieLoadStatePlayable){
[self.moviePlayer play];
slideTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
target:self
selector:#selector(updateProgressUI)
userInfo:nil
repeats:YES];
[streamTimer invalidate];
}
else if(self.downloadList)
//if you are playing local file then it will show track with full red color
progressBar.progress= appDel.moviePlayerController.moviePlayer.duration;
}
and here is the definition of updateProgressUI
- (void)updateProgressUI{
if(self.moviePlayer.duration == self.moviePlayer.playableDuration){
// all done
[slideTimer invalidate];
}
progressBar.progress = self.moviePlayer.playableDuration/self.moviePlayer.duration ;
}
I hope this fully customize slider will help you lot in making custom player...I explained all my work in steps.....Thanks ....
You cannot do this with MPMoviePlayerController.
However if you switch to AVPlayer then you can use AVPlayerItem.loadedTimeRanges. It contains all information you need for visualization in your custom control.
There are number of questions that have been asked on that topic, for example:
AVPlayer streaming progress
According the documentation of MPMoviePlayerController (available here) you can use the playableDuration property to retrieve the duration that has been loaded by the player.
You would then have to subclass UISlider to draw the red part of your control in the drawRect: method.
As for AVPlayer you would call loadedTimeRanges (documentation here) on the AVPlayerItem representing your content. That would give you an array of (as of iOS 8) a single CMTimeRange representing the part of your media that has been buffered. I would advise using [NSArray firstObject] when retrieving the time range to protect your code against a possibly empty array.
I'm displaying a short clip fullscreen, and at the end I would like NOT to dismiss the MPMoviePlayerController, as is the default behaviour, but to simple show the controls and allow the user to tap "Done" when they are ready to move on.
So far I managed to disable the auto-dismiss with the help of this answer: https://stackoverflow.com/a/19596598/362657 now I just need to show the controls.
I have the following:
-(void)videoFinished:(NSNotification*)aNotification{
int value = [[aNotification.userInfo valueForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue];
if (value == MPMovieFinishReasonUserExited) { // user tapped Done
[self dismissMoviePlayerViewControllerAnimated];
} else { // movie finished playing
MPMoviePlayerController *player = aNotification.object;
// do something to show the controls, but what?
}
}
I tried setting the controlStyle to none and then back to fullscreen, but that still has no effect until user taps the screen. Is there any way to trigger the display of the movie controls programmatically?
I havn't found the way how to rewind song from lock screen iOS 7. Volume slider is available, all the buttons work correctly but the upper slider is not available!
AVPlayer is the class I'm using.
Any help is appreciated!
You'll need to set this in the "now-playing info".
Whenever the item (track) changes, do something like
NSMutableDictionary *nowPlayingInfo = [[NSMutableDictionary alloc] init];
[nowPlayingInfo setObject:track.artistName forKey:MPMediaItemPropertyArtist];
[nowPlayingInfo setObject:track.trackTitle forKey:MPMediaItemPropertyTitle];
...
[nowPlayingInfo setObject:[NSNumber numberWithDouble:self.player.rate] forKey:MPNowPlayingInfoPropertyPlaybackRate];
[nowPlayingInfo setObject:[NSNumber numberWithDouble:self.currentPlaybackTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
Whenever the playback rate changes (play/pause), do
NSMutableDictionary *nowPlayingInfo = [[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo mutableCopy];
[nowPlayingInfo setObject:[NSNumber numberWithDouble:self.player.rate] forKey:MPNowPlayingInfoPropertyPlaybackRate];
[nowPlayingInfo setObject:[NSNumber numberWithDouble:self.currentPlaybackTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
You can get the current playback time like this:
- (NSTimeInterval)currentPlaybackTime {
CMTime time = self.player.currentTime;
if (CMTIME_IS_VALID(time)) {
return time.value / time.timescale;
}
return 0;
}
You have to register for different events like below in swift
MPRemoteCommandCenter.shared().playCommand.addTarget(self, action: #selector(self.handleRemoteCommandActions))
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(self, action: #selector(self.handleRemoteCommandActions))
MPRemoteCommandCenter.shared().previousTrackCommand.addTarget(self, action: #selector(self.handleRemoteCommandActions))
MPRemoteCommandCenter.shared().stopCommand.addTarget(self, action: #selector(self.handleRemoteCommandActions))
MPRemoteCommandCenter.shared().pauseCommand.addTarget(self, action: #selector(self.handleRemoteCommandActions))
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.isEnabled = true
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget(self, action: #selector(self.handleChangePlaybackPositionRemoteCommandActions))
let rcc = MPRemoteCommandCenter.shared()
let skipBackwardIntervalCommand: MPSkipIntervalCommand? = rcc.skipBackwardCommand
skipBackwardIntervalCommand?.isEnabled = false
let skipForwardIntervalCommand: MPSkipIntervalCommand? = rcc.skipForwardCommand
skipForwardIntervalCommand?.isEnabled = false
let seekForwardCommand: MPRemoteCommand? = rcc.seekForwardCommand
seekForwardCommand?.isEnabled = true
rcc.changePlaybackPositionCommand.isEnabled = true
rcc.changePlaybackRateCommand.isEnabled = true
rcc.ratingCommand.isEnabled = true
rcc.playCommand.isEnabled = true
rcc.togglePlayPauseCommand.isEnabled = true
here handleChangePlaybackPositionRemoteCommandActions this method will be your method which will have to manage seeking of song when scrubber (upper slider) changes its value
it will look something like below:-
#objc func handleChangePlaybackPositionRemoteCommandActions(event:MPChangePlaybackPositionCommandEvent) -> MPRemoteCommandHandlerStatus
{
print("handleChangePlaybackPositionRemoteCommandActions : \(event.positionTime)")
self.appDelegate.audioPlayer?.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: (self.appDelegate.audioPlayer?.currentItem?.currentTime().timescale)!))
MPNowPlayingInfoCenter.default().nowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = event.positionTime
return MPRemoteCommandHandlerStatus.success
}
I can not seem to find a way to add the functionality of dragging the progress bar to scrub through the currently playing song either, however getting the progress bar to show is partly covered by Chris above, but you have to pass the song's duration in as well as the other information to get the progress bar to show up. So in addition to what Chris pointed out, you need to add the following to the NSMutableDictionary:
[nowPlayingInfo setObject:[track valueForProperty: MPMediaItemPropertyPlaybackDuration]
forKey:MPMediaItemPropertyPlaybackDuration];
Also, you can handle a long press of the forward and back buttons and scroll your music. In the:
- (void) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent
you can look for these event subtypes:
if (receivedEvent.subtype == UIEventSubtypeRemoteControlBeginSeekingBackward){};
if (receivedEvent.subtype == UIEventSubtypeRemoteControlBeginSeekingForward){};
if (receivedEvent.subtype == UIEventSubtypeRemoteControlEndSeekingBackward){};
if (receivedEvent.subtype == UIEventSubtypeRemoteControlEndSeekingForward){};
The BeginSeekingForward and BeginSeekingBackward are triggered by long presses of the forward and back buttons respectively, and of course the EndSeekingForward and EndSeekingBackward event subtypes are when the finger is lifted off the button.
When you get these event subtypes, pass a new NSDictionary to
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo
with all of the information that was already in it (otherwise properties that you do not pass in will be cleared) plus a new MPNowPlayingInfoPropertyElapsedPlaybackTime and MPNowPlayingInfoPropertyPlaybackRate. For the rate, you decide how fast you want to scroll the audio. 1 is normal speed, negative values play backwards, so -2 is 2x normal speed in reverse. You will have to set the playback rate of the MPNowPlayingInfoCenter to be the same as the playback rate of the AVPlayer or they will not line up. This is why you want to also pass in the current time. You can get this with:
[player currentTime]
so to set the current time and rate:
[nowPlayingInfo setObject:[player currentTime]
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[nowPlayingInfo setObject:[[NSNumber alloc] initWithFloat:10]
forKey:MPNowPlayingInfoPropertyPlaybackRate];
and set the rate of the player with:
[player setRate:10]
This should line up both the player and the progress bar while scrolling with a 10x forward scroll speed. So you would do this when you get the BeginSeekingForward event subtype. When the scrolling ends, set the rate back to one, but still pass in the time and other information to the information center. For BeginSeekingBackward, just use a negative number for the rate.
I know this is an old post, but there was not a good solution and I ran across this thread while searching for how to get music information on to the lock screen. I was trying to implement this functionality in C# with Xamarin iOS and found a nice guide and sample Objective-C app with some of this functionality at http://www.sagorin.org/ios-playing-audio-in-background-audio/. But it did not have the scrolling functionality, nor did it make use of the information center, it just covered handling pause/play from the lock screen. I made a sample app in C# with Xamarin that demonstrates supplying info to the information center and scrolling with the back and forward buttons from the lock and control screens, which you can get here: https://github.com/jgold6/Xamarin-iOS-LockScreenAudio
I hope someone finds this helpful.
Here's how you do this thing with the timeline slider:
First of all the nowPlayingInfo must be set right. You can find how to do that anywhere (don't forget the key MPNowPlayingInfoPropertyPlaybackRate);
Second is to define the action for 'sliding':
MPChangePlaybackPositionCommand *changePlaybackPositionCommand = [[MPRemoteCommandCenter sharedCommandCenter] changePlaybackPositionCommand];
[changePlaybackPositionCommand addTarget:self action:#selector(changePlaybackPositionEvent:)];
[[MPRemoteCommandCenter sharedCommandCenter].changePlaybackPositionCommand setEnabled:YES];
Inside the action you can do some additional things, but the main thing here is
[yourPlayer seekTo:event.positionTime completionHandler:^(BOOL finished) {
[self updatePlaybackRateMetadata];
}];
updatePlaybackRateMetadata method has to update MPNowPlayingInfoPropertyElapsedPlaybackTime and MPNowPlayingInfoPropertyPlaybackRate.
A lot of time passed, but i hope this will help someone!
I am trying to play a video clip that loops indefinitely. I am doing this the way Apple recommends; by setting up a notification that gets triggered by AVPlayerItemDidPlayToEndTimeNotification:
#property(nonatomic) AVPlayer *videoPlayer;
#property(nonatomic) AVPlayerItem *videoPlayerItem;
-(void)loadVideo
{
NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:extension];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
NSString *tracksKey = #"tracks";
[asset loadValuesAsynchronouslyForKeys:#[tracksKey] completionHandler:
^{
dispatch_async(dispatch_get_main_queue(),
^{
NSError *error;
AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];
if (status == AVKeyValueStatusLoaded)
{
[self setVideoPlayerItem:[AVPlayerItem playerItemWithAsset:asset]];
[videoPlayerItem addObserver:self forKeyPath:#"status" options:0 context:&ItemStatusContext];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector (videoPlayerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:videoPlayerItem];
[self setVideoPlayer:[AVPlayer playerWithPlayerItem:videoPlayerItem]];
[scenePlayerView setVideoPlayer:videoPlayer];
}
});
}];
}
When triggered, this calls my method to effectively rewind and play the clip again:
-(void)videoPlayerItemDidReachEnd:(NSNotification *)notification
{
[videoPlayerItem seekToTime:kCMTimeZero];
[videoPlayer play];
}
The problem is, there is a brief but visible pause in the video playback every time it hits this method and loops back to the beginning.
The video clips are H.264 and have been tested in other players to ensure that they have no visible "skips" in their content. This is all happening under iOS6 both in the Simulator and on an iPad2 and iPad3.
What am I doing wrong?
You have two possible approaches to get a really professional looking loop with no glitches (AVPlayer by itself will not work). First, you could take your original video and decode it to a series of RGB frames (images). Then encode a new h.264 video and feed the frames into a longer h.264 video that is much longer than the first. For example, you might encode a 10 second looping clips 6 times to make a clip that last one minute or 6 * 5 to make a clip that would play for 5 minutes without glitching when the loop restarts. Then, open the longer clip and play it with AVPlayer and it will not glitch for a period of time.
The second approach would be to use an existing library that already handles seamless looping. You can take a look at my library, see my answer to basically the same question iphone-smooth-transition-from-one-video-to-another. The character in the example at the linked answer uses a repeating "loop" of one video clip that loops over and over when no buttons have been pressed.
Unfortunately it appears this actually is not currently possible using AVPlayer as it stands- there is an unavoidable hiccup upon beginning playback in an AVPlayer in every case. So "messier" hacks appear to be the only way to solve this, using multiple AVPlayers, etc.
I managed to get it working with two AVPlayer, two AVPlayerLayer added to two different views (I've tried add to the same view and the upper one get stuck once in a while), and switch alpha value between these two views.