Jul 18, 2013

Shell script: Converting DTS/AC3 audio to AAC

I have been using HandBrake to transcode movie files for reasons. One of the reasons was the audio codecs. iDevices and Raspberry Pi cannot perform hardware accelerated DTS/AC3 audio decoding due to license issues.

With HandBrake, I couldn't find a way to convert only audio but not video. What I wanted was not touching the video for quality reason and convert only the audio to AAC or MP3.

I found several people posted how to do that for MKV container format. From those scripts I made my own script that converts audio tracks. I don't know how to manipulate AVI or other container formats yet but most of cases MKV seems to be enough.

This script is faster than transcoding both video and audio because this converts only audio not video. And since it doesn't touch the video, the video quality wouldn't be affected as well.

I believe as long as you use Raspberry Pi, you wouldn't need to transcode video. But if you are using iDevices, you may need to transcode audio and video both.

You will need to install two packages.
sudo apt-get install mkvtoolnix ffmpeg
Now here is the script:
#!/bin/sh
mkv_input=$1
if [ ! -f "$mkv_input" ]; then
        echo "*** File not found: $mkv_input ***"
fi

DIRNAME=/usr/bin/dirname
BASENAME=/usr/bin/basename
WC=/usr/bin/wc
DATE=/bin/date
GREP=/bin/grep
HEAD=/usr/bin/head
TAIL=/usr/bin/tail
CUT=/usr/bin/cut
AVCONV=/usr/bin/avconv
MKVINFO=/usr/bin/mkvinfo
MKVEXTRACT=/usr/bin/mkvextract
MKVMERGE=/usr/bin/mkvmerge

filename_only=`$BASENAME "$mkv_input" .mkv`
dirname_only=`$DIRNAME "$mkv_input"`

trackCount=`$MKVINFO "$mkv_input" | $GREP track\ ID | $WC -l`
echo "[`$DATE`] $trackCount tracks are found from $mkv_input..."
echo "[`$DATE`] This script will generate temporary files on the way and you may want to remove them by yourself..."

trackId=0
while [ $trackId -lt $trackCount ]
do
        trackNum=`expr $trackId + 1`
        mkv_output=$dirname_only/$filename_only.$trackId.mkv
        audio_org=$dirname_only/_tmp_audio.$trackId.org
        audio_new=$dirname_only/_tmp_audio.$trackId.aac

        if [ -f "$mkv_output" ]; then
                echo "[`$DATE`] Already converted file found: $mkv_output"
                trackId=`expr $trackId + 1`
                continue;
        fi

        srcAudio=`$MKVINFO "$mkv_input" | $GREP Codec\ ID | $HEAD -$trackNum | $TAIL -1 | $GREP -e "\(DTS\|AC3\)" | $CUT -d ":" -f 2`
        if [ "$srcAudio" = "" ]; then
                trackId=`expr $trackId + 1`
                continue
        fi
        echo "[`$DATE`] Audio track found at $trackId: $srcAudio"

        echo "[`$DATE`] Extracting audio to a file: $audio_org ..."
        $MKVEXTRACT tracks "$mkv_input" $trackId:"$audio_org"
        if [ ! -f "$audio_org" ]; then
                echo "[`$DATE`] *** Audio track extraction failed ***"
                trackId=`expr $trackId + 1`
                continue
        fi

        echo "[`$DATE`] Converting extracted audio to AAC: $audio_new ..."
        $AVCONV -i "$audio_org" -strict experimental -acodec 'aac' -ac '2' -pass '1' -y "$audio_new"
        if [ ! -f "$audio_new" ]; then
                echo "[`$DATE`] *** Audio converting failed ***"
                trackId=`expr $trackId + 1`
                continue
        fi

        echo "[`$DATE`] Merging new audio into MKV: $mkv_output ..."
        $MKVMERGE -o "$mkv_output" -A "$mkv_input" "$audio_new"
        if [ ! -f "$mkv_output" ]; then
                echo "[`$DATE`] *** Merging failed ***"
        fi

        trackId=`expr $trackId + 1`
done

