Update UISlider from AVPlayer (iOS / objective-c) - ios

I would like my AVPlayer object to automaticly update my UISlider when playing.
I have found a code on this forum that seems to work for other but I'm broken at some point:
CMTime interval = CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC); // 1 second
self.playbackTimeObserver = [self.player addPeriodicTimeObserverForInterval:interval queue:NULL usingBlock:^(CMTime time) {
// update slider value here...
}];
I have inserted this code in my viewDidLoad but I removed "self.playbackTimeObserver" as I can't find what type of object is this. I guess that's why it s not working correctly.
Can you please tell me what type is it and where/how to declare it?
Here is my current code:
- (void)viewDidLoad
{
[super viewDidLoad];
CMTime interval = CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC); // 1 second
[songPlayer addPeriodicTimeObserverForInterval:interval queue:NULL usingBlock:^(CMTime time) {
NSLog(#"seconds = %f", CMTimeGetSeconds(songPlayer.currentTime));
}];
self.mmContainerSearch.hidden = NO;
self.mmContainerDownload.hidden = YES;
self.mmContainerLibrary.hidden = YES;
}

Its type is id. It right in the documentation.
Return Value
An opaque object that you pass as the argument to removeTimeObserver: to cancel observation.
If you never need to remove the time observer, then you don't really need to save the return value. My guess is that as some point you will want to cleanup. At that point, then you will need to call -removeTimeObserver:.

Related

Is it possible to check that a condition holds true continuously for a certain duration before executing a set of code? Objective C

For example, let's say that some variable changes based on user input. When this variable reaches some condition, it must hold that condition for some set time before some other code is executed.
if(changingVariable == someValue for some amount of time){
//code to be executed
}
NOTE: I want the answer to be as general as possible so that it can be applied to any situation.
For some context, here is the situation that I will be using it in:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (CMMotionManager *)motionManager {
_motionManager = [[CMMotionManager alloc] init];
_motionManager.accelerometerUpdateInterval = 0.1;
return _motionManager;
}
- (IBAction)takePhoto:(UIButton *)sender {
[self isStable];
}
- (void)isStable {
if(!self.motionManager.isAccelerometerActive) {
[self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accData, NSError *error){
CGFloat x = accData.acceleration.x;
CGFloat y = accData.acceleration.y;
//CGFloat z = accData.acceleration.z;
}];
// This is the condition that needs to hold true
// for half of a second before executing some code
if(fabs(x) < .095 && fabs(y) < .095){
// Code that takes a picture
}
}
}
#end
Edit: I just want to say thanks to all who helped!
Being unaware of your real intention, I'm proposing a bridged solutions covering up the scenarios.
First declare x and y as class properties
#property(nonatomic) CGFloat x;
#property(nonatomic) CGFloat y;
#property(nonatomic) BOOL scheduled;
...
Next modify your motion manager update code like this
[motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accData, NSError *error){
self.x = accData.acceleration.x;
self.y = accData.acceleration.y;
// This is the condition that needs to hold true
// for half of a second before executing some code
if(fabs(self.x) < .095 && fabs(self.y) < .095) {
//schedule your takePicture routine to be executed after 0.5 seconds
//if not already scheduled
if (!self.scheduled) {
[self performSelector:#selector(takePicture) withObject:nil afterDelay:0.5];
self.scheduled = YES;
}
}
/*
else {
//cancel previously scheduled method
//if the condition doesn't hold true in an intermediate update
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(takePicture) object:nil];
//reset the flag so that further scheduling is possible
self.scheduled = NO;
}
*/
}];
And now in your takePicture routine reevaluate the condition and execute code only if the condition is true:
-(void)takePicture {
if (fabs(self.x) < .095 && fabs(self.y) < .095) {
// Code that takes a picture
}
//reset the flag so that further scheduling is possible
self.scheduled = NO;
}
So to summarize things:
We capture accelerometer updates for each frame.
Schedule a takePicture routine after 0.5 seconds
In the takePicture routine reevaluate the condition and if it still holds
true we execute capture code.
So what's left? Here we have considered only the terminal conditions -- we evaluated the condition just before 0.5 seconds interval commences and just after it ends. But it doesn't guarantee the condition holds true in between. If we also need to conform to this scenario, we need to cancel the previously scheduled routine if the condition deviates from its true state and that can be achieved by uncommenting the else block in motion manager update portion that I've commented out for the time being.

MPNowPlayingInfoCenter throwing EXC_BAD_ACCESS

