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 = None
In 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 image
To 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