So far I know how to stream a video and how to download it and afterwards stream it, but here's the tricky bit: streaming it once, storing it on the device and in the future play it from the device.
Is that possible?
Not quite sure here how you get your stream but look in to the AVAssetWriter, AVAssetWriterInput and AVAssetWriterPixelBufferAdaptor and as soon as you receive data you should be able to append the data to the to the pixel buffer adaptor using:
appendPixelBuffer:withPresentationTime:
not sure it will work for you but with some fiddling you should be able to adapt your input to match this method. There are lots of example code for setting up the writer
It's quite easy to save the video. Do something similar to this:
//Saving Movie
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:*MovieObject* forKey:#"MovieObjectDataKey"];
[archiver finishEncoding];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"MovieObjectDefaultsDataKey"];
[archiver release];
[data release];
//Retrieving movie
NSData *savedMovieData = [[NSUserDefaults standardUserDefaults] objectForKey:#"MovieObjectDefaultsDataKey"];
if (savedMovieData != nil) {
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:savedMovieData];
*MovieObject* = [[unarchiver decodeObjectForKey:#"MovieObjectDataKey"] retain];
[unarchiver finishDecoding];
[savedMovieData release];
[unarchiver release];
} else {
//Download Stream of Your Movie
}
The only thing you really have to change there is * MovieObject *, once in each step.
I know what you want to achieve, I only got a workaround. I had to implement the same behavior and ended up with streaming the video from the server and downloading it next to streaming. Next time the user tries to stream the video determine whether it was downloaded to disk, otherwise stream it again. In a normal case the video was downloaded properly and could be reviewed offline.
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:somePath];
and
fileURLWithPath:isDirectory:
Initializes and returns a newly created NSURL object as a file URL with a specified path.
+ (id)fileURLWithPath:(NSString *)path isDirectory:(BOOL)isDir
Parameters
path
The path that the NSURL object will represent. path should be a valid system path. If path begins with a tilde, it must first be expanded with stringByExpandingTildeInPath. If path is a relative path, it is treated as being relative to the current working directory.
Passing nil for this parameter produces an exception.
isDir
A Boolean value that specifies whether path is treated as a directory path when resolving against relative path components. Pass YES if the path indicates a directory, NO otherwise.
Return Value
An NSURL object initialized with path.
Availability
Available in iOS 2.0 and later.
You can't stream it and save it at the same time, especially with large video files as the Apple doc sais that you must use a transport stream for HTTP Live Streaming.
ASIHttpRequest might make your life easier.
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDownloadDestinationPath:#"video.m4v"]; // use [NSBundle mainBundle] to find a better place
From your delegate, handle this:
- (void)request:(ASIHTTPRequest *)request didReceiveData:(NSData *)data;
Do whatever data transcoding with data as you get it and push it off to your AVAssetWriter or movie player layer in real time, whatever you are using. When you're done, the asset should still be saved so you can get it later.
Related
Suppose I have some data I want to post to a server. You're in a deep, deep underground cellar (or you use t-mobile) and you have no internet connection. Where does that data go? Just leave it there and wait for a connection? I would rather save that data, and make it available for upload the second you have a connection.
Would using SQlite be the best way of doing this? It's just a couple of objects i want to temporarily store.
If it's only a couple of objects either save the data in NSUserDefaults or in a plist file.
NSUserDefaults set:
[[NSUserDefaults standardUserDefaults] setObject:savedStr forKey:objectDataStoreName];
[[NSUserDefaults standardUserDefaults] synchronize];
NSUserDefaults get:
savedStr = [[NSUserDefaults standardUserDefaults] objectForKey:objectDataStoreName];
or use a plist:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *newDirectory = [NSString stringWithFormat:#"%#/savedData", [paths objectAtIndex:0]];
[[NSFileManager defaultManager] createDirectoryAtPath:newDirectory withIntermediateDirectories:YES attributes:nil error:nil];
NSString *fullFileName = [NSString stringWithFormat:#"%#/savedItemsFile", newDirectory];
savedItems = [[NSMutableArray alloc] initWithContentsOfFile:fullFileName];
if (!savedItems) {
savedItems = [[NSMutableArray alloc] init];
}
savedItem = [[NSMutableDictionary alloc] init];
[savedItem setObject:savedStr forKey:objectDataStoreName];
[savedItems addObject:savedItem];
[savedItems writeToFile:fullFileName atomically:NO];
Well that is up to you, apple specifies you need to check if there is an internet connection available. And you will receive error's if you request fails.
If you can send the data later just save it and retry later. If you can't send the data on a later date then inform the user that the data could not be send.
It all depends on the specifications of the app.
I've uses plist, SQLite and coredata to keep data on the device and send them to the server when a network connection becomes available.
If you all ready store the data in database just add a bit which tells you if you have send the data to a server, or beter yet get the server to return you an identifier for the upload so you may update some record later on.
If it is not something big you can use NSUserDefaults
If it is something big I recommend you to use Core Data
My main goal is to stream a video from a server, and cut it frame by frame while streaming (so that it can be used by OpenGL). For that, I've used this code that I found everywhere on the Internet (as I recall it was from Apple's GLVideoFrame sample code):
NSArray * tracks = [asset tracks];
NSLog(#"%d", tracks.count);
for(AVAssetTrack* track in tracks) {
NSLog(#"type: %#", [track mediaType]);
initialFPS = track.nominalFrameRate;
width = (GLuint)track.naturalSize.width;
height = (GLuint)track.naturalSize.height;
NSError * error = nil;
// _movieReader is a member variable
#try {
self._movieReader = [[[AVAssetReader alloc] initWithAsset:asset error:&error] autorelease];
}
#catch (NSException *exception) {
NSLog(#"%# -- %#", [exception name], [exception reason]);
NSLog(#"skipping track");
continue;
}
if (error)
{
NSLog(#"CODE:%d\nDOMAIN:%#\nDESCRIPTION:%#\nFAILURE_REASON:%#", [error code], [error domain], error.localizedDescription, [error localizedFailureReason]);
continue;
}
NSString* key = (NSString*)kCVPixelBufferPixelFormatTypeKey;
NSNumber* value = [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA];
NSDictionary* videoSettings = [NSDictionary dictionaryWithObject:value forKey:key];
[_movieReader addOutput:[AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track
outputSettings:videoSettings]];
[_movieReader startReading];
[self performSelectorOnMainThread:#selector(frameStarter) withObject:nil waitUntilDone:NO];
}
But I always get this exception at [[AVAssetReader alloc] initWithAsset:error:].
NSInvalidArgumentException -- *** -[AVAssetReader initWithAsset:error:] Cannot initialize an instance of AVAssetReader with an asset at non-local URL 'http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8'
So my two questions are:
Is the exception really telling me that AVAssetReader must have a local URL? Can it be used for streaming (just like the rest of the AVFoundation classes)?
If the AVFoundation approach won't work, what are other suggestions to stream the video and split its frames at the same time?
Thanks a lot for your help.
AVFoundation does not seem to distinguish as much between local and non-local files, as it does between the KIND of files or protocols used. There is a VERY clear distinction between using mp4/mov's versus using the HTTP Live streaming protocol via m3u8's, but the differences using a local or remote mp4 are a little fuzzier.
To expand on the above:
a) If your 'remote' asset is an M3U8 (that is, you are using HTTP 'live' streaming), then no chance whatsoever. No matter if the M3U8 is in your local filesystem or on a remote server, for a multitude of reasons AVAssetReader and all AVAsset-associated functionality just does NOT work. However, AVPlayer, AVPlayerItem etc would work just fine.
b) If it is an MP4/MOV, a little further investigation is due. Local MP4/MOV's work flawlessly. While in case of remote MP4/MOV's, I'm able to create (or retrieve from an AVPlayerItem or AVPlayer or AVAssetTracks) an AVURLAsset with which I'm sometimes able to initialize an AVAssetReader successfully (I'll expand on the 'sometimes' as well, shortly). HOWEVER, copyNextSampleBuffer always returns nil in case of remote MP4's. Since several things UPTO the point of invoking copyNextSampleBuffer work, I'm not 100% sure if:
i) copyNextSampleBuffer not working for remote mp4's, after all the other steps having been successful, is intended/expected functionality.
ii) That the 'other steps' seem to work at all for remote MP4's is an accident of Apple's implementation, and this incompatibility is simply coming to the fore when we hit copyNextSampleBuffer..............what these 'other steps' are, I'll detail shortly.
iii) I'm doing something wrong when trying to invoke copyNextSampleBuffer for remote MP4's.
So #Paula you could try to investigate a little further with remote MOV/MP4's.
For reference, here are the approaches I tried for capturing a frame from videos:
a)
Create an AVURLAsset directly from the video URL.
Retrieve the video track using [asset tracksWithMediaType:AVMediaTypeVideo]
Prepare an AVAssetReaderTrackOutput using the video track as the source.
Create an AVAssetReader using the AVURLAsset.
Add AVAssetReaderTrackOutput to the AVAssetReader and startReading.
Retrieve images using copyNextSampleBuffer.
b)
Create an AVPlayerItem from the video URL, and then an AVPlayer from it (or create the AVPlayer directly from the URL).
Retrieve the AVPlayer's 'asset' property and load its 'tracks' using "loadValuesAsynchronouslyForKeys:".
Separate the tracks of type AVMediaTypeVideo (or simply call tracksWithMediaType: on the asset once the tracks are loaded), and create your AVAssetReaderTrackOutput using the video track.
Create AVAssetReader using the AVPlayer's 'asset', 'startReading' and then retrieve images using copyNextSampleBuffer.
c)
Create an AVPlayerItem+AVPlayer or AVPlayer directly from the video URL.
KVO the AVPlayerItem's 'tracks' property, and once the tracks are loaded, separate the AVAssetTracks of type AVMediaTypeVideo.
Retrieve the AVAsset from AVPlayerItem/AVPlayer/AVAssetTrack's 'asset' property.
Remaining steps are similar to approach (b).
d)
Create an AVPlayerItem+AVPlayer or AVPlayer directly from the video URL.
KVO the AVPlayerItem's 'tracks' property, and once the tracks are loaded, separate the ones of type AVMediaTypeVideo.
Create an AVMutableComposition, and initialize an associated AVMutableCompositionTrack of type AVMediaTypeVideo.
Insert the appropriate CMTimeRange from video track retrieved earlier, into this AVMutableCompositionTrack.
Similar to (b) and (c), now create your AVAssetReader and AVAssetReaderTrackOutput, but with the difference that you use the AVMutableComposition as the base AVAsset for initializing your AVAssetReader, and AVMutableCompositionTrack as the base AVAssetTrack for your AVAssetReaderTrackOutput.
'startReading' and use copyNextSampleBuffer to get frames from the AVAssetReader.
P.S: I tried approach (d) here to get around the fact that the AVAsset retrieved directly from AVPlayerItem or AVPlayer was not behaving. So I wanted to create a new AVAsset from the AVAssetTracks I already had in hand. Admittedly hacky, and perhaps pointless (where else would the track information be ultimately retrieved from if not the original AVAsset!) but it was worth a desperate try anyway.
Here's a summary of the results for different types of files:
1) Local MOV/MP4's - All 4 approaches work flawlessly.
2) Remote MOV/MP4's - The asset and tracks are retrieved correctly in approaches (b) through (d), and the AVAssetReader is initialized as well but copyNextSampleBuffer always returns nil. In case of (a), creation of the AVAssetReader itself fails with an 'Unknown Error' NSOSStatusErrorDomain -12407.
3) Local M3U8's (accessed through an in-app/local HTTP server) - Approaches (a), (b) and (c) fail miserably as trying to get an AVURLAsset/AVAsset in any shape or form for files streamed via M3U8's is a fools errand.
In case of (a), the asset is not created at all, and the initWithURL: call on AVURLAsset fails with an 'Unknown Error' AVFoundationErrorDomain -11800.
In case of (b) and (c), retrieving the AVURLAsset from the AVPlayer/AVPlayerItem or AVAssetTracks returns SOME object, but accessing the 'tracks' property on it always returns an empty array.
In case of (d), I'm able to retrieve and isolate the video tracks successfully, but while trying to create the AVMutableCompositionTrack, it fails when trying to insert the CMTimeRange from the source track into the AVMutableCompositionTrack, with an 'Unknown Error' NSOSStatusErrorDomain -12780.
4) Remote M3U8's, behave exactly the same as local M3U8's.
I'm not entirely educated on why these differences exist, or could not have been mitigated by Apple. But there you go.
You can get a remote file on AVMutableCompositionTrack
AVURLAsset* soundTrackAsset = [[AVURLAsset alloc]initWithURL:[NSURL URLWithString:#"http://www.yoururl.com/yourfile.mp3"] options:nil];
AVMutableCompositionTrack *compositionAudioSoundTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
[compositionAudioSoundTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset.duration)
ofTrack:[[soundTrackAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0]
atTime:kCMTimeZero error:nil];
However, this approach does not work very well with files that have a higher compression like MP4s
I'm implementing a manually-triggered migration process for a CoreData-based app, and after the migration completes successfully, I'm trying to move the migrated DB back over the top of the original one using replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error:.
The problem is that on iOS, nothing I do will make this method return YES, however it also never puts anything into the error pointer to allow you to see what's going wrong.
I'd read things elsewhere (e.g. http://www.cocoabuilder.com/archive/cocoa/287790-nsdoc-magic-file-watcher-ruins-core-data-migration.html) indicating that not shutting down all the CoreData objects (e.g. NSMigrationManager, NSManagedObjectModel etc) before attempting the replace might be the cause, but that wasn't it. I even implemented a little two file create-and-swap thing that didn't involve CoreData DBs at all to verify that the CoreData stuff didn't have anything to do with it.
I then noticed in the official documentation that the newitemURL is supposed to be in a directory deemed appropriate for temporary files. I assumed that that meant a directory returned by URLForDirectory:inDomain:appropriateForURL:create:error: using NSItemReplacementDirectory as the search path.
That didn't work either! I ended up falling back to implementing the replacement logic using separate operations, but this is non-atomic and unsafe and all that bad stuff.
Does anyone have a working snippet of code that runs on iOS that either return YES from a call to replaceItemAtURL or actually puts error information into the error pointer?
Any help much appreciated.
EDIT - Test code included below. This runs in application:didFinishLaunchingWithOptions: on the main thread.
NSFileManager *fm = [[NSFileManager alloc] init];
NSError *err = nil;
NSURL *docDir = [NSURL fileURLWithPath:[self applicationDocumentsDirectory]];
NSURL *tmpDir = [fm URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:docDir
create:NO
error:&err];
NSURL *u1 = [docDir URLByAppendingPathComponent:#"f1"];
NSURL *u2 = [tmpDir URLByAppendingPathComponent:#"f2"];
NSURL *repl = nil;
[fm createFileAtPath:[u1 path]
contents:[[NSString stringWithString:#"Hello"]
dataUsingEncoding:NSUTF8StringEncoding]
attributes:nil];
[fm createFileAtPath:[u2 path]
contents:[[NSString stringWithString:#"World"]
dataUsingEncoding:NSUTF8StringEncoding]
attributes:nil];
BOOL test = [fm replaceItemAtURL:u1 withItemAtURL:u2 backupItemName:#"f1backup"
options:0 resultingItemURL:&repl error:&err];
// At this point GDB shows test to be NO but error is still nil
I have experienced issues with all the NSFileManager methods using an URL on iOS. However, all the methods using Path work. So I think you should use removeItemAtPath:error:and copyItemAtPath:toURL:error: for that purpose.
Hope it helps
In mac file system is not case sensitive, but in IOS it. Even though you cant have two files with same name but with different case at one location, the path is case sensitive. So if file is has .JPEG and in your code you are passing link with .jpeg it will fail.
It may not be the case with you but just what to share
Although strangely it should give you error.
I have alot of wav files stored in sqlite3, but when I retrieve one of them, I can't play it. The retrieve code is
NSData *soundData = (NSDATA *)sqlite3_column_blob(statement, 0);
mPlayer = [[AVAudioPlayer alloc] initWithData:soundData error:&error];
The data is stored as binary and it's there when I search for it using sqlite3.
Sorry. Never mind. I just compressed the data more and it works fine now. Seems the number of files is not as important as their size afterall.
My app is accessing data from a remote XML file. I have no issues receiving and parsing the data. However, I'd like to take the most current XML data and store it locally so - in the event that the user's internet service isn't available - the local data from the previous load is used.
Is there a simple way to do this? Or am I going to have to create an algorithm that will create a plist as the xml data is parsed? That seems rather tedious... I was wondering if there was an easier way to save the data as a whole.
Thanks in advance!
I don't know what format your XML data is in as you receive it, but using NSData might be helpful here, because it has very easy-to-use methods for reading/writing data from either a URL or a pathname.
For example:
NSURL *url = [NSURL URLWithString:#"http://www.fubar.com/sample.xml"];
NSData *data = [NSData dataWithContentsOfURL:url]; // Load XML data from web
// construct path within our documents directory
NSString *applicationDocumentsDir =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *storePath = [applicationDocumentsDir stringByAppendingPathComponent:#"sample.xml"];
// write to file atomically (using temp file)
[data writeToFile:storePath atomically:TRUE];
You can also easily convert an NSData object to/from a raw buffer (pointer/length) in memory, so if your data is already downloaded you might do:
NSData *data = [NSData dataWithBytes:ptr length:len]; // Load XML data from memory
// ... continue as above, to write the NSData object to file in Documents dir