I am making an app that plays back audio and I have set it up so that the lock screen gets updated through MPNowPlayingInfoCenter, but I've run into a problem.
At seemingly random times, I get an EXC_BAD_ACCESS error when trying to update the now playing info.
Here's the code that does so:
- (void)updatePlayback
{
if(!active)
return;
NowPlayingController* npc = [AudioController nowPlayingController];
CMTime elapsed = player.currentTime;
Float64 elInterval = CMTimeGetSeconds(elapsed);
[npc setElapsed:elInterval];
CMTime duration = player.currentItem.duration;
Float64 durInterval = CMTimeGetSeconds(duration);
[npc setRemaining:ceilf(durInterval - elInterval)];
[npc setPlayPauseValue:isPlaying];
if(durInterval > 0)
{
[npc setProgressValue:elInterval/durInterval];
[npc setAudioDuration:durInterval];
}
_activeMetadata[MPMediaItemPropertyPlaybackDuration] = #(durInterval);
_activeMetadata[MPNowPlayingInfoPropertyPlaybackRate] = #(isPlaying);
_activeMetadata[MPNowPlayingInfoPropertyElapsedPlaybackTime] = #(elInterval);
MPNowPlayingInfoCenter* npInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
if(npInfoCenter && _activeMetadata)
{
if([npInfoCenter respondsToSelector:#selector(setNowPlayingInfo:)])
{
//////////THE FOLLOWING LINE TRIGGERS EXC_BAD_ACCESS SOMETIMES////////////
[npInfoCenter setNowPlayingInfo:_activeMetadata];
}
}
}
99.9% of the time, this works, but sometimes when resigning the app to the background or when changing audio files, or just randomly,
[npInfoCenter setNowPlayingInfo:_activeMetadata];
throws EXC_BAD_ACCESS.
Also, _activeMetadata is declared as:
#property (atomic, strong, retain) NSMutableDictionary* activeMetadata;
It is instantiated when the AVPlayer is created:
AVAsset* asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:path]];
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:asset];
player = [AVPlayer playerWithPlayerItem:playerItem];
CMTime duration = player.currentItem.duration;
NSTimeInterval durInterval = CMTimeGetSeconds(duration);
NSLog(#"%f", durInterval);
MPMediaItemArtwork* albumArtwork = [[MPMediaItemArtwork alloc] initWithImage:[downloader useCachedImage:CacheKeySeriesBanners withName:nil withURL:info[#"image"]]];
NSDictionary* nowPlayingInfo = #{MPMediaItemPropertyTitle:ptString,
MPMediaItemPropertyArtist:spString,
MPMediaItemPropertyArtwork:albumArtwork,
MPMediaItemPropertyAlbumTitle:info[#"title"],
MPMediaItemPropertyPlaybackDuration:#(durInterval),
MPNowPlayingInfoPropertyPlaybackRate:#(1),
MPNowPlayingInfoPropertyElapsedPlaybackTime:#(0)};
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:nowPlayingInfo];
_activeMetadata = [nowPlayingInfo mutableCopy];
updatePlayback is called via a CADisplayLink on every frame.
Any ideas what could be causing the exception?
I think you're calling setNowPlayingInfo way too often. Granted, it really shouldn't crash but there's no need to use CADisplayLink to call it 60 times a second.
So why are you calling it so often? If it's because you want to progress bar to track smoothly, there's still no need. From the MPNowPlayingInfoPropertyElapsedPlaybackTime declaration:
// The elapsed time of the now playing item, in seconds.
// Note the elapsed time will be automatically extrapolated from the previously
// provided elapsed time and playback rate, so updating this property frequently
// is not required (or recommended.)
p.s. I tried the code with an m4a file and found durInterval was NotANumber. With the correct duration and calling setNowPlayingInfo only once, the progress bar tracked fine & nothing crashed.
Apple fixed this crash in iOS 10.3 and above.
So if you want to support iOS 10.2.1 and below, be sure to throttle how often you set [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo property. Perhaps limiting setting the property only once a second.

iOS sequencer tempo slider is causing app to crash

I have created a drum sequencer using Objective C. I want the slider to control the tempo. At the moment, everything works, and the interval between each step is being controlled by:
while (self.running)
{
// sleep until the next step is due
[NSThread sleepUntilDate:time];
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// time duration until next step
time = [time dateByAddingTimeInterval:0.2];
}
So the time between each step is 0.2 seconds. I have tried to implement a tempo slider like so in the view controller .m (the slider has a range of 0.3 to 1.0 so will output similar value to what is currently in time):
- (IBAction)sliderMoved:(UISlider *)sender
{
AppDelegate* app = [[UIApplication sharedApplication] delegate];
app.tempo = sender.value;
}
and by changing the line in the while(self.running) thread to:
time = [time dateByAddingTimeInterval: (NSTimeInterval) _tempo];
However, this causes the time between steps to be far too short (tempo is crazy fast) and when any control in the app is touched, it crashes.
I wonder if I need to set up a function like this, but I'm not sure what would go inside to enable the tempo slider to work:
- (void)setTempo:(float)tempo
{
}
I have tried to be as clear as I can, if anyone can help me I'd be very grateful, thanks in advance
-(void) startDrumTick{
[self.myDrumTimer invalidate]; // stop any current existing timer
// perform the call to the method 'drumMethodOperation:'
// every 0.2 sec. NB: drumMethodOperation will run on main thread.
// this means that if you expect to do long-blocking operation,
// you will need to move that op to an async thread, in order to avoid
// the UI blocking
self.myDrumTimer = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:#selector(drumMethodOperation:)
userInfo:nil
repeats:YES];
}
-(void)drumMethodOperation:(id)sender
{
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// any other needed operation to run every 0.2 secs
}
Below an example for an async thread management using GCD
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
// Long blocking operation ( DO NOT PERFORM ANY UI OPERATION, like changing a text label, setting an image to an UIImageView, etc. )
[self myLongDbQuery];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Perform you UI Updates here
self.myLabel.text = #"Query done!!!";
});
});
Hope it helps
Luca is right about using GCD. If talk about your initial solution.
Did you set initial value for _tempo? Looks like your bug can be caused by _tempo = 0 initially. As you understand sliderMoved will be called only after some user action so you need to set initial value.

