Using NSURLSession to play remote audio, code critique - ios

First, to avoid posting a great deal of code on here, I have created a very basic example of what I am after - https://github.com/opheliadesign/AudioTest.
I am brand new to iOS development and I'm having a little trouble understanding the best way to load and play a static MP3 file on a remote server. The audio is not streaming live, is less than a megabyte - about 30 seconds long in average (they are radio dispatches).
It has been suggested that I use NSURLSession to load the MP3 file and then play it within the completion block. This seems to be working but I have a feeling that I could be handling it better, just do not know how. Here is the block where I grab the audio and begin playing:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
NSLog(#"View loaded");
// Register to receive notification from AppDelegate when entering background
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(stopPlayingAudio) name:#"stopPlayingAudio" object:nil];
// Assign timer to update progress
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
// Demo recording URL
NSString *recordingUrl = #"https://s3.amazonaws.com/tevfd-recording/All_Fire_and_EMS_2015-02-0214_48_33_630819.mp3";
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:recordingUrl] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSLog(#"No error..");
self.player = [[AVAudioPlayer alloc]initWithData:data error:nil];
// Setup slider for track position
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[self.player prepareToPlay];
[self.player play];
}
}]resume];
}
For example, have a slider that updates the player's position when it is moved. I initialize the AVAudioPlayer in the completion block but create a property for it in the header. If the player has not been initialized, could moving the slider cause a crash? If so, how should I handle this better?
Also, I have a timer that updates the slider position as the AVAudioPlayer plays. When the track reaches its end, the timer continues - clearly this is a potential memory issue. Not sure of the best way to handle this and I would also like for the user to be able to start playing the recording again after it is completed, for example if they moved the slider to the right.
I have searched and searched, cannot seem to find anything related to specifically what I am doing. Any help would be greatly appreciated.
UPDATE
This is what I first began with but I experienced some lag between clicking on a UITableView cell and presenting the view that loaded/played the audio. Several suggested NSURLSession.
- (void)viewDidLoad {
[super viewDidLoad];
// Get the URL
NSURL *callAudioURL = [NSURL URLWithString:[self.call objectForKey:#"url"]];
NSData *callAudioData = [NSData dataWithContentsOfURL:callAudioURL];
AVAudioPlayer *audio = [[AVAudioPlayer alloc] initWithData: callAudioData error:nil];
self.player = audio;
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[[self player] play];
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
}

If you're concerned about a small delay, then downloading and caching it is the best solution. You need to download the audio file and then play it locally. Typically playing -- or streaming -- from the network is useful only when the audio is so unique that you cannot store it locally.
To implement something like this (code untested):
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSFileHandle *hFile = [NSFileHandle fileHandleForWritingAtPath:self.localPath];
//did we succeed in opening the existing file?
if (!hFile) { //nope->create that file!
[[NSFileManager defaultManager] createFileAtPath:self.localPath contents:nil attributes:nil];
//try to open it again...
hFile = [NSFileHandle fileHandleForWritingAtPath:self.localPath];
}
//did we finally get an accessable file?
if (!hFile) {
NSLog("could not write to file %#", self.localPath);
return;
}
#try {
//seek to the end of the file
[hFile seekToEndOfFile];
//finally write our data to it
[hFile writeData:data];
} #catch (NSException * e) {
NSLog("exception when writing to file %#", self.localPath);
result = NO;
}
[hFile closeFile];
}
When all of the data has been received, start your audio:
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:self.localPath error:nil];

Related

Should AVAudioPlayer be used to play sound in background or main thread?