echo "[`$DATE`] Done"
As you can see it generates temporary files on the way. You will need to manually remove them or you will need to modify the script by yourself. I just don't like any scripts to remove anything for safety reasons.

Jul 14, 2013

How to setup Raspberry Pi for Foscam IP Camera

I explained how to store video data from Foscam IP Camera on Raspberry Pi. Now I am going to show you how to wire them all together.

As I explained, Foscam IP Camera, FI8910W, has motion detection feature in it. When any motion is detected, it will either send an email or upload a few still images to FTP server. A problem is that it doesn't store video data. That's what I am trying to do in this article.

The trick is that on the camera side it will do the motion detection and uploading files to FTP. When FTP connection is requested, it actually interact with a fake ftp server and the fake ftp server will trigger the video recording with a script that I explained.

Here is a big picture of how whole thing works.
  1. It opens a fake ftp port and wait for FTP request.
  2. Camera request FTP login.
  3. Start recording video stream
  4. Wait for another FTP request to identify whether or not there is motion going on still.
  5. If FTP access is request within a given time, it keeps recording the video stream.
  6. If FTP access is not requested within a given time, it stops recording.
The setting on the camera side is already explained here.

A fake ftp server for shell script version is here:
#!/bin/sh
CUT=/usr/bin/cut
TR=/usr/bin/tr

if [ ! -x $CUT -o ! -x $TR ]; then
        echo "*** some of utilities are not found or not executable ***"
        exit 1
fi

echo "220 Welcome message."
read read_id
if [ ! "`echo $read_id | $CUT -d ' ' -f 1`" = "USER" ]; then
        echo "530 Invalid login information..."
        exit 2
fi
ID=`echo $read_id | $CUT -d ' ' -f 2 | $TR -d ^M`

echo "331 Please specify the password."
read read_pw
if [ ! "`echo $read_pw | $CUT -d ' ' -f 1`" = "PASS" ]; then
        echo "530 Invalid login information..."
        exit 3
fi
PW=`echo $read_pw | $CUT -d ' ' -f 2 | $TR -d ^M`

echo "230 Guest login ok, access restrictions apply."

read read_typei
if [ ! "`echo $read_typei | $TR -d ^M`" = "TYPE I" ]; then
        exit 4
fi
echo "200 Type Set to I"

echo ID=$ID >&2
echo PW=$PW >&2
I placed it here: /home/pi/script/foscam_fake_ftp.sh
But you can place it wherever you want.

In the script, a special character, "^M", is not two characters but one character. In VI, you can type in the character by pressing "Ctrl+V Ctrl+M". The special character was entailed when the camera send text message so I had to remove them in the script.

A difference compare to the previous Windows batch file version is that the ID and Password information given to the fake ftp server will be used as a login information to access the video data. This time, we don't care whether it is Passive or not.

Here is a configuration file that will be placed at /etc/foscam.conf
#!/bin/sh
fake_ftp_if=wlan0
fake_ftp_port=2122
foscam_fake_ftp=/home/pi/script/foscam/foscam_fake_ftp.sh
foscam_record=/home/pi/script/foscam/foscam_record.sh
foscam_listener=/home/pi/script/foscam/foscam_listener.sh
foscam_cron=/home/pi/script/foscam/foscam_cron.sh

record_basedir=/home/pi/camera
recording_ip_dir=/var/run/foscam.d
#recording_ip_dir=/home/pi/script/foscam/var

alarm_wait_interval=60
cron_sleep_interval=2
file_ext=.mkv

LISTENER_PIDFILE=/var/run/fld.pid
CRON_PIDFILE=/var/run/fcd.pid

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

NCAT=/usr/bin/ncat
GREP=/bin/grep
CUT=/usr/bin/cut
SLEEP=/bin/sleep
PS=/bin/ps
KILL=/bin/kill
DATE=/bin/date
IFCONFIG=/sbin/ifconfig
TAIL=/usr/bin/tail
RM=/bin/rm

LS=/bin/ls
Now we need a listener script here:
#!/bin/sh
. /etc/foscam.conf

if [ "$foscam_fake_ftp" = "" -o ! -x $foscam_fake_ftp ]; then
        echo "*** fake ftp server is not found or executable: $foscam_fake_ftp ***"
        exit 0
