Can AVContentKeySession makeStreamingContentKeyRequestDataForApp safely be forced to synchronous? - ios

I have an app that streams audio protected by FairPlay DRM. It originally shipped using AVAssetResourceLoaderDelegate to deliver FairPlay keys, but now I'm updating it to use AVContentKeySession for iOS 11.2 and later. Side note: if you're trying to do this and are frustrated at the lack of documentation, there's sample code at the "FairPlay Streaming Server SDK (4.2.0)" link here.
Each of my audio products is broken into many tracks. When I open an audio product, I queue up more than one track, via AVQueuePlayer. Each of those tracks generates a request for a FairPlay key. In the case when there is no persistent key already downloaded, each of those requests goes to the key server, downloads a key, generates a persistent key, and saves it. Each track has the same key, so they all end up with the same persistent key data, and each one overwrites the last one to finish.
Because the cost of my key servers is dependent on the number of key requests I make, I'd like to have only the first request actually hit the key server, and subsequent requests use the persistent key. But the method used to get the SPC data to pass up to the key server, makeStreamingContentKeyRequestDataForApp, uses an async completion block. The equivalent method on AVAssetResourceLoadingRequest is synchronous.
My question: is it safe to force this call to be synchronous using a semaphore? Like this:
-(void)handleOnlineRequest:(AVContentKeyRequest *)req
NSData *appCert = [self _getAppCertData];
NSData *assetId = [self _kidFromRequest:req];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[req makeStreamingContentKeyRequestDataForApp:appCert
contentIdentifier:assetId
options:nil
completion:^
NSData *contentKeyRequestData, NSError *error)
{
//request key data and respond to request
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
The effect is especially pronounced when downloading audio that has not been streamed before. Download speeds for audio using AVAssetDownloadTask are very slow, so I initiate many at once, and each one generates a key request.

Related

CKModifyRecordsOperation modifyRecordsCompletionBlock not being called

I'm using CKModifyRecordsOperation to save a set of records and if I have internet connection all works well and completion block is being called. But when I don't have connection the completion block is not being called and I don't get any information that my operations failed.
I'm using the following code in completion block
modifyOperations.modifyRecordsCompletionBlock = ^(NSArray *savedRecords, NSArray *deletedRecordIDs, NSError *error)
{
if(error){
NSLog(#"Error: %#", error.localizedDescription);
}
item.creatorRecordId = record.recordID;
};
and then I'm performing operation using
[self.publicDB addOperation:modifyOperations];
Any ideas how can I get an information if the operation failed for example in the case where there is no internet connection?
CloudKit operations have their qualityOfService property set to NSQualityOfServiceUtility by default.
Operations that use NSQualityOfServiceUtility or NSQualityOfServiceBackground may be marked as using discretionary network requests. The system can hold discretionary network requests if network connectivity is poor, so you might not get a response from the server until conditions improve and the system sends the request.
If you'd like your request to be sent immediately in all cases, set CKOperation.qualityOfService to NSQualityOfServiceUserInitiated or NSQualityOfServiceUserInteractive.

Unknown error -12881 when using AVAssetResourceLoader

I am trying to write a custom resourceLoader delegate to work with an AVURLAsset. I have started with the AVARLDelegateDemo code from Apple. I am trying to playback an HLS url. I am testing on an iPad.
What I notice is that the playlist file (.m3u8) gets downloaded correctly. Then video file (.ts) also gets downloaded. I know that the .ts file is downloaded because I can see the GET request completing on the web server with status 200. I also set a breakpoint at the following line:
[loadingRequest.dataRequest respondWithData:data];
The length of data matches the file size and the first byte is the sync byte of the .ts (0x47) as expected.
The problem is that the app displays an error code. The following dialog pops up:
"The operation could not be completed. An unknown error occurred (-12881)"
Googling for this error has not turned up any information. I do not know what to check for or how to get more information. It is not as if the app is crashing and giving me a stack trace. The video refuses to play and I get no more information beyond the "unknown error -12881" This is not a lot to go on.
Also, if I point an unmodified version of the demo code at my playlist, the video plays just fine.
Can anyone tell me what is going wrong? Here is the code from my customized resource loader.
- (BOOL) resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
NSURLRequest *redirect = nil;
redirect = [self generateRedirectURL:(NSURLRequest *)[loadingRequest request]];
if (redirect)
{
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:redirect.URL
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
[loadingRequest.dataRequest respondWithData:data];
[loadingRequest finishLoading];
}] resume];
} else
{
[self reportError:loadingRequest withErrorCode:badRequestErrorCode];
}
return YES;
}
For the record:
I contacted Apple Developer Tech Support about the issue with trying to use the AVAssetResourceLoaderDelegate to get access to the .ts files. This approach does not work because:
"
It is not possible to have access to the data as it is being
downloaded. iOS only allows only the following to be returned via
AVAssetResourceLoaderDelegate for HTTP Live Streaming media:
- key requests
- playlist
- media redirects
"
For my use case, I ended up using a local web server (https://github.com/swisspol/GCDWebServer) and sending all requests to a web server within the app. This web server then makes requests to the remote server

How return data from an HTTP request in Swift/Objective C

I'm trying to use Coinbase's API to get information about my online bitcoin wallet, and I'm trying to use Swift's NSURLSession object to do so. Perhaps I'm missing something obvious in the Apple docs, but after reading through the information about NSURLSession and NSURLSessionTask I still do not understand how to make an HTTP request and then return the body of the response so that the body can persist throughout the life of my app. As of now I only see the ability to use completion blocks which return void, or delegates which either return void themselves or use completion blocks which also return void. I want to use the data I get from the response later in the app, but because I'm using completion blocks I must handle the response data immediately after the response arrives.
To make it clear, I want to do something along the lines of the pseudocode function below:
func makeHTTPCall(urlString : String) -> String? {
create NSURLSession object
create request with proper headers and using the passed-in urlString
use the session object to send out the request
get the response object, extract the response body as a string, and return it
}
Then later, I could call something like this:
let myObject : MyObject = MyObject()
let respData : String = myObject.makeHTTPCall("https://coinbase.com/api/v1/account/balance")
This data is returning a JSON Object string, which is the String I want to persist beyond the life of the response and its completion block. How can I do this in either Swift or Objective C, since I'll be able to use either in Xcode 6?
EDIT: Two answers have been posted, but they miss the fundamental point of this question. I need to RETURN the data which I receive from the response. Both answers (and all other answers I've seen on SO) simply print the data received. I would like code that doesn't use a void-returning completion handler, but instead returns the data so that it can be used later in the lifecycle of the app. If there is anything unclear about my question, please tell me, though I don't see how this can be made clearer.
In the edit to your question, you say:
I need to RETURN the data which I receive from the response. Both answers (and all other answers I've seen on SO) simply print the data received. I would like code that doesn't use a void-returning completion handler, but instead returns the data so that it can be used later in the lifecycle of the app. If there is anything unclear about my question, please tell me, though I don't see how this can be made clearer.
I understand the appeal of this strategy, because it feels so intuitively logical. The problem is that your networking requests should always run asynchronously (e.g. use that completion handler pattern to which you allude).
While there are techniques making a function "wait" for the asynchronous request to complete (i.e. to make the asynchronous NSURLSession method behave synchronously or use one of the old synchronous network request methods), this is a really bad idea for a number of reasons:
If you do this from the main thread, it results in a horrible user experience (the app will be unresponsive while the request is in progress and the user won't know if the app is busy doing something or whether it's frozen for some unknown reason).
Again, if you do this from the main thread, you also risk having the iOS "watch dog" process kill your app (because if you block the main queue for more than a few seconds at the wrong time, particularly as the app comes to foreground, the OS will unceremoniously terminate your app). See Technical Q&A #1693 for a discussion on the problems of doing synchronous network requests.
We generally prefer the asynchronous network techniques because they offer more features unavailable with synchronous techniques (e.g. making requests cancelable, offer progress updates when using delegate-based network requests, etc.).
You really should use the completion handler pattern that those other questions suggest, and manage the changing state of the app in those handlers. In those situations where you absolutely cannot let the user proceed until some network request is done (e.g. you can't let the user buy something until you confirm their bitcoin balance, and you can't do that until they log in), then change the UI to indicate that such a request is in progress. For example, dim the UI, disable the controls, pop up an activity indicator view (a.k.a., a "spinner"), etc. Only then would you initiate the request. And upon completion of the request, you would restore the UI. I know it seems like a lot, but it's the right way to do it when the user absolutely cannot proceed until the prior request is done.
I'd also think long and hard as to whether it's truly the case that you absolutely have to force the user to wait for the prior network request to complete. Sometimes there are situations where you can let the user do/review something else while the network request is in progress. Yes, sometimes that isn't possible, but if you can find those sorts of opportunities in your app, you'll end up with a more elegant UX.
I know that problem and use this code for synchronous requests:
func synchronousRequest() -> NSDictionary {
//creating the request
let url: NSURL! = NSURL(string: "exampledomain/...")
var request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
var error: NSError?
var response: NSURLResponse?
let urlData = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
error = nil
let resultDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(urlData!, options: NSJSONReadingOptions.MutableContainers, error: &error) as! NSDictionary
return resultDictionary
}
What you are asking for is a synchronous network request. There are many ways to do this, such as...
NSData's init(contentsOfURL aURL: NSURL!)
NSURLConnection's synchronous request method
...etc.
These methods will block the current thread until they complete - which can be a potentially long time. Network requests can have very high timeouts, it may be several minutes before the device gives up. NSData's init with contents of URL will return NSData, not void, and does not execute asynchronously. It will block until it is complete, which is why it's recommended to not do these types of requests from the main thread. The UI will be frozen until it completes.
In general the use of synchronous networking methods is discouraged. Asynchronous network requests are greatly preferred for a number of reasons. Using an asynchronous method that takes a completion block as a parameter will not prevent you from using the returned data elsewhere in your application. The block is executed when the network request has finished (wether it succeeds or fails) and it is passed the data, response metadata, and error. You are free to do what you want with that data - nothing prevents you from persisting it, passing it off to another object, etc. Based on your comments it sounds like you want to take the data that was the result of the network request and set it as the value of a property somewhere - that is entirely doable using an asynchronous method that uses a block as a completion handler.
In objective-C you can use __block and get the data when the operation finishes:
__block NSData *myData;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:urlString]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
myData = data;
}] resume];

