Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cordova external app + local video

We have an iOS app built with PhoneGap / Cordova 4.3.0. This app directly loads an external website by using <content src="http://example.com/foo" /> in the config.xml file. All the functionality is contained within this website, so we are not actually using any local HTML or JS files.

As part of the app functionality, we must play some videos. Because the app is designed to work offline as well, we want to cache these videos locally. Therefore we are downloading them to the device using the FileTransfer plugin, along with other resources such as images or PDFs. After downloading the files we are getting URLs with the file:// protocol. We also have the option of using the cdvfile:// protocol. When we use cdvfile:// URLs to show images, the images show up correctly. However, videos do not play at all.

To play videos we are using standard HTML5 video tags:

<video width="auto" height="100%" controls="controls" autoplay="true">
    <source src="..." type="video/mp4" />
</video>

The videos themselves are working, and they will play correctly from an external source (as in, they will play if we are accessing them from the server instead of the local filesystem). I realize that the problem has something to do with web-related concepts such as the same-origin policy and with the restriction to access the local filesystem. However, at the same time I must wonder why it is that images are working fine under these same constraints.

What I have tried so far:

  1. Using file:// and cdvfile:// URLs as the src of the video. This does not produce any sort of visual effect. The screen is simply black.
  2. Using an iframe with the src set to the video URL. When using file://, the screen is still black. However, when using cdvfile://, the iOS video player interface appears, with a play button and a full-screen button, but the video does not play and there is no timeline either.
  3. Adding a local file to cordova called video.html which takes a URL as parameter and renders a video tag with that URL as src. The plan was to include this file as an iframe, but apparently I can't make an iframe to a local file. I have tried various URLs that might point to that particular video.html file (though in truth I'm not sure this is possible). Among the ones I tried were: cordova.file.applicationDirectory + 'www/video.html', http://localhost/www/video.html, cdvfile://localhost/www/video.html.
  4. I looked for some cordova plugin that will play videos, but I have been unsuccessful in finding one for iOS. Most plugins seem to be targeted towards Android.

Now, it's possible that I'm going about this in a wrong way. As I see it, the "standard use case" for cordova is that you store your HTML/JS/CSS files locally. External content like the one I'm using is probably a bit unusual. I will explain the requirements for this app that have brought me to use this functionality.

  • The app is supposed to be built for multiple platforms (though we're starting with iOS). Therefore we are using PhoneGap.
  • It is supposed to be accessible both online and offline, though all the content comes from the server (no content is produced locally). This is why we are downloading content and saving it locally.
  • It is also supposed to auto-update any part of itself on the fly, without requiring an update from the App Store. This is why we are using an external page - because it has a cache.manifest that allows us to control updates to the web app code, while at the same time allowing it to be cached locally. This is probably the most important thing to consider, because if we wanted to keep some files locally within Cordova, we would have to re-implement this cache functionality within Javascript (using as thin a layer as possible).

In any case, my main concern is how to get these videos working. I am willing to try the hackiest workarounds! If it's really not possible with the current development decisions, then maybe you could give me some hints as to how I should structure the app in order to make it work and still fulfill my requirements.

Thank you very much!

like image 329
Grampa Avatar asked Apr 05 '15 09:04

Grampa


1 Answers

I had a similar project about a year ago. But i don't remember running into this issue because we were bundling our html/js/css assets with the app.

The issue is that your are trying to load file:/// protocol url from an html file served from http:/// protocol, which is not something that the native UIWebView is comfortable with.

You can bypass this by using a custom URL scheme like video://, but you need to write some native code which intercepts this requests and pipes the actual video back to URL loading system.

The end result:

The end result

Here is how i did it using Cordova 4.3.0 & a bit of ObjectiveC

  1. Create a new Objective C class named VideoURLProtocol which extends NSURLProtocol:

VideoURLProtocol.h:

#import <Foundation/Foundation.h>

@interface VideoURLProtocol : NSURLProtocol <NSURLConnectionDelegate>

@property (strong, nonatomic) NSURLConnection *connection;

@end

VideoURLProtocol.m:

#import "VideoURLProtocol.h"

@implementation VideoURLProtocol

@synthesize connection;

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    return [[[request URL] absoluteString] rangeOfString:@"video://"].location != NSNotFound;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

- (void)startLoading {
    NSString *currentURL = [[self.request URL] absoluteString];
    NSString *newURL = [currentURL stringByReplacingOccurrencesOfString:@"video://" withString:@"file:///"];
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:newURL]];
    self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
}

- (void)stopLoading {
    [self.connection cancel];
    self.connection = nil;
}

@end
  1. Add the following line to the didFinishLaunchingWithOptions method of AppDelegate.m

    .
    .
    // These lines should already be there
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    
    // This line
    [NSURLProtocol registerClass:[VideoURLProtocol class]];
    .
    .    
    
  2. And here's the javascript code that utilises this new URL scheme

    document.addEventListener('deviceready', function(){    
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fileSystem){        
            var caches = fileSystem.root.nativeURL.replace("Documents", "Library/Caches"), 
                videoPath = caches + "video.mp4";
            new FileTransfer().download("http://clips.vorwaerts-gmbh.de/VfE_html5.mp4", videoPath, function(entry){            
                document.getElementsByTagName("video")[0].src = caches.replace("file:///", "video://") + "video.mp4"
            }, function(error){
                alert("unable to download file: " + error);
            });
        });
    }, false);
    

Some additional points worth mentioning:

Notice in my javascript code i am downloading the file to "/Library/Caches" instead of the "/Documents" directory (the default location) this is because the "/Documents" directory gets backed up to iCloud & Apple rejects apps that try to backup more than ~100 MB. This is something I found the hard way after getting my App rejected. You can see the space taken by your app under: Settings > iCloud > Storage > Manage Storage > {{Your iphone name}} > Show All Apps

You can autoplay videos by add the following line to your config.xml

<preference name="MediaPlaybackRequiresUserAction" value="false" />    

You can also play videos inline by adding the following line to your config.xml in addition to this you also need to add webkit-playsinline="true" attribute to your video:

<preference name="AllowInlineMediaPlayback" value="true" />

<video controls="controls" autoplay="true" webkit-playsinline="true" preload="auto">
</video>

Here's a really interesting tutorial by Ray that explains NSURLProtocol in great detail: http://www.raywenderlich.com/59982/nsurlprotocol-tutorial

like image 141
Furqan Zafar Avatar answered Sep 28 '22 07:09

Furqan Zafar