fi
if [ "$foscam_record" = "" -o ! -x $foscam_record ]; then
        echo "*** fake ftp server is not found or executable: $foscam_record ***"
        exit 1
fi
if [ ! -x $NCAT -o ! -x $GREP -o ! -x $CUT -o ! -x $SLEEP -o ! -x $PS -o ! -x $KILL -o ! -x $DATE -o ! -x $IFCONFIG -o ! -x $TAIL -o ! -x $RM ]; then
        echo "*** some of utilities not found ***"
        exit 3
fi

fake_ftp_ip=`$IFCONFIG $fake_ftp_if | $GREP inet\ addr | $CUT -d ":" -f 2 | $CUT -d " " -f 1`
#echo fake_ftp_ip=$fake_ftp_ip

while [ true ]; do
        echo [`$DATE`] Waiting for a new request...
        ncat_result=`$NCAT -v -l -c $foscam_fake_ftp $fake_ftp_ip $fake_ftp_port 2>&1 3> /dev/null`
        if [ ! $? = 0 ]; then
                echo "*** Fake ftp cannot start: $fake_ftp_ip:$fake_ftp_port ***"
                exit 5
        fi

        #echo "$ncat_result"
        IP=`echo "$ncat_result" | $GREP "Ncat: Connection from " | $GREP ":[0-9]" | $CUT -d " " -f 4 | $CUT -d ":" -f 1`
        ID=`echo "$ncat_result" | $GREP ID= | $CUT -d "=" -f 2`
        PW=`echo "$ncat_result" | $GREP PW= | $CUT -d "=" -f 2`
        echo [`$DATE`] Streaming requested from $ID\@$IP...
        #echo PW=$PW

        info_file=$recording_ip_dir/$IP
        if [ ! -f $info_file ]; then
                record_result=`$foscam_record $IP $ID $PW`
                #echo "$record_result"

                alarm_upload_interval=`echo "$record_result" | $GREP alarm_upload_interval= | $CUT -d "=" -f 2`
                record_interval=`expr $alarm_upload_interval + $alarm_wait_interval`
                gstreamer_pid=`echo "$record_result" | $GREP pid= | $CUT -d "=" -f 2`

                echo "gstreamer_pid=$gstreamer_pid" > $info_file
                if [ ! -f $info_file ]; then
                        echo [`$DATE`] info file coundn\'t be created: $info_file
                        echo [`$DATE`] Recording streamming video from $ID\@$IP for $record_interval seconds...
                        $SLEEP $record_interval
                        $KILL $gstreamer_pid
                else
                        record_started=`$DATE +%s`
                        finish_recording_time=`expr $record_started + $record_interval`

                        echo "record_started=$record_started" >> $info_file
                        echo "alarm_upload_interval=$alarm_upload_interval" >> $info_file
                        echo "record_interval=$record_interval" >> $info_file
                        echo "finish_recording_time=$finish_recording_time" >> $info_file
                fi
        else
                more_recording_requested=`$DATE +%s`
                record_interval=`$GREP record_interval= $info_file | $CUT -d "=" -f 2`
                finish_recording_time=`expr $more_recording_requested + $record_interval`

                echo "more_recording_requested=$more_recording_requested" >> $info_file
                echo "finish_recording_time=$finish_recording_time" >> $info_file
        fi
done
I placed it at  /home/pi/script/foscam/foscam_listener.sh

In order to get this script working, you will need three other scripts: foscam_record.sh, foscam_cron.sh and foscam_fake_ftp.sh, which is shown above.

You can get foscam_record.sh from this previous article of mine. But you must get "gst-launch-1.0" running as a background process. It can be easily done by adding "&" at the end of the line.

Another script, foscam_cron.sh, will figure out when to stop recording. This script should be running all the time as a daemon or cron-job.
#!/bin/sh
. /etc/foscam.conf

if [ ! -d $recording_ip_dir ]; then
        echo "*** recording_ip_dir does not exist: $recording_ip_dir ***"
        exit 1
fi

