Posted:12/15/2014 11:54AM
Mike Mclain discusses how to get images from a EasyN IP Camera using Python
A number of months ago I purchased a EasyN Wireless WIFI IP Camera from Amazon with the intent of creating a bird watching video feed. While, these particular plans have not come to fruition (predominately because I decided that I wanted a higher resolution camera for this particular application); nevertheless, regardless of such envisioned intentions, the software that did come with this camera was extremely lacking (in my book) when it came to the ability to capture and save images from the camera.
Conversely, while there are an assortment of projects (and/or applications) on the Internet that attempt to address such inadequacies; however, the artisan within me is generally aggravated/discontent by the existence of such limitations (predominantly because I like to understand and/or control the underlying framework behind such implementations), thus I opted to try my hand at implementing my own framework to communicate with this particular device.
Towards this end, I started examining the EasyN IP Cameras web interface using the Firebug web debugger in order to intercept and observe the communications between the camera and the web browser. Conversely, I observed (via Firebug) that a simplistic Ajax based query string URL GET/POST system was being utilized to both control and configure the EasyN IP camera.
For example, this:
http://{IP ADDRESS}:{PORT}/videostream.cgi?user={USER}&pwd={PASSWORD}&resolution={CAPTURE RESOLUTION}&rate={FRAME RATE}URL, tells the EasyN IP camera that it should send back a series of JPEG images at a rate and resolution defined above, while this:
http://{IP ADDRESS}:{PORT}/decoder_control.cgi?command={COMMAND}URL, tells the EasyN IP camera that it should change the viewing location via rotation (for the most part, since some other non moving actions can be performed using this command).
Conversely, an assortment of other device controlling URLs, like:
http://{IP ADDRESS}:{PORT}/reboot.cgi?user={USER}&pwd={PASSWORD}or
http://{IP ADDRESS}:{PORT}/camera_control.cgi?param={CONTROL PARAMETER}&value={CONTORL VALUE}also exist, that perform various administrative functions (none of which will be discussed within this particular article, but might be addressed in future articles).
Nevertheless, with this information now known, I created a simplistic python class like so:
import urllib2
# A class to communicate with the EasyN IP Camera
class EasyN:
# A Basic Class Constructor
def __init__(self, ip, port, user, password, resolution=Resolutions["640x480"], frame_rate=Rate["20"], debug=True):
self.ip = ip
self.port = str(port)
self.user = user
self.password = password
self.resolution = resolution
self.frame_rate = frame_rate
self._debug = debug
self._connection = None
self._connection_url = NoneIn which the following constants (defined within my newly created EasyN class):
#camera resolutions
Resolutions = {
"160x120": 2,
"320x240": 8,
"640x480": 32,
}
#camera frame rate
Rate = {
"full": 0,
"20": 1,
"15": 3,
"10": 6,
"5": 11,
"4": 12,
"3": 13,
"2": 14,
"1": 15,
"1p2": 17,
"1p3": 19,
"1p4": 21,
"1p5": 23,
}
were obtained using firebug.
Next, I created a class function to generate a EasyN camera communication URL (in order to request JPEG information from the EasyN camera):
#camera resolutions
Resolutions = {
"160x120": 2,
"320x240": 8,
"640x480": 32,
}
#camera frame rate
Rate = {
"full": 0,
"20": 1,
"15": 3,
"10": 6,
"5": 11,
"4": 12,
"3": 13,
"2": 14,
"1": 15,
"1p2": 17,
"1p3": 19,
"1p4": 21,
"1p5": 23,
}
and then I wrote a class function to open a connection (via the urllib2.urlopen() command) with the camera and a class function to ensure that the opened connection is fully closed (to prevent the maximum users reached camera lockout error and to stop any buffer overflows in the urllib2 class) upon request:
# Open a Stream Connection to a url
def _connect(self, url):
if self._connection is None:
self._connection_url = url
self._connection = urllib2.urlopen(self._connection_url)
else:
if not self._connection_url == url:
self._connection = None
self._connection_url = url
self._connection = urllib2.urlopen(self._connection_url)
# Close a Connection
def _close(self):
self._connection = None
self._connection_url = None
# A user accessible close connection function
def close(self):
self._close()like so.
Next, upon doing some preliminary investigation of the response returned by the EasyN camera videostream.cgi URL (predominantly done by utilizing the following commands):
Demo = EasyN({CAM IP}, {CAM PORT},"{CAM USER}","{CAM PASSWORD}")
Demo._connect(Demo._get_stream_url())
print Demo._connection.read(80)
Demo._close()I was able to obtain the following response from the camera:
--ipcamera
Content-Type: image/jpeg
Content-Length: 48408
{JPEG IMAGE}noting that, this information was continuously retransmitted as the camera updated its image at the specified capture rate.
Conversely, with this information known, I defined this header information within the EasyN class constructor, like so:
# the header obtained via sniffing the reply self._stream_header = "--ipcamera\r\nContent-Type: image/jpeg\r\nContent-Length: "
(noting that, carriage returns and new lines were utilized within the observed response), and then I wrote a class function :
# Get a JPEG Image from the video stream
def _get_stream_jpeg(self):
image = None
self._connect(self._get_stream_url())
while True:
restart = False
# skip the observed header
for item in self._stream_header:
data = self._connection.read(1)
if not data == item:
restart = True
if self._debug:
print "skipping data", ord(data)
break
# if the header failed retry processing
if restart:
continue
# get the length of the jpeg
str_jpeg_length = ""
while True:
item = self._connection.read(1)
if item == "\r":
break
else:
str_jpeg_length += item
# skip the last line LF and the next line CR LF combo (len('\r+\n+\r') = 3)
item = self._connection.read(3)
# now cast string to int
jpeg_length = 0
try:
jpeg_length = int(str_jpeg_length)
except ValueError:
jpeg_length = 0
# if zero this is bad so return None
if jpeg_length == 0:
return image
#else read the image
image = self._connection.read(jpeg_length)
# skip the ending CR LF so the buffer is ready for a new jpeg reply
item = self._connection.read(2)
return imageTo separate the JPEG image from the HTML response produced by the EasyN Camera.
Likewise, upon validating the information obtained from this particular function, saving the JPEG image information to a file was achieved by creating the following function:
# get a single jpeg image from the camera
def get_frame(self, filename, auto_close=True):
image = self._get_stream_jpeg()
f = open(filename, 'wb')
f.write(image)
f.close()
if auto_close:
self._close()
Noting that the _close() function was called (within this function) in order to halt additional image transmissions from the EasyN Camera.
Conversely, this methodology can be extended to save multiple JPEG images from the EasyN Camera, like so:
# get a number of images from the camera
def get_frames(self, filename, number):
count = 0
while True:
if count >= number:
break
self.get_frame(filename % (count), False)
count += 1
self._close(), while raw data access (for more involved user tasks) can be obtained by adding the function:
# get a single raw jpeg image from the camera
def get_raw_frame(self, auto_close=True):
image = self._get_stream_jpeg()
if auto_close:
self._close()
return image, like so.
Overall, while I found the process of communicating with the EasyN IP Camera and obtaining its current viewable image a relatively straightforward process, an example of which is shown below:
However, I am aware that such implementations (presented within this article) are usually relatively trivial to the general requirements typically desired (like the ability to make movies or controlling the camera) by most end user applications, thus it is my intent to create a number of more advanced articles that will discuss these topics in further detail.
Enjoy!
By Mike Mclain