AVQueuePlayer/AVPlayer loading notification? - objective-c

I have an AVQueuePlayer (which obviously extends AVPlayer) that loads a playlist of streaming audio. Streaming is all working fine, but I'd like to have an activity indicator to show the user audio is loading. Trouble is, I can't seem to find any such Notification in AVQueuePlayer (or AVPlayer) that would indicate when the audio buffer has finished loading/is ready to play (nor does there appear to be a delegate method). Any thoughts?

You will have to use KVO to get this done.
For each item you are adding to the queue, you may setup observers like this:
item_ = [[AVPlayerItem playerItemWithURL:[NSURL URLWithString:#"http://somefunkyurl"]] retain];
[item_ addObserver:self forKeyPath:#"status" options:0 context:nil];
[item_ addObserver:self forKeyPath:#"playbackBufferEmpty" options:0 context:nil];
Now you can evaluate the status of that item within the observer method;
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([object isKindOfClass:[AVPlayerItem class]])
{
AVPlayerItem *item = (AVPlayerItem *)object;
//playerItem status value changed?
if ([keyPath isEqualToString:#"status"])
{ //yes->check it...
switch(item.status)
{
case AVPlayerItemStatusFailed:
NSLog(#"player item status failed");
break;
case AVPlayerItemStatusReadyToPlay:
NSLog(#"player item status is ready to play");
break;
case AVPlayerItemStatusUnknown:
NSLog(#"player item status is unknown");
break;
}
}
else if ([keyPath isEqualToString:#"playbackBufferEmpty"])
{
if (item.playbackBufferEmpty)
{
NSLog(#"player item playback buffer is empty");
}
}
}
}

Related

How to detect if VoiceOver is on in MacOS?

I am working on a MacOS app, and I cannot figure out how to listen to accessibility status changes (for example, when VoiceOver is turned on or off).
In iOS, there is a notification I can listen to, UIAccessibilityVoiceOverStatusDidChangeNotification.
Is there an equivalent in MacOS?
It turns out there is a hidden api to listen to the accessibility notification. You can listen to #"NSApplicationDidChangeAccessibilityEnhancedUserInterfaceNotification"
[center addObserver:self
selector:#selector(onAccessibilityStatusChanged:)
name:#"NSApplicationDidChangeAccessibilityEnhancedUserInterfaceNotification"
object:nil];
and then in the method, you can check voiceOverEnabled which is introduced in 10.13
if(#available(macOS 10.13, *)) {
NSWorkspace * ws = [NSWorkspace sharedWorkspace];
NSLog(#"got notification voiceover enabled ? %d",ws.voiceOverEnabled);
}
On macOS, the correct way to do this is via KVO:
[[NSWorkspace sharedWorkspace] addObserver:self forKeyPath:#"voiceOverEnabled" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
with the accompanying handler method:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:#"voiceOverEnabled"]) {
// handle the event in your way.
//
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
On iOS and tvOS, use:
[[NSNotificationCenter defaultCenter] addObserverForName:UIAccessibilityVoiceOverStatusDidChangeNotification
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
// handle the event in your way.
}];

Optimise a function that calls itself in Obj C

I've written a function that checks whether a live score has changed every 0.1 seconds, and if it has it plays a system sound on iPhone.
- (void) checkIfShouldHaptic {
loadedScore = [self loadScoreFromKey:keyScore]; //load score saved in key
controllerScore = [self checkCurrentScore]; //check current score
if (loadedScore < controllerScore){ //if score in key is less than controller score
[self saveScore:controllerScore]; //save new score in key
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); //play system sound
[self repeatCheckIfShouldHaptic]; //repeat
}
else { //else score is not less
[self repeatCheckIfShouldHaptic]; //repeat
}
}
- (void)repeatCheckIfShouldHaptic {
[NSTimer scheduledTimerWithTimeInterval:timeDelayInSeconds target:self selector:#selector(checkIfShouldHaptic) userInfo:nil repeats:NO];
}
My programming capabilities are pretty limited so I was wondering if someone could show me if and how this could be optimised?
I'm not sure if a function calling itself over and over is good practice or if there's a better way to repeat a check.
Thank you.
I think you can use KVO
#property NSUInteger score;
- (void)viewDidLoad
{
[super viewDidLoad];
[self addObserver:self forKeyPath:#"score" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(#"From KVO");
if([keyPath isEqualToString:#"score"])
{
id oldScore = [change objectForKey:NSKeyValueChangeOldKey];
id newScore = [change objectForKey:NSKeyValueChangeNewKey];
NSLog(#"%# %#", oldScore, newScore);
}
}
I would rewrite your code a little. IMHO Using NSTimer is more resources consuming operation then using GCD.
Your approach is not so bad if you don't have any other options to receive some sort of notification.
- (void)checkIfShouldHaptic {
loadedScore = [self loadScoreFromKey:keyScore]; //load score saved in key
controllerScore = [self checkCurrentScore]; //check current score
if (loadedScore < controllerScore){ //if score in key is less than controller score
[self saveScore:controllerScore]; //save new score in key
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); //play system sound
}
[self repeatCheckIfShouldHaptic]; //repeat
}
- (void)repeatCheckIfShouldHaptic {
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeDelayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf checkIfShouldHaptic]
});
}

AVPlayer Skips the Beginning of a Video

I'm having an issue with AVPlayer skipping the first 0.5 seconds of a video I'm playing when I attempt to play it immediately after pre-rolling.
First, I create the player:
self.videoPlayer = [[AVPlayer alloc] init];
Then, I add the player item to the player and add an observer to check and see when the item is ready:
[self.videoPlayer addObserver:self forKeyPath:#"currentItem" options:0 context:kBLCurrentPlayerItemStatusContext];
[self.videoPlayer replaceCurrentItemWithPlayerItem:self.currentVideoPlayerItem];
When the player item is observed, I will check to see if the player is ready to play. If it's not ready to play, I will add an observer to make sure it's ready to play:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
...
} else if (context == kBLCurrentPlayerItemStatusContext) {
[self.videoPlayer removeObserver:self forKeyPath:#"currentItem" context:&kBLCurrentPlayerItemStatusContext];
if ([self.videoPlayer status]==AVPlayerStatusReadyToPlay) {
[self prerollVideoPlayer];
} else {
[self.videoPlayer addObserver:self forKeyPath:#"status" options:0 context:&kBLVideoPlayerStatusContext];
}
}
...
When the player status is observed as ready to play, I then preroll the player:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
...
if (context == kBLVideoPlayerStatusContext) {
if ([self.videoPlayer status] == AVPlayerStatusReadyToPlay) {
[self.videoPlayer removeObserver:self forKeyPath:#"status" context:&kBLVideoPlayerStatusContext];
[self prerollVideoPlayer];
}
}
...
After the pre-roll is complete, I play the player:
-(void)prerollVideoPlayer{
[self.videoPlayer prerollAtRate:1.0 completionHandler:^(BOOL finished){
if (finished) {
[self.videoPlayer play];
}
}];
}
When the player plays in this way, it skips the first ~0.5 seconds of the video. If I don't have the player play immediately when it's finished pre-rolling, and I call the play method later it plays fine. I've checked to make sure all the observers are correctly observed and they occur in the order I've noted above. Is there another observer I need to add or a notification I should check for to avoid this skipping behavior?

AVPlayer not playing MP3 audio file in documents directory iOS

I'm using AVPlayer to play a MP3 located in the documents directory (file is confirmed to be in there) - I load the AVPlayerItem wait for the AVPlayerItemStatusReadyToPlay then instantiate the AVPlayer with the item and play.
The AVPlayerItemStatusReadyToPlay does get called, but no audio actually plays, anyone have an idea why?
- (void)checkFileExists {
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.urlForEightBarAudioFile path]]) {
[self beginEightBarDownload];
} else {
// set up audio
[self setupAudio];
//NSLog(#"file %# already exists", [self.urlForEightBarAudioFile path]);
}
}
- (void)setupAudio
{
AVAsset *eightBarAsset = [AVAsset assetWithURL:self.urlForEightBarAudioFile];
self.eightBarsPlayerItem = [[AVPlayerItem alloc] initWithAsset:eightBarAsset];
[self.eightBarsPlayerItem addObserver:self forKeyPath:#"status" options:0 context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([object isKindOfClass:[AVPlayerItem class]])
{
AVPlayerItem *item = (AVPlayerItem *)object;
if ([keyPath isEqualToString:#"status"])
{
switch(item.status)
{
case AVPlayerItemStatusFailed:
break;
case AVPlayerItemStatusReadyToPlay:
self.player = [[AVPlayer alloc] initWithPlayerItem:self.eightBarsPlayerItem];
[self.player play];
NSLog(#"player item status is ready to play");
break;
case AVPlayerItemStatusUnknown:
NSLog(#"player item status is unknown");
break;
}
}
else if ([keyPath isEqualToString:#"playbackBufferEmpty"])
{
if (item.playbackBufferEmpty)
{
NSLog(#"player item playback buffer is empty");
}
}
}
}
Do this:
- (void)setupAudio
{
if([NSFileManager defaultManager] fileExistsAtPath:[self.urlForEightBarAudioFile absoluteString]])
{
AVAsset *eightBarAsset = [AVAsset assetWithURL:self.urlForEightBarAudioFile];
self.eightBarsPlayerItem = [[AVPlayerItem alloc] initWithAsset:eightBarAsset];
self.eightBarsPlayer = [AVPlayer playerWithPlayerItem:self.eightBarsPlayerItem]; //forgot this line
[self.eightBarsPlayer addObserver:self forKeyPath:#"status" options:0 context:nil]; // add observer for player not item
}
}
Refer avplayer-and-local-files link
in swift
func setupAudio() {
if NSFileManager.defaultManager().fileExistsAtPath(self.urlForEightBarAudioFile.absoluteString) {
self.eightBarsPlayerItem = AVPlayerItem(asset: AVAsset(URL: self.urlForEightBarAudioFile))
self.eightBarsPlayer = AVPlayer(playerItem: self.eightBarsPlayerItem)
}
}
- (void)setupAudio
{
dispatch_async(dispatch_get_main_queue(), ^{
AVAsset *eightBarAsset = [AVAsset assetWithURL:self.urlForEightBarAudioFile];
self.eightBarsPlayerItem = [[AVPlayerItem alloc] initWithAsset:eightBarAsset];
self.eightBarsPlayer = [AVPlayer playerWithPlayerItem:self.eightBarsPlayerItem]; //forgot this line
[self.eightBarsPlayer addObserver:self forKeyPath:#"status" options:0 context:nil]; // add observer for player not item
});
}

How to show migration progress of NSMigrationManager in a UILabel?

I have a lot of binary data on Core Data which I would like to remove and save as files so I setup the code so that it would run a manual migration.
The problem is that because I have to pull binary from Core Data and save them to files, the migration process could take a very long time depending on how much data there is.
I would like to show a simple progress label so that users wouldn't think that the app is frozen.
// app did launch
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
// add the progress label
self.progressLabel = [[UILabel alloc] initWithFrame:CGRectMake(103, 36, 114, 21)];
[self.progressLabel setText:#"0%"];
[self.window addSubview:self.progressLabel];
[self.window makeKeyAndVisible];
...
// start migration
[self progressivelyMigrateURL:storeURL];
...
}
// manually migrate store
- (BOOL)progressivelyMigrateURL:(NSURL*)sourceStoreURL
{
NSLog(#"start progressivelyMigrateURL");
...
// create the migration manager
NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:targetModel];
...
// register for KVO of migration progress here
[manager addObserver:self forKeyPath:#"migrationProgress" options:NSKeyValueObservingOptionNew context:NULL];
// start the migration
if (![manager migrateStoreFromURL:sourceStoreURL type:type options:nil withMappingModel:mappingModel toDestinationURL:destinationStoreURL destinationType:type destinationOptions:nil error:&error])
{
return NO;
}
...
}
// handle KVO of migration process here
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqual:#"migrationProgress"])
{
float migrationProgress = [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
int percent = migrationProgress * 100;
[self.progressLabel setText:[NSString stringWithFormat:#"%d%", percent]];
[self.progressLabel layoutIfNeeded];
[self.progressLabel setNeedsDisplay];
NSLog(#"migrationProgress: %d", percent);
NSLog(#"self.testLabel text: %#", self.testLabel.text);
}
}
The migration runs perfectly and I can confirm that the NSLog is returning incrementing values while the migration is taking place.
The problem is that self.progressLabel does not redraw every time the progress is updated and just skips from 0% to 100% while the migration is taking place. I know I can't run the migration on a separate thread. How can I make this work?
You precisely need to perform the migration on a background thread, and update the UI as you get progress notifications on the main thread.
- (void)startMigration {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSMigrationManager * manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:destinationModel];
[manager addObserver:self forKeyPath:#"migrationProgress" options:0 context:NULL];
BOOL success = [manager migrateStoreFromURL:sourceStoreURL type:type options:nil withMappingModel:mappingModel toDestinationURL:destinationStoreURL destinationType:type destinationOptions:nil error:&error];
[manager removeObserver:self forKeyPath:#"migrationProgress"];
if(success) {
dispatch_async(dispatch_get_main_queue(), ^{
// Go back to the main thread and continue…
});
}
}
And when getting notifications:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
dispatch_sync(dispatch_get_main_queue(), ^{
CGFloat progress = [(NSMigrationManager *)object migrationProgress];
// Update UI progress indicator
});
}