Jul 14, 2013

Record Foscam IP Camera streaming video on Raspberry Pi

I spent whole day to implement this code. I think there will be many people who would like to use Raspberry Pi as a IP Camera recorder so I am sharing it here.

This is a shell script that uses GStream 1.0 library, which is not an official Wheezy dist yet. The reason why I am using it is that it was only one way to use hardware accelerated H.264 encoder. Currently only gstream-0.10 is officially available not 1.0 yet.

If you are not comfortable using unverified package, you can also use VLC without hardware acceleration. I haven't tested VLC way thoroughly but it shouldn't be too hard for anybody to use it.

Also my script uses "wget" to retrieve some information from Foscam IP Camera such as "alias name" and "alarm_upload_interval". If you don't want to install wget and you know those values for sure, you can simply modify the script and hard-code it.
#!/bin/sh
if [ ! $# = 3 ]; then
        echo "*** input argument error: IP ID PW ***"
        exit 1
fi
IP=$1
ID=$2
PW=$3

record_basedir=/home/pi/camera
file_ext=.mkv

WGET=/usr/bin/wget
GREP=/bin/grep
CUT=/usr/bin/cut
MKDIR=/bin/mkdir
DATE=/bin/date
GSTLAUNCH=/usr/bin/gst-launch-1.0

if [ ! -x $WGET -o ! -x $GREP -o ! -x $CUT -o ! -x $MKDIR -o ! -x $DATE -o ! -x $GSTLAUNCH ]; then
        echo "*** some of utilities not found ***"
        exit 2
fi

alias_name=`$WGET -q -S -O - http://$ID\@$IP/get_params.cgi\?user=$ID\&pwd=$PW 2> /dev/null | $GREP var\ alias= | $CUT -d"'" -f 2`
if [ "$alias_name" = "" ]; then
        echo "*** alias_name not found ***"
        exit 3
fi

record_dir=$record_basedir/$alias_name
if [ ! -d $record_dir ]; then
        $MKDIR -p $record_dir
fi
if [ ! -d $record_dir ]; then
        echo "*** Cannot make a folder: $record_dir ***"
        exit 4
fi

alarm_upload_interval=`$WGET -q -S -O - http://$ID\@$IP/get_params.cgi\?user=$ID\&pwd=$PW 2> /dev/null | $GREP var\ alarm_upload_interval= | $CUT -d"=" -f 2 | $CUT -d";" -f 1`
echo alarm_upload_interval=$alarm_upload_interval

timestamp=`$DATE +%Y_%m_%d-%H_%M_%S`
file_name=$timestamp$file_ext
fullpath=$record_dir/$file_name
echo file_name=$file_name
echo fullpath=$fullpath

$GSTLAUNCH souphttpsrc location="http://$ID\@$IP/videostream.asf\?user=$ID\&pwd=$PW" ! decodebin ! videoconvert ! omxh264enc ! "video/x-h264,profile=high" ! h264parse ! matroskamux ! filesink location=$fullpath 2> /dev/null 1> /dev/null

echo pid=$!

The last line is the core of the script. The logic is that it retrieve ASF video streaming data from Foscam IP Camera with "souphttpsrc". We need to decode the video with "decodebin". Then it becomes "raw video". The raw video is piped into "videoconvert". Now it is passed to the hardware accelerated H.264 encoder, "omxh264enc". I don't know about "h264parse" but without it, it didn't go through so you need the step as well. Then the encoded H.264 video is stored as MKV with "matroskamux" plug-in. Finally the file name for the output data is specified with "filesink" plug-in.

I found that two steps, souphttpsrc and decodebin, can be merged with "uridecodebin". It seems that uridecodebin can handle "buffering" feature and probably it would work better in different cases.

Another thing I want to mention is that stderr of gst-launch-1.0 should be redirected to /dev/null. Otherwise, it will make the script process "defunct". I think a child process of gst-launch-1.0 is holding the stderr of the parents' and it causes defunct processors when those parents are dead.


In order to install gstream1.0, you will need to follow these steps:
$ echo "deb http://vontaene.de/raspbian-updates/ . main" >> /etc/apt/sources.list
$ apt-get update
$ apt-get install libgstreamer1.0-0 libgstreamer1.0-0-dbg libgstreamer1.0-dev liborc-0.4-0 liborc-0.4-0-dbg liborc-0.4-dev liborc-0.4-doc gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 gstreamer1.0-alsa gstreamer1.0-doc gstreamer1.0-omx gstreamer1.0-plugins-bad gstreamer1.0-plugins-bad-dbg gstreamer1.0-plugins-bad-doc gstreamer1.0-plugins-base gstreamer1.0-plugins-base-apps gstreamer1.0-plugins-base-dbg gstreamer1.0-plugins-base-doc gstreamer1.0-plugins-good gstreamer1.0-plugins-good-dbg gstreamer1.0-plugins-good-doc gstreamer1.0-plugins-ugly gstreamer1.0-plugins-ugly-dbg gstreamer1.0-plugins-ugly-doc gstreamer1.0-pulseaudio gstreamer1.0-tools gstreamer1.0-x libgstreamer-plugins-bad1.0-0 libgstreamer-plugins-bad1.0-dev libgstreamer-plugins-base1.0-0 libgstreamer-plugins-base1.0-dev
Then you can verify that you have omxh264enc, which is the most important one, by this command:
$ gst-inspect-1.0 | grep omxh264enc
omx:  omxh264enc: OpenMAX H.264 Video Encoder
Now in case you want to use VLC, you can use this command:
vlc http://$ID\@$IP/videostream.asf\?user=$ID\&pwd=$PW\&res=8\&rate=6 --run-time=10 -Idummy --sout=#transcode{vcodec=h264}:standard{dst="a.mp4"} vlc://quit
I think it will be better to do -Irc without "--run-time=10 -Idummy" and control the time with remote control process. But I didn't go deep into the step.

As a reference of GStream, I found this page the most useful.

With more tweaks, I will be able to append time stamp on the corner of the video and I will also be able to include audio. But I haven't been gone that far yet.

16 comments:

Jay said...

I figured how to add audio. I wasn't sure which encoder can give me the best compression but it didn't seem like I had many choices.

$GSTLAUNCH souphttpsrc location="http://$ID\@$IP/videostream.asf\?user=$ID\&pwd=$PW" ! decodebin name=decoder \
decoder. ! videoconvert ! omxh264enc ! "video/x-h264,profile=high" ! h264parse \
! matroskamux name=mux ! filesink location=$fullpath \
decoder. ! audioconvert ! lamemp3enc ! mux.

Jay said...

I figured that a plug-in, "clockoverlay", gives me a nice clock on the side.

It should be placed right before videoconvert plug-in.

It will be looking like this:
$GSTLAUNCH souphttpsrc location="http://$ID\@$IP/videostream.asf\?user=$ID\&pwd=$PW" ! decodebin name=decoder \
decoder. ! clockoverlay ! videoconvert ! omxh264enc ! "video/x-h264,profile=high" ! h264parse \
! matroskamux name=mux ! filesink location=$fullpath \
decoder. ! audioconvert ! lamemp3enc ! mux. 2> /dev/null 1> /dev/null

Anonymous said...

Hi, quite cool! will try your method and see how it works.

fakada said...

Awesome post. I'll try this.
Do you think its possible to setup more than one camera recording on the Pi ?

Regards
Fabio

Jay said...

I think so but I haven't had a chance to try. lol

Alex said...

Hi, I tried your command but gst-launcher-1.0 will get stuck and the output file seems to be empty. Do you know anything I should tweak? I'm using 1.0.8

pi@raspberrypi ~/video $ gst-launch-1.0 --version
gst-launch-1.0 version 1.0.9
GStreamer 1.0.9
http://packages.qa.debian.org/gstreamer1.0

Jay said...

I hope u have sorted it out by now. I have no further info about gstreamer. It is not the part of official port so I guess we will have to suffer from non standard builds.

Gilson said...

Is there a way of limiting recording time to, let's say, 10 seconds and upload the resulting file to box.net?

Jay said...

If you use VLC, it is very easy to limit the recording time.

vlc http://$ID\@$IP/videostream.asf\?user=$ID\&pwd=$PW\&res=8\&rate=6 --run-time=10 -Idummy --sout=#transcode{vcodec=h264}:standard{dst="a.mp4"} vlc://quit

Gilson said...

Hi Jay,
Trying to use VLC got a few interface errors that are probably fine (Failed to connect to the D-Bus session daemon: Unable to autolaunch a dbus-daemon without a $DISPLAY for X11).
But it fails with:
mux_mp4 mux error: unsupported codec ms in mp4

Gilson said...

It works with vcodec=mp4v, but image quality is not good. What muxer should I use with h264?

Jay said...

Interesting.. I think I had the issue but I can barely remember. I think I had to download and install something manually in order to use h264 in VLC. I couldn't get it working with H/W encoder for h264 in VLC. I am not sure why you have quality issue with mp4 tho. You may want to check out VLC options for mp4 visual quality.

Gilson said...

Going back to the original issue, wouldn't it be possible to limit recording time with GStream?

Gilson said...

Could not limit the time length, but number of frames can be set with num-buffers. If set like num-buffers=2000 it will record about 10 seconds of video. Now the problem is that the script won't end, a CTRL+C is needed to return to the prompt.

Gilson said...

Hi Jay,

Just in case anyone is trying to do the same thing, found a way of setting recording time length using Python with the help of the GStreamer-devel group at http://gstreamer-devel.966125.n4.nabble.com/timeout-not-working-td4665437.html#a4665445

Thanks!

Chad Moore said...

Hi Gilson/Jay,

I tried the python version and cannot get it to work. My files collect and the size grows with the length of recording. I cannot play the file on any player. Any help you could provide would be greatly appreciated...

When I use
#!/usr/bin/python3

I receive:
Traceback (most recent call last):
File "./VideoCapture2.py", line 3, in
import gi

So I changed to:
#!/usr/bin/python

Now, I receive the errors after the timeout period:

Traceback (most recent call last):
File "/usr/lib/python2.7/dist-packages/gi/overrides/GLib.py", line 629, in
return (lambda data: callback(*data), user_data)
File "./VideoCapture2.py", line 27, in _timeout
sink.send_event(Gst.Event.new_eos())
AttributeError: 'NoneType' object has no attribute 'send_event'


My file script is:
#!/usr/bin/python

import gi
gi.require_version('Gst', '1.0')
from gi.repository import GObject, Gst

GObject.threads_init()
Gst.init(None)

class Encode:
def __init__(self):
self.mainloop = GObject.MainLoop()
self.pipeline = Gst.parse_launch('souphttpsrc location=http://192.168.xxx.xxx/videostream.asf?user=aaaa&pwd=bbbb! decodebin ! videoconvert ! omxh264enc ! video/x-h264,profile=high ! h264parse ! matroskamux ! filesink location=/home/pi/video.mkv')
self.bus = self.pipeline.get_bus()
self.bus.add_signal_watch()
self.bus.connect('message::eos', self.on_eos)
self.bus.connect('message::error', self.on_error)

def run(self):
self.pipeline.set_state(Gst.State.PLAYING)
tid = GObject.timeout_add(10000, self._timeout)
self.mainloop.run()

def _timeout(self):
print('timeout')
#self.pipeline.send_event(Gst.Event.new_eos()
sink = self.pipeline.get_by_name('matroskamux')
sink.send_event(Gst.Event.new_eos())

def kill(self):
self.pipeline.set_state(Gst.State.NULL)
self.mainloop.quit()

def on_eos(self, bus, msg):
print('on_eos()')
self.kill()

def on_error(self, bus, msg):
print('on_error():', msg.parse_error())
self.kill()

encode = Encode()
encode.run()