/*
* Copyright (C) 2013 Greg Perry
*
* Licensed either under the Apache License, Version 2.0, or (at your option)
* under the terms of the GNU General Public License as published by
* the Free Software Foundation (subject to the "Classpath" exception),
* either version 2, or any later version (collectively, the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.gnu.org/licenses/
* http://www.gnu.org/software/classpath/license.html
*
* or as provided in the LICENSE.txt file that accompanied this code.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bytedeco.javacv;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.Loader;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.TimeUnit;
import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_imgcodecs.*;
public class IPCameraFrameGrabber extends FrameGrabber {
/*
* excellent reference - http://www.jpegcameras.com/ foscam url
* http://host/videostream.cgi?user=username&pwd=password
* http://192.168.0.59:60/videostream.cgi?user=admin&pwd=password android ip
* cam http://192.168.0.57:8080/videofeed
*/
private static Exception loadingException = null;
public static void tryLoad() throws Exception {
if (loadingException != null) {
throw loadingException;
} else {
try {
Loader.load(org.bytedeco.javacpp.opencv_highgui.class);
} catch (Throwable t) {
throw loadingException = new Exception("Failed to load " + IPCameraFrameGrabber.class, t);
}
}
}
private final FrameConverter converter = new OpenCVFrameConverter.ToIplImage();
private final URL url;
private final int connectionTimeout;
private final int readTimeout;
private DataInputStream input;
private byte[] pixelBuffer = new byte[1024];
private IplImage decoded = null;
/**
* @param url The URL to create the camera connection with.
* @param startTimeout How long should this wait on the connection while trying to {@link #start()} before
* timing out.
* If this value is less than zero it will be ignored.
* {@link URLConnection#setConnectTimeout(int)}
* @param grabTimeout How long should grab wait while reading the connection before timing out.
* If this value is less than zero it will be ignored.
* {@link URLConnection#setReadTimeout(int)}
* @param timeUnit The time unit to use for the connection and read timeout.
* If this value is null then the start timeout and grab timeout will be ignored.
*/
public IPCameraFrameGrabber(URL url, int startTimeout, int grabTimeout, TimeUnit timeUnit) {
super(); // Always good practice to do this
if (url == null) {
throw new IllegalArgumentException("URL can not be null");
}
this.url = url;
if (timeUnit != null) {
this.connectionTimeout = toIntExact(TimeUnit.MILLISECONDS.convert(startTimeout, timeUnit));
this.readTimeout = toIntExact(TimeUnit.MILLISECONDS.convert(grabTimeout, timeUnit));
} else {
this.connectionTimeout = -1;
this.readTimeout = -1;
}
}
public IPCameraFrameGrabber(String urlstr, int connectionTimeout, int readTimeout, TimeUnit timeUnit) throws MalformedURLException {
this(new URL(urlstr), connectionTimeout, readTimeout, timeUnit);
}
/**
* @param urlstr A string to be used to create the URL.
* @throws MalformedURLException if the urlstr is a malformed URL
* @deprecated By not setting the connection timeout and the read timeout if your network ever crashes
* then {@link #start()} or {@link #grab()} can hang for upwards of 45 to 60 seconds before failing.
* You should always explicitly set the connectionTimeout and readTimeout so that your application can
* respond appropriately to a loss or failure to connect.
*/
@Deprecated
public IPCameraFrameGrabber(String urlstr) throws MalformedURLException {
this(new URL(urlstr), -1, -1, null);
}
@Override
public void start() throws Exception {
try {
/*
* We don't need to keep a reference to the connection
* after it is opened in the parent class.
* It never uses it outside of start.
*/
final URLConnection connection = url.openConnection();
// If the class was initialized with timeout values then configure those
if (connectionTimeout >= 0) {
connection.setConnectTimeout(connectionTimeout);
}
if (readTimeout >= 0) {
connection.setReadTimeout(readTimeout);
}
input = new DataInputStream(connection.getInputStream());
} catch (IOException e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public void stop() throws Exception {
if (input != null) {
try {
input.close();
} catch (IOException e) {
throw new Exception(e.getMessage(), e);
} finally {
// Close may have failed but there's really nothing we can do about it at this point
input = null;
// Don't set the url to null, it may be needed to restart this object
releaseDecoded();
}
}
}
@Override
public void trigger() throws Exception {
}
@Override
public Frame grab() throws Exception {
try {
final byte[] b = readImage();
final CvMat mat = cvMat(1, b.length, CV_8UC1, new BytePointer(b));
releaseDecoded();
return converter.convert(decoded = cvDecodeImage(mat));
} catch (IOException e) {
throw new Exception(e.getMessage(), e);
}
}
public BufferedImage grabBufferedImage() throws IOException {
BufferedImage bi = ImageIO.read(new ByteArrayInputStream(readImage()));
return bi;
}
/**
* Ensures that if the decoded image is not null that it gets released and set to null.
* If the image was not set to null then trying to release a null pointer will cause a
* segfault.
*/
private void releaseDecoded() {
if (decoded != null) {
cvReleaseImage(decoded);
decoded = null;
}
}
private byte[] readImage() throws IOException {
final StringBuffer sb = new StringBuffer();
int c;
// read http subheader
while ((c = input.read()) != -1) {
if (c > 0) {
sb.append((char) c);
if (c == 13) {
sb.append((char) input.read());// '10'+
c = input.read();
sb.append((char) c);
if (c == 13) {
sb.append((char) input.read());// '10'
break; // done with subheader
}
}
}
}
// find embedded jpeg in stream
/*
* Some cameras return headers 'content-length' using different casing
* Eg. Axis cameras return 'Content-Length:' while TrendNet cameras return 'content-length:'
*/
final String subheader = sb.toString().toLowerCase();
//log.debug(subheader);
// Yay! - server was nice and sent content length
int c0 = subheader.indexOf("content-length: ");
final int c1 = subheader.indexOf('\r', c0);
if (c0 < 0) {
//log.info("no content length returning null");
throw new EOFException("The camera stream ended unexpectedly");
}
c0 += 16;
final int contentLength = Integer.parseInt(subheader.substring(c0, c1).trim());
//log.debug("Content-Length: " + contentLength);
// adaptive size - careful - don't want a 2G jpeg
ensureBufferCapacity(contentLength);
input.readFully(pixelBuffer, 0, contentLength);
input.read();// \r
input.read();// \n
input.read();// \r
input.read();// \n
return pixelBuffer;
}
@Override
public void release() throws Exception {
}
/**
* Grow the pixel buffer if necessary. Using this method instead of allocating a new buffer every time a frame
* is grabbed improves performance by reducing the frequency of garbage collections. In a simple test, the
* original version of IPCameraFrameGrabber that allocated a 4096 element byte array for every read
* caused about 200MB of allocations within 13 seconds. In this version, almost no additional heap space
* is typically allocated per frame.
*/
private void ensureBufferCapacity(int desiredCapacity) {
int capacity = pixelBuffer.length;
while (capacity < desiredCapacity) {
capacity *= 2;
}
if (capacity > pixelBuffer.length) {
pixelBuffer = new byte[capacity];
}
}
/**
* Returns the value of the {@code long} argument;
* throwing an exception if the value overflows an {@code int}.
*
* @param value the long value
* @return the argument as an int
* @throws ArithmeticException if the {@code argument} overflows an int
* @see <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#toIntExact-long-">Java 8 Implementation</a>
*/
private static int toIntExact(long value) {
if ((int) value != value) {
throw new ArithmeticException("integer overflow");
}
return (int) value;
}
}