Should AVAudioPlayer be used to play sound in background or main thread? Is it a better practice to use background thread to play sound to not block the UI?
play your sound on a separate thread and I suspect your blocking issues will go away.
The easiest way to make this happen is to do:
- (void)playingAudioOnSeparateThread: (NSString *) path
{
if(_audioPlayer)
{
_audioPlayer = nil; // doing this while a sound is playing might crash...
}
NSLog(#"start playing audio at path %#", path);
NSError *error = nil;
NSData *audioData = [NSData dataWithContentsOfFile:path];
_audioPlayer = [[AVAudioPlayer alloc] initWithData:audioData error:&error];
if (error == nil)
{
[_audioPlayer play];
}
}
- (void)playAudio:(NSString *)path
{
[NSThread detachNewThreadSelector: #selector(playingAudioOnSeparateThread:) toTarget: self withObject: path];
}

ios - Loading an MP3 from remote URL and playing, freezing UI

I am building a very simple app that plays a very small (less than 30 seconds of audio) static MP3 file from the Internet. The files are listed in a TableView and clicking on a cell opens a View that in turn loads and plays the remote MP3. Audio is played using the AVAudioPlayer. Please note that I am NOT attempting to stream live, continuous audio - just a brief clip. Here is an example of what I am trying to play - https://s3.amazonaws.com/tevfd-recording/All_Fire_and_EMS_2015-02-0314_38_54_457551.mp3
Initially, I downloaded the data of the MP3 file using [NSData dataWithContentsOfURL:], which worked fine except for the fact that there is a delay between tapping a row and loading the view / playing the audio. One thing that I like about this approach is that the audio stops playing when the user leaves the View (back button press).
NSURL *callAudioURL = [NSURL URLWithString:[self.call objectForKey:#"url"]];
NSData *callAudioData = [NSData dataWithContentsOfURL:callAudioURL];
AVAudioPlayer *audio = [[AVAudioPlayer alloc] initWithData: callAudioData error:nil];
self.player = audio;
// Set slider to change position in audio
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[self.player prepareToPlay];
[[self player] play];
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
In an attempt to eliminate the delay between the views, I wrapped the loading of the file in a call to `dispatch_async' -
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSURL *callAudioURL = [NSURL URLWithString:[self.call objectForKey:#"url"]];
NSData *callAudioData = [NSData dataWithContentsOfURL:callAudioURL];
dispatch_async(dispatch_get_main_queue(), ^{
AVAudioPlayer *audio = [[AVAudioPlayer alloc] initWithData: callAudioData error:nil];
self.player = audio;
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[self.player prepareToPlay];
[[self player] play];
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
});
});
This works pretty well - the segue completes quickly and the data is loaded off of the main thread. However, if the user presses the Back button while the audio is playing, it continues.
My questions:
Am I on the right track with dispatch_async for this? If so, how should I go about killing the player when the user presses the Back button?
Thanks!
You should probably move away from using -[NSData dataWithContentsOfURL:] and start managing the network operation yourself using NSURLSession. -[NSData dataWithContentsOfURL:] is always going to load 100% of the data before playing anything, and it's synchronous, so calling it on the main thread is a good way to get the OS to kill your app for stalling the main thread. Doing the loading with dispatch_async keeps you from stalling the main thread, but doesn't solve the problem where it's going to load 100% of the data before you can do anything with it.

AVAudioPlayer returns wrong duration for audio file

I'm trying to play an audio file in my app, and I want to show progress of playing file.
The file completely plays but I can't get correct duration of file to display. The returned value is considerably lower than the actual duration. For example it returns 1 second for a 4-Seconds audio file, or 9 seconds for a 41-Seconds one.
Please do not say it only returns correct value while playing, because I tried that too and it doesn't. I always get 1 second for any file using AVAudioPlayer duration method, and I tried the solution here via AVURLAssets I get that weird 9 seconds instead of 41 !
There's also another problem with AVAudioPlayer. It plays the file but meanwhile its isPlaying method returns NO
I don't know if it makes any difference but my audio file is an AMR file.
Any help is really appreciated. Thanks.
**** UPDATE : I TRIED WITH OTHER FILES, IT WORKS FINE, SO NOW, I NEED TO GET THE DURATION OF AN AMR AUDIO FILE ****
The below code may help you to get the right progress on slider
NSURL *soundFileURL = [NSURL fileURLWithPath:path];
NSError *error;
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error];
_sliderSong.maximumValue = _audioPlayer.duration; //_sliderSong is a UISlider property
_sliderSong.value = 0.0; //_audioPlayer is a AVAudioPlayer property
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(updateTime:) userInfo:nil repeats:YES];
if (error)
{
NSLog(#"Error in audioPlayer: %#",
[error localizedDescription]);
} else {
_audioPlayer.delegate = self;
[_audioPlayer prepareToPlay];
[_audioPlayer play];
}
}
- (IBAction)slide { //called when we drag the slider explicitly (we have to bind it with slider)
_audioPlayer.currentTime = _sliderSong.value;
}
- (void)updateTime:(NSTimer *)timer { //slider updation
_sliderSong.value = _audioPlayer.currentTime;
}

Error with AVAudio Player in an SKScene

I am trying to make a background music in my game with AVAudioPlayer, so I set this code at the initWithSize method of my SKScene:
//SETTING BACKGROUND MUSIC//
NSError * err = nil;
NSURL * musicFile = [[NSBundle mainBundle] URLForResource:#"Cosmic Rush" withExtension:#"m4a"];
music = [[AVAudioPlayer alloc] initWithContentsOfURL:musicFile error:&err];
if(err)
{
NSLog(#"%#", [err userInfo]);
return;
}
[music prepareToPlay];
music.numberOfLoops = -1;
[music setVolume:0.5];
//SETTING BACKGROUND MUSIC//
Then, in another method: [music play]; However, the sound never plays, and I have tried all sort of extensions: .mp3, .caf, .mp4 etc. I have also checked people who had the same problem, but none of the solutions that solved their problems worked with mine.
there is a easier way to add background music to your scene. i use the following steps and works just as good as any other methods and it is easier.
import the #import AVFoundation; at the top of your scene.
add it as a property right after your #implementation.
AVAudioPlayer *_backgroundMusicPlayer;
then declare a method for the music
- (void)playBackgroundMusic:(NSString *)filename
{
NSError *error;
NSURL *backgroundMusicURL =
[[NSBundle mainBundle] URLForResource:filename
withExtension:nil];
_backgroundMusicPlayer =
[[AVAudioPlayer alloc]
initWithContentsOfURL:backgroundMusicURL error:&error];
_backgroundMusicPlayer.numberOfLoops = -1;
[_backgroundMusicPlayer prepareToPlay];
[_backgroundMusicPlayer play];
}
after that create a reference to that method in your -(id)initWithSize:(CGSize)size like the following
[self playBackgroundMusic:#"bgMusic.mp3"];
that should take care of the background music for you.

Playing audio file while I download it

I am trying to play partially downloaded file (I want the audio to start when it has enough downloaded data).
I have tried the following:
self.mutableData = nil;
self.mutableData = [NSMutableData data];
self.mutableData.length = 0;
GTMHTTPFetcher *fetcher =
[[self driveService].fetcherService fetcherWithURLString:song.filePath];
[fetcher setReceivedDataBlock:^(NSData *dataReceivedSoFar) {
[self.mutableData appendData:dataReceivedSoFar];
if (!self.audioPlayer.playing && self.mutableData.length > 1000000)
{
self.audioPlayer = nil;
self.audioPlayer = [[AVAudioPlayer alloc] initWithData:self.mutableData error:nil];
[self.audioPlayer play];
}
}];
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
if (error == nil)
{
NSLog(#"NSURLResponse: %#", fetcher.response.);
}
else
{
NSLog(#"An error occurred: %#", error);
}
}];
But it starts playing the first 1-3 seconds and then the app either crashes or it plays that 1-3 seconds over and over again until the download finishes and then it plays the whole song.
Can I somehow achieve this with AVPlayer, or fix this problem?
AVAudioPlayer cannot play files that are open for writing / downloading, but AVPlayer can. Switch to AVPlayer.
To stream audio, you should use AVPlayer or Audio Queue Services.
initWithData isn't intended for this purpose. You can use initWithData after you have downloaded the entire file.

Resources