Switch between different API hosts

I'm working on an app which primarily works with an API that will be installed in an internal system. The API is also accessible via the public internet. The client wants to allow users to enter both an internal and external (public internet) URL that the app will then connect to depending on availability of the internal and external URLs.
The app is basically done with the exception that it currently connects to the internal URL only for all it's API calls. I'm using AFNetworking with block-based completion/failure invocations for each API call.
Based on the logic that we have designed, the app will always check for the API's availability by querying for the server's current time. This is done by calling http://internal_url/api/time. If this API fails to return an appropriate respond, we'll switch to the external URL http://external_url/api/time and call the same API on that URL. If both fails, the app will inform the user accordingly and not perform any other queries to the API.
Without revealing too much, here's some code on how I the API calls are currently setup:
- (void)someAPIMethodCall:(NSDictionary *)parameters completionBlock:block failure:block {
// query /api/time and return the URL (internal/external) that is currently up
AFHTTPClient *client = [AFHTTPClient clientWithBaseURL:<url returned from above query>];
[client operationWithSuccess:block failure:block];
}
So my question would be: what is the best way to get the query /api/time method above to work? Obviously, this method needs to complete and return either the internal/external URL so that the subsequent actual API query could use. AFAIK, AFNetworking calls are block-based so it will return before the above /api/time returns. I've also thought of a separate class that uses NSURLConnection synchronously which will block the main-thread while it waits for the /api/time to return.
I'd like to tell you to simply use the same URL internally and externally (via DNS) but that's not what you want.
I think you're asking how to conditionally call the other url.
You want someAPIMethodCall to be asynchronous... so you don't want to block on the call to checking for the correct api to call.
Aside from caching the results so you don't have to do this every time, you simply want to call another block based method of your own that has a completion block which passes IN a parameter of the URL to call for your real query.
- (void)someAPIMethodCall:(NSDictionary *)parameters completionBlock:(void (^)(void))succesBlock failure((^)(void)):failureBlock {
[self callBlockWithMyApiUrl:^(NSString *apiUrl){
AFHTTPClient *client = [AFHTTPClient clientWithBaseURL:apiUrl];
[client operationWithSuccess:successBlock failure:failureBlock];
} onFailure:^{
failureBlock
}
}
- (NSString *)callBlockWithMyApiUrl:(NSString * (^)(void))success (void (^)(void))failure
{
// Your code to test for the working URI
// If you're doing it this way, I'd suggest caching the result.
// Subscribe to networking interface changes to dump the cache.
}

To know date and time settings in device from an iPhone application

I want to know whether the user has set their device to automatically set the date and time in the Settings.app in iPhone. I need this information because I want to make user to access some informations in the app only for a day, which he can access again only on second day. But an smart user can change the date and access the information. So I want to check whether they set the date/time automatically for the device or not.
Is there any way for doing so. Please suggest any other alternate way if exist.
Thanks in advance.
I don't believe there exists a way to detect the user's preference regarding the automatic update of the date time settings. To determine the length of time from a given point, you have a few options none of which are absolute or without any error.
Use NSDate, which you already know can be circumvented by manually changing the date on the device.
Use a time server. By querying a time server you can get an independent date time value. Store that value and compare against it to determine if the right amount of time has passed. The drawback of course, is this requires an internet connection to function property.
Use a system clock value or series of values to roughly calculate the time elapsed. A functions like CACurrentMediaTime or mach_absolute_time() can provide a count to compare against. You can take the values and increment them until you have reached a specified duration to reset a flag for your second day check.
None of the options alone will provide an ideal solution, but by combining the approaches, you might achieve an acceptable level of assurance the user is accessing the information only during the time allowed.
This is an alternate suggestion, I had similar problem for a conference app I was developing. I wanted to show some data to user only in the second day of conference.
After discussing getting date option from device with our team. We decided not to use this approach because as you said some user may change date settings and also some user may be reluctantly set their device to other countries time and date zones where there may be date conflict issues for users.
Finally we decided to create a simple web page and get http 200 and http 404 request for that info. You dont have to put your data or whatever to the server, we just wanted to get the response code from web.
So lets say you have a webpage http://xxxxxxxxxxxxxx.pdf or .html we dont put any pdf files to server so user always get http 404 response in that case we disable or hide the related data/button/row whatever. In the second day of conference we put a dummy page so now user can get http 200 response, so we enable button/row/data.
if you decide to use this approach download block for nsurlconnection from here
then use following code:
NSURL *theFileURL=[NSURL URLWithString:#"yoururl"];
NSURLRequest *request = [NSURLRequest requestWithURL:theFileURL];
[URLConnection asyncConnectionWithRequest:request
completionBlock:^(NSData *data, NSURLResponse *response) {
//get data and response
if ([response isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *) response;
//NSString *fileMIMEType = [[httpResponse MIMEType] lowercaseString];
NSLog(#"httpResponse.statusCode %i",httpResponse.statusCode);
if (httpResponse.statusCode==200) {
//correct respond show data
}
else if (httpResponse.statusCode==404)
{
//no file this
NSLog(#"no file at the server");
}
}
//[HUD removeFromSuperview];
} errorBlock:^(NSError *error) {
//get error
NSLog(#"error %#",error);
} uploadPorgressBlock:^(float progress) {
//Upload progress (0..1)
} downloadProgressBlock:^(float progress) {
//Download progress (0.1)
//progress += 0.01;
//HUD.progress = progress;
}];

Resources