AVPlayer currentTime update for a UISlider when ViewController load

I'm playing songs in AVPlayer. I have created a separate view controller for my media player and initialization, and all the methods that I'm using for the player (play, pause, repeat, shuffle) are there in the same view controller.
I update a slider like this:
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(sliderUpdate:) userInfo:nil repeats:YES];`
- (void) sliderUpdate:(id) sender{
int currentTime = (int)((song.player.currentTime.value)/song.player.currentTime.timescale);
slider.value=currentTime;
NSLog(#"%i",currentTime);
song.currentTime=currentTime;
int currentPoint=(int)((song.player.currentTime.value)/song.player.currentTime.timescale);
int pointMins=(int)(currentPoint/60);
int pointSec=(int)(currentPoint%60);
NSString *strMinlabel=[NSString stringWithFormat:#"%02d:%02d",pointMins,pointSec];
lblSlidermin.text=strMinlabel;
song.strslidermin=strMinlabel;
}
Once I'm going out of the viewcontroller and when come again, song is playing but the problem is slider is not updating. So I created a singleton class to assign currently playing song details. Also inside the slider update I asigned playerCurrentTime (slidercurrent value) for a singleton class variable. And my viewdidload method I assigned like this:
if (song.isPlaying==NO) {
[self prePlaySong];
}else{
lblAlbum.text=song.currentAlbum;
lblArtist.text=song.currentArtist;
lblSong.text=song.currentSong;
slider.value=song.currentTime;
slider.maximumValue=song.sliderMax;
slider.minimumValue=song.sliderMin;
imgSong.image=song.songImage;
[btnMiddle setBackgroundImage:[UIImage imageNamed:#"pause.png"] forState:UIControlStateNormal];
}
but slider is not getting updated. Why is that and how I can solve this problem?
You should not use a timer for this, AVPlayer provides an API you observe the time.
Register an observer (e.g. in viewWillAppear) like this:
CMTime interval = CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC); // 1 second
self.playbackTimeObserver =
[self.player addPeriodicTimeObserverForInterval:interval
queue:NULL usingBlock:^(CMTime time) {
// update slider value here...
}];
To unregister the observer (e.g. in viewDidDisappear), do:
[self.player removeTimeObserver:self.playbackTimeObserver];
Hope that helps.
Swift 3 version.
Register an observer:
// Invoke callback every second
let interval = CMTime(seconds:1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Keep the reference to remove
self.playerObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) { time in
print(time)
}
Remove an obserber:
if let observer = self.playerObserver {
self.player.removeTimeObserver(observer)
self.playerObserver = nil
}

AVMutableAudioMixInputParameters setVolume atTime not working only with kCMTimeZero

Im am developing an iOS app in Xcode. I am still new to programming.
I am trying to set the volume of an audiotrack while playing it with an AVPlayer. It is working great if a set the time at kCMTimeZero but I want it to set the volume one second after the button was pressed.
Not working
- (IBAction)maxVolButtonPressed:(id)sender {
[audioInputParams1 setVolume:1 atTime:time1];
[self applyAudioMix];
}
Working
- (IBAction)minVolButtonPressed:(id)sender {
[audioInputParams1 setVolume:0 atTime:kCMTimeZero];
[self applyAudioMix];
}
What should I write after atTime if I want a one second delay?
ANSWER:
Ok so I figured it out. You have to add the time where the current item is at the moment. I use setVolumeRampFromStartVolume with a very little time interval instead of setVolume. setVolume fades to the given volume for some reason I haven't figured it out why.
It works for me like this:
CMTime time1 = CMTimeMake(1000, 1000); //2s
CMTime time2 = CMTimeMake(1001, 1000); //3s
CMTime timeCurrent = [player currentTime];
CMTime time1Added = CMTimeAdd(timeCurrent, time1);
CMTime time2Added = CMTimeAdd(timeCurrent, time2);
CMTimeRange fadeInTimeRange = CMTimeRangeFromTimeToTime(time1Added, time2Added);
[audioInputParams1 setVolumeRampFromStartVolume:0 toEndVolume:1 timeRange:fadeInTimeRange];
[self applyAudioMix];
You should do something like this:
- (IBAction)maxVolButtonPressed:(id)sender {
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(setVolume:) userInfo:nil repeats:NO];
}
- (void)setVolume:(id)sender{
[audioInputParams1 setVolume:1 atTime:kCMTimeZero];
[self applyAudioMix];
}
In first method you are saying: wait for 1 second and then call method setVolume:.

Resources