Error in downloading an audio file form server synchronously - ios

I am using a NSBlockOperation in which i am trying to downlaod an audio file from server & storing it in documents directory.
NSBlockOperation *audioOperation = [NSBlockOperation blockOperationWithBlock:^{
//Perform doanload
NSString *itemStoredPath = [self downloadPOIAudioByUrl:itemUrl itemName:itemName folderName:itemFolder iteInfo:cacheAudioDetails];
// Update database
.....
}];
-(NSString *)downloadPOIAudioByUrl:(NSString *)itemUrl itemName:(NSString *)itemName folderName:(NSString *)folderName iteInfo:(CacheAudioDetails *)itemInfo {
// Get the url for video upload
NSURL *audioUrl = [NSURL URLWithString:[itemUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
// Set the response parameter
NSURLResponse *serverResponce = nil;
// Set the error parameter
NSError *error = nil;
// Create a request & set the time out interval for 1 min.
//NSURLRequest *videoRequest = [NSURLRequest requestWithURL:videoUrl cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0];
NSURLRequest *audioRequest = [NSURLRequest requestWithURL:audioUrl];
// Set the connection
NSData *audioData = [NSURLConnection sendSynchronousRequest:audioRequest returningResponse:&serverResponce error:&error];
if (error == nil && audioData != nil) {
// Data Found
// Store in directory & return the path to store in database
return audioPath;
}
return nil;
}
I have made a synchronous call to downlaod an audio file. But it is taking too much time & after long time it returns zero bytes of NSData.I thought it was due to my timed out request for 60 sec. Then i removed the time out request but still the problem remains as it is. My query is
Time out is related to server connection & not to fetching data from server
What should be the reason of Zero bytes responce from server.

Related

File loading doesn't work with NSURLSession via FTP

Using URL session FTP download is not working. I tried using below code.
Approach 1
NSURL *url_upload = [NSURL URLWithString:#"ftp://user:pwd#121.122.0.200:/usr/path/file.json"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url_upload];
[request setHTTPMethod:#"PUT"];
NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSURL *docsDirURL = [NSURL fileURLWithPath:[docsDir stringByAppendingPathComponent:#"prova.zip"]];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.timeoutIntervalForResource = 60.0;
sessionConfig.allowsCellularAccess = YES;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
NSURLSession *upLoadSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionUploadTask *uploadTask = [upLoadSession uploadTaskWithRequest:request fromFile:docsDirURL];
[uploadTask resume];
Approach 2
NSURL *url = [NSURL URLWithString:#"ftp://121.122.0.200:/usr/path/file.json"];
NSString * utente = #"xxxx";
NSString * codice = #"xxxx";
NSURLProtectionSpace * protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:url.host port:[url.port integerValue] protocol:url.scheme realm:nil authenticationMethod:nil];
NSURLCredential *cred = [NSURLCredential
credentialWithUser:utente
password:codice
persistence:NSURLCredentialPersistenceForSession];
NSURLCredentialStorage * cred_storage ;
[cred_storage setCredential:cred forProtectionSpace:protectionSpace];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.URLCredentialStorage = cred_storage;
sessionConfiguration.allowsCellularAccess = YES;
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];
The error I get is as follows:
the requested url is not found on this server
But the same url is working in terminal with SCP command and file is downloading successfully
First of all, you should consider switching from ftp to sftp or https protocol, since they are much more secure and address some other problems.
Having that said, ftp protocol is not strictly prohibited in iOS (unlike, say, http), and you still can use it freely. However NSURLSession is not designed to work with ftp-upload tasks out of the box. So you either have to implement a custom NSURLProtocol which adopts such a request or just use other means without NSURLSession.
Either way you will have to rely on the deprecated Core Foundation API for FTP streams. First create a CFWriteStream which points to the destination url on your ftp-server like this:
CFWriteStreamRef writeStream = CFWriteStreamCreateWithFTPURL(kCFAllocatorDefault, (__bridge CFURLRef)uploadURL);
NSOutputStream *_outputStream = (__bridge_transfer NSOutputStream *)writeStream;
And specify the user's login and password in the newly created object:
[_outputStream setProperty:login forKey:(__bridge NSString *)kCFStreamPropertyFTPUserName];
[_outputStream setProperty:password forKey:(__bridge NSString *)kCFStreamPropertyFTPPassword];
Next, create an NSInputStream with the URL to the source file you want to upload to (it's not neccesarily, to bound the input part to the streams API, but I find it consistent, since you anyway have to deal with streams):
NSInputStream *_inputStream = [NSInputStream inputStreamWithURL:fileURL];
Now the complicated part. When it comes to streams with remote destination, you have to work with them asynchronously, but this part of API is dead-old, so it never adopted any blocks and other convenient features of modern Foundation framework. Instead you have to schedule the stream in a NSRunLoop and wait until it reports desired status to the delegate object of the stream:
_outputStream.delegate = self;
NSRunLoop *loop = NSRunLoop.currentRunLoop;
[_outputStream scheduleInRunLoop:loop forMode:NSDefaultRunLoopMode];
[_outputStream open];
Now the delegate object will be notified about any updates in the status of the stream via the stream:handleEvent: method. You should track the following statuses:
NSStreamEventOpenCompleted - the output stream has just established connection with the destination point. Here you can open the input stream or do some other preparations which became relevant shortly before writing the data to the ftp server;
NSStreamEventHasSpaceAvailable - the output stream is ready to receive the data. Here is where you actually write the data to the destination;
NSStreamEventErrorOccurred - any kind of error what may occur during the data transition / connection. Here you should halt processing the data.
Be advised that you don't want to upload a whole file in one go, first because you may easily end up with memory overflow in a mobile device, and second because remote file may not consume every byte sent immediately. In my implementation i'm sending the data with chunks of 32 KB:
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted:
[_inputStream open];
return;
case NSStreamEventHasSpaceAvailable:
if (_dataBufferOffset == _dataBufferLimit) {
NSInteger bytesRead = [_inputStream read:_dataBuffer maxLength:kDataBufferSize];
switch (bytesRead) {
case -1:
[self p_cancelWithError:_inputStream.streamError];
return;
case 0:
[aStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];
// The work is done
return;
default:
_dataBufferOffset = 0;
_dataBufferLimit = bytesRead;
}
}
if (_dataBufferOffset != _dataBufferLimit) {
NSInteger bytesWritten = [_outputStream write:&_dataBuffer[_dataBufferOffset]
maxLength:_dataBufferLimit - _dataBufferOffset];
if (bytesWritten == -1) {
[self p_cancelWithError:_outputStream.streamError];
return;
} else {
self.dataBufferOffset += bytesWritten;
}
}
return;
case NSStreamEventErrorOccurred:
[self p_cancelWithError:_outputStream.streamError];
return;
default:
break;
}
}
At the line with // The work is done comment, the file is considered uploaded completely.
Provided how complex this approach is, and that it's not really feasible to fit all parts of it in a single SO answer, I made a helper class available in the gist here.
You can use it in the client code as simple as that:
NSURL *filePathURL = [NSBundle.mainBundle URLForResource:#"895971" withExtension:#"png"];
NSURL *uploadURL = [[NSURL URLWithString:#"ftp://ftp.dlptest.com"] URLByAppendingPathComponent:filePathURL.lastPathComponent];
TDWFTPUploader *uploader = [[TDWFTPUploader alloc] initWithFileURL:filePathURL
uploadURL:uploadURL
userLogin:#"dlpuser"
userPassword:#"rNrKYTX9g7z3RgJRmxWuGHbeu"];
[uploader resumeWithCallback:^(NSError *_Nullable error) {
if (error) {
NSLog(#"Error: %#", error);
} else {
NSLog(#"File uploaded successfully");
}
}];
It doesn't even need to be retained, because the class spawns a thread, which retain the instance until the work is done. I didn't pay too much attention to any corner cases, thus feel free to let me know if it has some errors or doesn't meet the required behaviour.
EDIT
For GET requests the only difference from any other protocol is that you pass login and password as part of URL and cannot use any secure means to do the same. Apart from that, it works straightforward:
NSURLComponents *components = [NSURLComponents componentsWithString:#"ftp://121.122.0.200"];
components.path = #"/usr/path/file.json";
components.user = #"user";
components.password = #"pwd";
[[NSURLSession.sharedSession dataTaskWithURL:[components URL] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable
response, NSError * _Nullable error) {
NSLog(#"%#", response);
}] resume];

Read Data/Response/Body of redirected NSURLDataTask

I am currently trying to access a webpage where the user can login using their credentials, after entering their user and password - if correct it will redirect to a new url. This new url loads a webpage with a single string which I intend to use.
However, how am I able to check the contents of the redirected url? At the moment I am only able to check the Response/Data/Contents of the initial page loaded by the following method;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
casSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSString *urlAddress = #"https://originalurl.com";
NSURL *httpUrl = [NSURL URLWithString:urlAddress];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:httpUrl];
[loginPage loadRequest:requestObj];
NSURLSessionDataTask *redirect = [casSession dataTaskWithURL:httpUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *newURL = [NSString stringWithFormat: #"%#", response.URL];
if ([newURL containsString:#"ticket=ST"]) {
NSString * registrationID = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"REGISTRATION: %#", registrationID);
if (registrationID != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
loginPage.hidden = YES;
});
}
} else {
NSLog(#"No ticket recieved");
}
}];
[redirect resume];
I'm not sure which delegate to use, in order to actively check every time a redirection happens and then obtain the contents of the new url?
Thanks.
You’re looking at this the wrong way. You should query the user for the login info directly and insert that into a single NSURLDataTask. Then the data task should query the server with the login info, and return some data.
This all happens with APIs (in a broad manner of speaking) where you will not present HTML contents to the user, but instead some sort of encoded data that is returned.
So for example, once you have a task defined from a URL or URLRequest, and you begin the task, you then use the completion handler to verify the returned data and/or error. If here, you may decode the returned data as a NSString, and then convert the JSON to objects, such as a user’s profile’s data (name, age, email, ...)
I did not go into detail in this answer because it is a very very broad topic, with many use cases. Look up some tutorials on NSURLDataTasks or consuming APIs from Swift and/or Objective-C.

YouTube Live API Stream Status and Quality Callback

In the "Live Control Room" of a YouTube Live broadcast, I can see a "Stream Status" view which shows me details of the video being sent to YouTube's RTMP endpoint.
I hit the liveStreams endpoint to get the "status" of the stream, but that only returns active, meaning that the video stream is being successfully sent to YouTube's RTMP endpoint, but no information about video data or quality.
Is this information exposed somewhere in the API? Can I also see additional details about the video, such as the bitrate, fps, etc. being sent to YouTube so I can verify my encoder is working correctly? Or does that check need to be done on the client-side and check the video right after it leaves the encoder before hitting the RTMP endpoint. I'm writing an iOS application, so using the "Live Control Room" on the web isn't a viable solution for me.
Here's what I'm doing on the broadcasting side to check the liveStream status:
- (void)checkStreamStatus {
[self getRequestWithURL:[NSString stringWithFormat:#"https://www.googleapis.com/youtube/v3/liveStreams?part=id,snippet,cdn,status&id=%#", self.liveStreamId] andBlock:^(NSDictionary *responseDict) {
NSLog(#"response: %#", responseDict);
// if stream is active, youtube is receiving data from our encoder
// ready to transition to live
NSArray *items = [responseDict objectForKey:#"items"];
NSDictionary *itemsDict = [items firstObject];
NSDictionary *statusDict = [itemsDict objectForKey:#"status"];
if ([[statusDict objectForKey:#"streamStatus"] isEqualToString:#"active"]) {
NSLog(#"stream ready to go live!");
if (!userIsLive) {
[self goLive]; // transition the broadcastStatus from "testing" to "live"
}
} else {
NSLog(#"keep refreshing, broadcast object not ready on youtube's end");
}
}];
}
getRequestWithURL is just a generic method I created to do GET requests:
- (void)getRequestWithURL:(NSString *)urlStr andBlock:(void (^)(NSDictionary *responseDict))completion {
NSURL *url = [NSURL URLWithString:urlStr];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
[request addValue:[NSString stringWithFormat:#"Bearer %#", [[NSUserDefaults standardUserDefaults] objectForKey:#"accessToken"]] forHTTPHeaderField:#"Authorization"];
[request setHTTPMethod:#"GET"];
// Set the content type
[request setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
[self parseJSONwithData:data andBlock:completion];
}];
}
- (void)parseJSONwithData:(NSData *)data andBlock:(void (^)(NSDictionary * responseDict))completion {
NSError *error = nil;
NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:&error];
if (error) {
NSLog(#"error: %#", [error localizedDescription]);
}
completion(responseDict);
}
Here's what I'm doing on the consumer side to check the video quality:
I am using the YTPlayerView library from Google.
- (void)notifyDelegateOfYouTubeCallbackUrl: (NSURL *) url {
NSString *action = url.host;
// We know the query can only be of the format http://ytplayer?data=SOMEVALUE,
// so we parse out the value.
NSString *query = url.query;
NSString *data;
if (query) {
data = [query componentsSeparatedByString:#"="][4]; // data here is auto, meaning auto quality
}
...
if ([action isEqual:kYTPlayerCallbackOnPlaybackQualityChange]) {
if ([self.delegate respondsToSelector:#selector(playerView:didChangeToQuality:)]) {
YTPlaybackQuality quality = [YTPlayerView playbackQualityForString:data];
[self.delegate playerView:self didChangeToQuality:quality];
}
...
}
But the quality "auto" doesn't seem to be a supported quality constant in this library:
// Constants representing playback quality.
NSString static *const kYTPlaybackQualitySmallQuality = #"small";
NSString static *const kYTPlaybackQualityMediumQuality = #"medium";
NSString static *const kYTPlaybackQualityLargeQuality = #"large";
NSString static *const kYTPlaybackQualityHD720Quality = #"hd720";
NSString static *const kYTPlaybackQualityHD1080Quality = #"hd1080";
NSString static *const kYTPlaybackQualityHighResQuality = #"highres";
NSString static *const kYTPlaybackQualityUnknownQuality = #"unknown";
...
#implementation YTPlayerView
...
/**
* Convert a quality value from NSString to the typed enum value.
*
* #param qualityString A string representing playback quality. Ex: "small", "medium", "hd1080".
* #return An enum value representing the playback quality.
*/
+ (YTPlaybackQuality)playbackQualityForString:(NSString *)qualityString {
YTPlaybackQuality quality = kYTPlaybackQualityUnknown;
if ([qualityString isEqualToString:kYTPlaybackQualitySmallQuality]) {
quality = kYTPlaybackQualitySmall;
} else if ([qualityString isEqualToString:kYTPlaybackQualityMediumQuality]) {
quality = kYTPlaybackQualityMedium;
} else if ([qualityString isEqualToString:kYTPlaybackQualityLargeQuality]) {
quality = kYTPlaybackQualityLarge;
} else if ([qualityString isEqualToString:kYTPlaybackQualityHD720Quality]) {
quality = kYTPlaybackQualityHD720;
} else if ([qualityString isEqualToString:kYTPlaybackQualityHD1080Quality]) {
quality = kYTPlaybackQualityHD1080;
} else if ([qualityString isEqualToString:kYTPlaybackQualityHighResQuality]) {
quality = kYTPlaybackQualityHighRes;
}
return quality;
}
I created a issue for this on the project's GitHub page.
I received a reply from Ibrahim Ulukaya about this issue:
We are hoping to have more information to that call, but basically active indicates the good streaming, and your streaming info is https://developers.google.com/youtube/v3/live/docs/liveStreams#cdn.format where you set, and can see the format.
So the answer for the time being is no, this information is not available from the YouTube Livestreaming API for the time being. I will updated this answer if/when the API is updated.
It seems Youtube Live streaming API has been updated to show Live stream health status with this property: status.healthStatus.status
See their latest API for more info.

How to asynchronously download files in a specific order in iOS?

My iPhone app makes a asynchronous request with NSURLRequest to my site.
The response is JSON with 10 URLs to images. When I receive the asynchronous response I want to download the 10 files using a loop, creating an NSMutableArray of UIImage objects.
I tried to do this with the method NSData dataWithContentsOfURL, and it works but isn't asynchronous, so the user interface is blocked.
If I try to use the NSURL asynchronous method inside the response of this asynchronous method, when I receive the 10 responses with the images, I can't know if the images have been downloaded in order, and in my application the order is important.
What is a solution for downloading files in order, without blocking the UI?
My code:
// Create the request.
NSString *advertURL = [NSString stringWithFormat: #"http://www.myURL"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:advertURL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create url connection and fire request
imagesConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
When I receive response:
//decode json with the urls
NSArray* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
//star the loop to downloading the ten images
for (int i=0; i<10; i++) {
//find image[i] url from json
NSString *fullImage_URL = json[i][#"url"];
//download image synchronously (this blocks the UI!!)
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullImage_URL]];
//insert image in the array
arrayImages[i] = [UIImage imageWithData:data];
}
Then, when I'm sure the array has 10 images and they're in order, I can start to show images on the screen.
Instead of processing the JSON array and then looping and fetching all those images on the main thread, why not do it on a background thread. In the background, run the loop, and at the end, call another method on the main to signal the completed task.
//star the loop to downloading the ten images ... in the background
dispatch_queue_t currentQueue = dispatch_get_current_queue();
dispatch_retain(currentQueue);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0, ^(void) {
for (int i=0; i<10; i++) {
//find image[i] url from json
NSString *fullImage_URL = json[i][#"url"];
//download image synchronously (this blocks the UI!!)
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullImage_URL]];
//insert image in the array
arrayImages[i] = [UIImage imageWithData:data];
}
// Return to main queue
dispatch_async(currentQueue, ^{
// process arrayImages now
});
dispatch_release(currentQueue);
});
To read more about the dispatch queues, check this out:
https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW1
There are many other ways to do this. Personally, I'm a fan of using NSNotificationCenter myself, and the OBSERVER pattern.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/Reference/Reference.html

Downloading data and processing asynchronous with NSURLSession

I am using NSURLSession to download xml files and then I want to do different processing to this files, like parsing them:
-(void)parseFeed:(NSURL *)url
{
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionDataTask* task = [FeedSessionManager.sharedManager.session dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error)
{
Parser* parser = [[Parser alloc] initWithData:data];
[self.feeds addObjectsFromArray:[parser items]];
}];
[task resume];
}
Parser object will parse the xml file using NSXMLParser. The parseFeed:(NSURL*)url is called from the ViewController:
Downloader* downloader = [[Downloader alloc] init];
[downloader parseFeed:[NSURL URLWithString:#"http://www.engadget.com/tag/features/rss.xml"]];
NSArray* items = [downloader feeds];
And this is how I create the NSURLSession object:
-(id)init
{
if(self = [super init])
{
_session = [NSURLSession sessionWithConfiguration:FeedSessionConfiguration.defaultSessionConfiguration delegate:self delegateQueue:nil];
}
return self;
}
Of course this approach doesn't work for me. Inside parseFeed method I want to wait until all data is downloaded and processed. Only then I want to access the self.feeds array in the ViewController.
Can someone point me into the right direction into doing this ? Or maybe point me to a different approach ?
I have used ASIHTTPRequest but now no longer maintained but you can use AFHTTPClient's operation queue
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:nil];
// Important if only downloading one file at a time
[client.operationQueue setMaxConcurrentOperationCount: 1];
NSArray *videoURLs; // An array of strings you want to download
for (NSString * videoURL in videoURLs) {
// …setup your requests as before
[client enqueueHTTPRequestOperation:downloadRequest];
}

Resources