I'm trying to modify a behavior of a webpage within my iOS app and make the in-page media player play a file from the local caches folder instead of fetching it from a web server.
Below is my code that replaces the http:// video path with a local file path. The code does not work, giving me "Resource Temporary not available. Please try again" error message popup. Is it possible to have a web-based media player play file from a local disk using file URL?
I tried substituting these for the instanceURL, but they don't seem to work.
[fileURL path]
[fileURL absolutePath]
I'm intercepting the request for the file and am parsing it to find out that the page is asking for a video file:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
// An NSURLConnection delegate callback. We pass this on to the client.
{
NSDictionary* decisionDictionary = [[RequestListener sharedInstance] shouldContinue:connection processRequestData:data];
BOOL shouldContinue = [decisionDictionary[#"shouldContinue"] boolValue];
if(shouldContinue == NO)
{
return;
}else
{
NSData* d = data;
//substitute fake data
if(decisionDictionary[#"data"])
{
d = decisionDictionary[#"data"];
}
[[self client] URLProtocol:self didLoadData:d];
}
}
Within my shouldContinue method, I check if the video is present locally and modify the response data to create a path to a local video.
NSString* path = [VideoDownloader localVideoPathForVideoID:videoID];
NSURL* fileURL = [NSURL fileURLWithPath:path];
DLog(#"url:%#",[fileURL absoluteString]);
NSString* replacement = [NSString stringWithFormat:#"\"instanceUrl\":\"%#\"",[[fileURL absoluteURL] absoluteString]];
DLog(#"replacement:%#",replacement);
NSString* forgedResponse = [parts componentsJoinedByString:#","];
NSData* forgedData = [forgedResponse dataUsingEncoding:NSUTF8StringEncoding];
return #{#"shouldContinue":#(YES),#"data":forgedData};
Have a look at NSURLProtocol. You can intercept http requests before they are sent to a host to decide what to do about it: Continue to server, redirect to local cache.
There's a decent tutorial by our beloved Ray Wenderlich.
Apple has a programming guide as well.
Related
On iOS (14/15) I'm trying to pass security scoped bookmarks to user picked files on iCloud Drive between devices but whatever I try: I cannot get urls to be restored on another device running the same app.
The app is a UIDocument based app, the code below is in a UIViewController that display the document. The document is created like so:
Document* document = [[Document alloc] initWithFileURL:documentURL];
and then passed on to the ViewController
The url that's going to be bookmarked is picked using a plain UIDocumentPickerViewController
This is how I create a security scoped bookmark:
// Toggle this to create either a document scoped url or an app scoped url
static BOOL DOC_SCOPE = NO;
- (void) documentPicker:(UIDocumentPickerViewController*)controller didPickDocumentsAtURLs:(NSArray <NSURL *>*)urls {
if (urls.count > 0) {
NSURL* url = urls.firstObject;
NSURL* docURL = self.document.fileURL;
BOOL closeSource = [docURL startAccessingSecurityScopedResource];
BOOL doClose = [url startAccessingSecurityScopedResource];
NSData* bookmark = [url bookmarkDataWithOptions:0 includingResourceValuesForKeys:nil relativeToURL:DOC_SCOPE?docURL:nil error:nil];
if (doClose)
[url stopAccessingSecurityScopedResource];
if (closeSource)
[docURL stopAccessingSecurityScopedResource];
NSString* encoded = [NSString stringWithFormat:#"\n[%#]\n", [bookmark base64EncodedStringWithOptions:0]];
At this point I insert the encoded bookmark data in the document data and save the document.
When opening the linked document the bookmark and url are restored like so:
openLink:(NSString*)encoded {
NSData* bookmark = [[NSData alloc] initWithBase64EncodedString:encoded options:0];
NSURL* docURL = self.document.fileURL;
BOOL closeSource = [docURL startAccessingSecurityScopedResource];
NSError* error = nil;
NSURL* url = [NSURL URLByResolvingBookmarkData:bookmark options:0 relativeToURL:DOC_SCOPE?docURL:nil bookmarkDataIsStale:nil error:&error];
if (error != nil)
NSLog(#"%#", error.localizedFailureReason);
if (url != nil) {
BOOL doClose = [url startAccessingSecurityScopedResource];
// here use the url to access linked file
if (doClose)
[url stopAccessingSecurityScopedResource];
}
if (closeSource)
[docURL stopAccessingSecurityScopedResource];
}
To the project's entitlements I have added
com.apple.security.files.bookmarks.app-scope = 1
com.apple.security.files.bookmarks.document-scope = 1
When on the same device I can the restore the bookmark data and get access to the restored URL OK, but when opening the same file on another device, [NSURL URLByResolvingBookmarkData...] always sets an error:
Error Domain=NSCocoaErrorDomain Code=257 "The file couldn’t be opened because you don’t have permission to view it."
This is both the case for app scope book marks and document scope book marks.
Any idea what's missing / how to get this working?
I have downloaded a json file (data.json) using NSURLSession. I am trying to access this file from a local html file (main project folder) myfile.html which is displayed via UIWebView. From NSLog I have identified the temp file location is here:
file:///Users/administrator/Library/Developer/CoreSimulator/Devices/166B438D-4F67-448C-B0E5-B32438DA3BF9/data/Containers/Data/Application/FCA61482-BC5D-4804-91F8-891EAB4DB145/tmp/CFNetworkDownload_g6961Q.tmp
My question is how should I access this temp file from the local 'top level' html file, using a relative path?
More generally therefore - how does one access the iOS application file structure from such a top level (relative to the project) html file?
Some background info:
When I manually copy data.json to XCode and access it from myfile.html using the relative path 'data.json' it works.
The download code is:
NSURL *URL = [NSURL URLWithString:#"http://myurl.com/data.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request completionHandler:
^(NSURL *location, NSURLResponse *response, NSError *error) {
}
The reason I am doing this with NSURLSession is that I can't do it with Javascript in the html file since it is a cross domain request.
EDIT: Thanks for the helpful answers about adding HTML as a string into the UIWebView. I can see that trying to reference a temp file from the local HTML file is a bit unwieldly and may run counter to Apple's preference of not exposing filepaths - but I am still interested in a definitive answer on whether it can be done.
Perhaps you could inject the JSONData into the web page once it has finished loading within the web view. Something like this (I have not tested it):
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSError *error = nil;
NSURL *URL = [NSURL URLWithString:#"http://myurl.com/data.json"];
NSString *result = [NSString stringWithContentsOfURL:URL
encoding:NSUTF8StringEncoding error:&error];
if (!error && result.length > 0)
{
[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"var jsonData = \"%#\"", result]];
}
else
{
NSLog(#"Oops: %#", error);
}
}
I don't think you can either. Instead of loading your resource HTML as-is, use it as a "template" and put a "placeholder" string where you need the link to your temp file.
Then, load your HTML into a (mutable) string, replace the placeholder for the actual path to the file and pass this modified HTML string to your web view.
Both ForgeLogs and NSLogs aren't showing up in my Safari Web Inspector when I'm testing. Am I doing something wrong or is that intentional?
[ForgeLog d:#"Playing file at..."];
EDIT: Here's the rest of the code for context. (This time using NSLog.)
#import "audio_API.h"
static AVAudioPlayer* player = nil;
#implementation audio_API
+ (void)play:(ForgeTask*)task {
// parse the file url from the file object
ForgeFile* file = [[ForgeFile alloc] initWithFile:[task.params objectForKey:#"file"]];
NSString* fileURL = [file url];
NSLog(#"Playing file at %#", fileURL);
NSURL* url = [[NSBundle mainBundle] URLForResource:fileURL withExtension:#"m4a"];
// TESTING
//url = [[NSBundle mainBundle] URLForResource:#"seconds" withExtension:#"m4a"];
// END TESTING
NSAssert(url, #"URL is invalid.");
// create the player
NSError* error = nil;
player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if(!player)
{
NSLog(#"Error creating player: %#", error);
};
[player play];
[task success:nil];
}
ForgeLog and NSLog don't log to the web inspector console. When developing a plugin, this log output can be seen in XCode, when using a plugin as part of an app this output will appear in the Toolkit or commandline tool output.
If you want to make something appear in the web inspector from your plugin, you are probably best off communicating from native code to JavaScript data through an event: http://docs.trigger.io/en/v1.4/modules/native/javascript_events.html and use console.log in the event listener.
I have an mp3 file on a server. I want to get this file's information like what's the size of this file, what's the artists name, what's the album name, when was the file created, when was it modified, etc. I want all this information.
Is it possible to get this information without actually downloading the whole file? Using NSURLConnection or otherwise?
EDIT:
The following code doesn't give me the required information, i.e. file created by, artist name, etc
NSError *rerror = nil;
NSURLResponse *response = nil;
NSURL *url = [NSURL URLWithString:#"http://link.to.mp3"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"HEAD"];
NSData *result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&rerror];
NSString *resultString = [[[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding] autorelease];
NSLog(#"URL: %#", url);
NSLog(#"Request: %#", request);
NSLog(#"Result (NSData): %#", result);
NSLog(#"Result (NSString): %#", resultString);
NSLog(#"Response: %#", response);
NSLog(#"Error: %#", rerror);
if ([response isMemberOfClass:[NSHTTPURLResponse class]]) {
NSLog(#"AllHeaderFields: %#", [((NSHTTPURLResponse *)response) allHeaderFields]);
}
The "AllHeaderFields" is:
AllHeaderFields: {
"Cache-Control" = "max-age=0";
Connection = "keep-alive";
"Content-Encoding" = gzip;
"Content-Type" = "text/plain; charset=ascii";
Date = "Fri, 17 Feb 2012 12:44:59 GMT";
Etag = 19202n;
Pragma = public;
Server = dbws;
"x-robots-tag" = "noindex,nofollow";
}
It is quite possible to get the ID3 information embedded in an MP3 file (artist name, track title) without downloading the whole file or using low-level APIs. The functionality is part of the AVFoundation framework.
The class to look at is AVAsset and specifically it's network friendly subclass AVURLAsset. AVAsset has an NSArray property named commonMetadata. This commonMetadata property will contain instances of AVMetadataItem, assuming of course that the reference URL contains metadata. You will usually use the AVMetadataItem's commonKey property to reference the item. I find this method of iterating through an array checking commonKeys irritating so I create an NSDictionary using the commonKey property as the key and the value property as the object. Like so:
-(NSDictionary *)dictionaryOfMetadataFromAsset:(AVAsset *)asset{
NSMutableDictionary *metaData = [[NSMutableDictionary alloc] init];
for (AVMetadataItem *item in asset.commonMetadata) {
if (item.value && item.commonKey){
[metaData setObject:item.value forKey:item.commonKey];
}
}
return [metaData copy];
}
With the addition of this simple method the AVAsset's metadata becomes quite easy to use. Here is an example of getting an MP3's metadata through a URL:
NSURL *mp3URL = [NSURL URLWithString:#"http://'AddressOfMP3File'"];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:mp3URL options:nil];
NSDictionary *metaDict = [self dictionaryOfMetadataFromAsset:asset];
NSLog(#"Available Metadata :%#",metaDict.allKeys);
NSLog(#"title:%#",[metaDict objectForKey:#"title"]);
I have found that this code seems to load just the first few seconds of your MP3 file. Also note that this code is synchronous; So use with caution. But AVURLAsset does have some async functionality described in the docs.
Once you have the AVAsset you can create a AVPlayerItem with it and feed that to an AVPlayer and play it, or not.
Yes and no. Things like the file size and modification date often come as part of the HEAD response. But not always: with a lot of dynamic URLs, you won't get all of the information.
As for the artist and album name, they're part of the MP3's ID3, which is contained inside the file, and so you won't be able to get them with a HEAD request. Since the ID3 tag is typically at the beginning of a file, you could try to grab just that part and then read the ID3 tag. But you won't be able to do it with NSURLConnection since it doesn't support just fetching part of a file, so you'll need to find a more low-level way of getting data by HTTP.
Yep, you're right on target with NSURLConnection.
I think you want to send a HEAD request for the resource you want information about and then check the information you receive in connection:didReceiveResponse: and connection:didReceiveData:
Edit
Admittedly I didn't read your question in its entirety. It won't be possible to get ID3 information, but you should be able to get size of file and maybe creation date etc.
This answer does give some good information about how to get the ID3 information. You'd need to set up a php page to examine the mp3 file server-side and return just that information you require instead of the entire mp3.
Is it possible to use the:
[NSMutableArray writeToURL:(NSString *)path atomically:(BOOL)AuxSomething];
In order to send a file (NSMutableArray) XML file to a url, and update the url to contain that file?
for example:
I have an array and I want to upload it to a specific URL and the next time the app launches I want to download that array.
NSMutableArray *arrayToWrite = [[NSMutableArray alloc] initWithObjects:#"One",#"Two",nil];
[arrayToWrite writeToURL:
[NSURL urlWithString:#"mywebsite.atwebpages.com/myArray.plist"] atomically:YES];
And at runtime:
NSMutableArray *arrayToRead =
[[NSMutableArray alloc] initWithContentsOfURL:[NSURL urlWithString:#"mywebsite.atwebpages.com/myArray.plist"]];
Meaning, I want to write an NSMutableArray to a URL, which is on a web hosting service (e.g. batcave.net, the URL receives the information and updates server sided files accordingly.
A highscore like setup, user sends his scores, the server updates it's files, other users download the highscores at runtime.
As for part one of your question,
I'll assume you want to use the contents of a NSMutableArray to form some sort of a URL request (like POST) that you will send to your web service and expect back some information...
There is no prebuilt way of sending the contents of a NSMutableArray to an URL but there are simple ways of doing this yourself. For example, you can loop through the data of your array and make use of NSURLRequest to create a URL request that complies with the interface of your web service. Once you've constructed your request you can send it by passing it a NSURLConnection object.
Consider this very simple and incomplete example of what the client-side code might look like using an Obj-C array to provide data...
NSMutableData *dataReceived; // Assume exists and is initialized
NSURLConnection *myConnection;
- (void)startRequest{
NSLog(#"Start");
NSString *baseURLAddress = #"http://en.wikipedia.org/wiki/";
// This is the array we'll use to help make the URL request
NSArray *names = [NSArray arrayWithObjects: #"Jonny_Appleseed",nil];
NSString *completeURLAsString = [baseURLAddress stringByAppendingString: [names objectAtIndex:0]];
//NSURLRequest needs a NSURL Object
NSURL *completeURL = [NSURL URLWithString: completeURLAsString];
NSURLRequest *myURLRequest = [NSURLRequest requestWithURL: completeURL];
// self is the delegate, this means that this object will hanlde
// call-backs as the data transmission from the web server progresses
myConnection = [[NSURLConnection alloc] initWithRequest:myURLRequest delegate: self startImmediately:YES];
}
// This is called automatically when there is new data from the web server,
// we collect the server response and save it
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"Got some");
[dataReceived appendData: data];
}
// This is called automatically when transmission of data is complete
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// You now have whatever the server sent...
}
To tackle part 2 of your question, the receiver of a web request will likely require some scripting or infrastructure to make a useful response.
Here, Answer in this question:
Creating a highscore like system, iPhone side
I couldn't edit my post because I posted from my iPhone as an anonymous user, sorry.