Question

I am trying to play video that is coming from an IP camera in iOS, but currently I tried 2 methods and they both seem to be filling up the memory of my iOS device really fast. I am using ARC for this project.

My IP camera uses Videostream.cgi (Foscam), which is a well-known way for IP cameras to stream 'video' through the browser.

So, I tried 3 ways, which all end up in crashing my iOS app, with an out-of-memory exception.

1. Putting an UIWebView on my UIViewController and call the CGI directly using a NSURLRequest.

NSString* url = [NSString stringWithFormat:@"http://%@:%@/videostream.cgi?user=%@&pwd=%@&rate=0&resolution=%ld", camera.ip, camera.port, camera.username, camera.password, (long)_resolution];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
webView = [[UIWebView alloc] init];
[webView loadRequest:request];

2. Putting an UIWebView on my UIViewController and creating a piece of HTML (in code) which includes a <img> tag which has a source to the CGI mentioned before. (see: IP camera stream with UIWebview works on IOS 5 but not on IOS 6)

NSString* imgHtml = [NSString stringWithFormat:@"<img src='%@'>", url];
webView = [[UIWebView alloc] init];
[webView loadHTMLString:imgHtml];

3. Using a custom control, based on a UIImageView, which fetches data continuously. https://github.com/mateagar/Motion-JPEG-Image-View-for-iOS

All of these things burn through memory and even when I try to remove them and re-add them after a certain period of time, but this does not seem to fix the problem. Memory won't be released and the iPad crashes.

UPDATE:

I am currently modifying option 3 of the solutions I tried. It is based on a NSURLConnection and the data it retrieves.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

    if (!_receivedData) {
        _receivedData = [NSMutableData new];
    }

    [_receivedData appendData:data];

    NSRange endRange = [_receivedData rangeOfData:_endMarkerData 
                                          options:0 
                                            range:NSMakeRange(0, _receivedData.length)];

    NSUInteger endLocation = endRange.location + endRange.length;
    if (_receivedData.length >= endLocation) {

        NSData *imageData = [_receivedData subdataWithRange:NSMakeRange(0, endLocation)];
        UIImage *receivedImage = [UIImage imageWithData:imageData];
        if (receivedImage) {
            NSLog(@"_receivedData length: %d", [_receivedData length]);
            self.image = receivedImage;
            _receivedData = nil;
            _receivedData = [NSMutableData new];
        }
    }

    if (_shouldStop) {
        [connection cancel];
    }
}

_receivedData is a NSMutableData object. which I try to "empty" once an image is retrieved from the stream. The part in if (receivedImage) is called when it is supposed to be called. The length of the _receivedData object is also not increasing, it stays around the same size (~ 14k), so that seems to work.

But somehow, with every didReceiveData the memory my app is using increases, even when I disable the line self.image = receivedImage.

UPDATE As iosengineer suggested, I have been playing with autorelease pools, but this does not solve the problem.

Using Instruments I found out that most of the allocations are done by CFNetwork, in the method HTTPBodyData::appendBytes(unsigned char const*, long). (This allocates 64KB at a time and keeps them alive).

Was it helpful?

Solution 2

I rewrote the MotionJpegImageView thing, which was causing all my problems:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    if (!_receivedData) {
        _receivedData = [NSMutableData new];
    }

    [_receivedData appendData:data];

    NSRange endRange = [_receivedData rangeOfData:_endMarkerData
                                          options:0
                                            range:NSMakeRange(0, _receivedData.length)];

    if (endRange.location == NSNotFound) {
        return;
    }

    @autoreleasepool {
        UIImage *receivedImage = [UIImage imageWithData:_receivedData];
        if (receivedImage) {
            self.image = receivedImage;
        }
        else {
            DDLogVerbose(@"Invalid image data");
        }
    }

    [_receivedData setLength:0];

    if (_shouldStop) {
        [connection cancel];
        DDLogVerbose(@"Should stop connection");
    }
}

Also, my connections were opened multiple times in the end by not correctly canceling the old ones. Pretty stupid mistake, but for the people wanting to know how it works. Code is mentioned above.

OTHER TIPS

The next step I'd take would be to analyse the request/response patterns using Charlie, step through the source using Xcode and probably write my own solution using NSURLSession and NSURLRequest.

Streams don't just create themselves - something is pulling in data from the responses and not getting rid of it fast enough.

Here's my guess on what is possibly happening:

When you download something using NSURLRequest, you create an instance of NSMutableData to collect the responses in chunks until you are ready to save it to disk. In this case, the stream never ends and so the store grows massive and then bails.

A custom solution to this would have to know when it's safe to ditch the store based on the end of a frame (for example). Good Luck! Instruments is your friend.

P.S. Beware of autoreleased memory - use autoreleasepools wisely


In your revised question, the code sample shows a few objects that are created using autoreleased memory. The appropriate use of autorelease pools should fix this. It should be fairly straight-forward to see which object is causing the most problems and if your problems have been solved by profiling the app using Instruments (allocation tool).

Of particular interest, the UIImage imageWithData: call should definitely be wrapped, as that is creating a new image object every time.

Also subdataWithRange creates a new object which is only released once the pool is flushed.

I don't use the "new" syntax for creation ever so I can't recall how that one actually works. I always use alloc init.

Wrap MOST of this whole routine with this:

@autoreleasepool
{
  ROUTINE
}

That will make it so that at each chunk of data received, the pool will be drained and you will mop up any autoreleased memory objects.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top