if [ ! -x $GREP -o ! -x $LS -o ! -x $CUT -o ! -x $PS -o ! -x $RM -o ! -x $SLEEP -o ! -x $DATE -o ! -x $KILL ]; then
        echo "*** some of utilities not found ***"
        exit 3
fi

while [ true ]
do
        for info_filename in `$LS $recording_ip_dir`
        do
                info_file=$recording_ip_dir/$info_filename
                #echo info_file=$info_file

                gstreamer_pid=`$GREP gstreamer_pid= $info_file | $CUT -d "=" -f 2`
                if ! $PS $gstreamer_pid 2> /dev/null 1> /dev/null
                then
                        echo "*** gstreamer not found: $gstreamer_pid ***"
                        $RM $info_file
                        continue
                fi

                cur_time=`$DATE +%s`
                finish_recording_time=`$GREP finish_recording_time= $info_file | $TAIL -1 | $CUT -d "=" -f 2`
                if [ $finish_recording_time -lt $cur_time ]; then
                        echo Time to stop: $info_file ...
                        $KILL $gstreamer_pid
                        $RM $info_file
                fi
        done

        $SLEEP $cron_sleep_interval
done
Now I want it to start at boot time automatically.
You will have to make a new file at /etc/init.d/foscamd
#!/bin/sh

### BEGIN INIT INFO
# Provides:          wrice.blogspot.com
# Required-Start:    $network $local_fs $remote_fs
# Required-Stop:     $network $local_fs $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: start Foscam daemons (fcd and fld)
### END INIT INFO

. /etc/foscam.conf

# See if the daemon is there
test -x $foscam_listener || exit 0

. /lib/lsb/init-functions

fake_ftp_ip=`$IFCONFIG $fake_ftp_if | $GREP inet\ addr | $CUT -d ":" -f 2 | $CUT -d " " -f 1`

remove_stale_pid_file () {
        pid_file=$1
        # Wait a little and remove stale PID file
        sleep 1
        if [ -f $pid_file ] && ! ps h `cat $pid_file` > /dev/null
        then
                rm -f $pid_file
        fi
}

case "$1" in
        start)
                log_daemon_msg "Starting Foscam daemons"

                log_progress_msg "fcd"
                if ! start-stop-daemon --start --quiet --background --make-pidfile --pidfile $CRON_PIDFILE --exec $foscam_cron
                then
                        log_end_msg 1
                        exit 1
                fi

                log_progress_msg "fld"
                if ! start-stop-daemon --start --quiet --background --make-pidfile --pidfile $LISTENER_PIDFILE --exec $foscam_listener
                then
                        log_end_msg 1
                        exit 1
                fi
                sleep 1
                if ! ps h `cat $LISTENER_PIDFILE` > /dev/null
                then
                        log_end_msg 1
                        exit 1
                fi
                log_end_msg 0
                ;;
        stop)
                end_msg=0
                log_daemon_msg "Stopping Foscam daemons"
                log_progress_msg "fld"
                if ! start-stop-daemon --stop --quiet --pidfile $LISTENER_PIDFILE
                then
                        end_msg=1
                fi
                echo | /usr/bin/ncat --send-only $fake_ftp_ip $fake_ftp_port
                remove_stale_pid_file $LISTENER_PIDFILE

                log_progress_msg "fcd"
                if ! start-stop-daemon --stop --quiet --pidfile $CRON_PIDFILE
                then
                        end_msg=1
                fi
                remove_stale_pid_file $CRON_PIDFILE
                log_end_msg $end_msg
                ;;
        reload|restart|force-reload)
                $0 stop
                sleep 1
                $0 start
                ;;
        status)
                status="0"
                status_of_proc -p $LISTENER_PIDFILE $foscam_listener foscamd || status=$?
                exit $status
                ;;
        *)
                echo "Usage: /etc/init.d/foscamd {start|stop|reload|restart|force-reload|status}"
                exit 1
                ;;
esac

exit 0

Once foscamd is created, you will need to run this command to generate symbolic links for each boot sequence:
sudo update-rc.d foscamd defaults
Now when you restart your computer the listener will start automatically.

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.