Hogyan mentem az analóg biztonsági kamerák valós idejű képét távoli HDD-re?

A munkahelyemen van néhány TechSon TC-DVR LN4016, illetve TechSon TC-DVR LN4008 típusú biztonsági kamerarendszer. Ezek úgy működnek, hogy nyolc vagy tizenhat analóg videokamera jelét fogadják koaxkábeleken keresztül, és beépített HDD-re mentik a videót. Jellemzően csak akkor rögzítik a filmet, ha azon valami mozgás látható. Beállítható, hogy a mozgás kezdete előtt hány másodperccel induljon a felvétel, és hogy utána mennyivel érjen véget.

Az volt a feladatom, hogy azon kamerák képeiről, amelyek a központi egységgel közös helyiségben vannak, készüljön távoli mentés is. A mentés nyilván csak akkor jó, ha a kamerából jövő képet a lehető legkisebb késleltetéssel kiírjuk a távoli HDD-re. Maga az eszköz nem kezel semmilyen távoli tárhelyet, és a forgalmazó nem tudott olyan szoftverről, ami a kamerák képének valós időben történő lementését lehetővé tenné, és én sem találtam ilyet. Azóta megtudtam, hogy Androidra létezik ilyen, de akkor már késő volt, meg amúgy sem biztos, hogy megfelelő lenne.

Éppen el voltam havazva munkákkal, és azt reméltem, pár órán belül megoldom a feladatot. Amikor kezdett elszállni a reményem, hogy kész megoldást találjak a problémára, szomorúan beláttam, hogy az előzetesen reméltnél több időt fog elvinni ez a munka, de még mindig nem sejtettem, mennyi küzdelemnek nézek még elébe...

Az első ötletem az volt, hogy Raspberry Pi-vel emulálok egy USB háttértárat, ezt rádugom az eszközre, és ezen, illetve a hálózaton keresztül menti a képet a távoli épületben levő Linux szerverre. Ehhez a megoldáshoz nem állt rendelkezésre a megfelelő pénzügyi keret, így olyan irányba indultam el, ami egy fillér befektetést sem igényel (a munkaidőmön kívül).

Az eszköznek van webes felülete, ahol valós időben nézhetők a kamerák képei, de ez csak Windows és Internet Explorer alatt nézhető, mivel a videojel lekérését és megjelenítését ActiveX vezérlővel végzi. A Linuxos munkaállomásomon VirtualBoxba feltelepítettem egy Windows 7-est, és WireSharkkal figyeltem az ActiveX kommunikációját.

Azt láttam, hogy a nézőprogram bináris parancsokat küld az eszköznek, az pedig válaszul egy olyan adatfolyamot küld, ahol nyers h.264 stream van valamilyen egyedi(?) metainformációt hordozó adatcsomagokba ágyazva.

Megpróbáltam tehát írni egy olyan programot, ami az általam nem teljesen értett bináris parancsokat a megfelelő TCP csatornán elküldi az eszköznek, a válaszul kapott adatfolyamról pedig lefejti az egyedi keretfejléceket, hogy csak a nyers h.264 adatfolyam maradjon. Bash shell scriptben készült el az alkotás! Sajnos még mindig nem végzi teljesen tökéletesen a keretfejlécek eltávolítását, mert az mplayer és az ffplay időnként (átlagosan kb. fél óra folyamatos nézés után) elveszti a képet, és újra kell indítani. A lemezre mentett nyers adat azonban érdekes módon mindig jól konvertálható (mondjuk AVI-ba), és nem is biztos, hogy az én programomban van a hiba.

Néhány általánosan ismert segédprogramon kívül (pl. netcat, ffmpeg) fontos szerepet kapott a bbe (Binary Block Editor: http://bbe-.sourceforge.net/) nevű parancs is. Ez vágja ki a keretfejléceket az adatfolyamból. A kész szkript a következőképpen néz ki:


#!/bin/bash

#########################################################################
# TechSon TC-DVR LN40xx tipusu eszkozok elo videokepenek
# megtekintesere es mentesere szolgalo segedprogram
#
# A szkript a kovetkezo GNU stilusu opciokat fogadja el:
#
# --dvr=IPCIM_vagy_HOSZTNEV:
#   Kotelezo opcio. A kezelendo DVR eszkoz elerhetoseget kell itt megadni.
#
# --cam=KAMERA_SORSZAM
#   Nem kotelezo opcio. A figyelendo kamera sorszama 1-tol 16-ig.
#   Az alapertelmezett sorszam 1.
#
# --quality=MINOSEG
#   Nem kotelezo opcio. Ha erteke low, kis felbontasu kepet kapunk,
#   ha erteke high, nagy felbontasut. Az alapertelmezett ertek "high".
#
# --username=FELHASZNALONEV:
#   Kotelezo opcio. A szkript ezzel a felhasznalonevvel csatlakozik a DVR
#   eszkozhoz. Hossza legfeljebb 8 ASCII karakter.
#
# --password=JELSZO:
#   Kotelezo opcio. A szkript ezzel a jelszoval csatlakozik a DVR
#   eszkozhoz. Hossza legfeljebb 8 ASCII karakter.
#
# --duration=IDOTARTAM
#    Nem kotelezo opcio. Ennyi masodperc utan a szkript kilep.
#    Alapertelmezett erteke 0, ami vegtelen mukodest jelent.
#
# --recode=ATKODOLAS:
#   Nem kotelezo opcio. Haromfele erteket vehet fel:
#
#      avi: A szkript AVI formatumu adatfolyamot ad ki a standard outputon.
#      Elo megtekinteshez ez az opcio ajanlott.
#
#      avc: A szkript a gyartospecifikus metaadatot eltavolitva nyers H264
#      adatfolyamot ad ki a standard outputon. Ez az alapertelmezett ertek.
#
#      raw: A szkript a kamera altal kuldott adatfolyamot valtozatlan
#      formaban adja ki. Bar nagy a tarhelyigenye, folyamatos menteshez ez
#      az opcio ajanlott a bbe szurosbol adodo esetleges adatvesztesek
#      elkerulese erdekeben, illetve azert, hogy a lementett fajlbol
#      indexelheto (azaz keresheto/tekerheto) videofajlt keszithessunk,
#      mert az ffmpeg erre streambol nem, csak lementett fajlbol kepes.
#      Keresheto fajl keszitese raw fajlbol (cam1.264):
#
#      bbe -b '/1111/:6' -e 'r 4 \x00\x00' cam1.264 |\
#      bbe -b ':/1111\x00\x00\x00\x00PACK/' -e 'L 8' -e D |\
#      bbe -e 'I 1111\x00\x00\x00\x00PACK' |\
#      bbe -b '/1111\x00\x00\x00\x00/:36' -e D -o cam1.avc
#      ffmpeg -f h264 -r 25 -i cam1.avc -vcodec copy cam1.avi
#
#      Alternativ megoldas az ffmeg helyett (Windows-os segedprogrammal):
#      avc2avi.exe -i cam1.avc -o cam1.avi
#
#########################################################################

sender () {
sleep 2
echo -ne "$AUTH"
sleep 2
echo -ne "$INIT"
sleep 5
echo -ne "$CAM"
while true
do
 sleep 10
 echo -ne "$KEEPALIVE"
 if (( DURATION > 0 ))
 then
  NOW="$(date +%s)"
  (( NOW > DURATION )) && exit 0
 fi
done
}

INIT='\x31\x31\x31\x31\x10\x01\x00\x00\x03\x04\x00\x00\x20\xb6\x7c\x07\x3e\x0a\x3c\x63\x00\x01\x00\x00\x00\xf8\x20\x6e\x61\x6d\x65\x3d\x01\xf8\x6f\x6e\x66\x69\x67\x5f\x02\xf8\x71\x75\x65\x73\x74\x5f\x03\xf8\x74\x22\x3e\x0a\x3c\x70\x04\xf8\x61\x6d\x65\x74\x65\x72\x05\xf8\x61\x6d\x65\x3d\x22\x63\x06\xf8\x66\x69\x67\x5f\x69\x74\x07\xf8\x22\x3e\x70\x74\x7a\x5f\x08\xf8\x65\x73\x65\x74\x5f\x69\x09\xf8\x6f\x3c\x2f\x70\x61\x72\x0a\xf8\x65\x74\x65\x72\x3e\x0a\x0b\xf8\x61\x72\x61\x6d\x65\x74\x0c\xf8\x20\x6e\x61\x6d\x65\x3d\x0d\xf8\x6f\x6e\x66\x69\x67\x5f\x0e\xf8\x65\x6d\x22\x3e\x70\x74\x0f\xf8\x63\x72\x75\x69\x73\x65\x40\xf8\x6e\x66\x6f\x3c\x2f\x70\x41\xf8\x61\x6d\x65\x74\x65\x72\x42\xf8\x3c\x2f\x63\x6f\x6d\x6d\x43\xf8\x64\x3e\x0a\x3c\x2f\x72\x44\xf8\x75\x65\x73\x74\x3e\x0a\x45\xf8\x30\x35\x00\x00\x00\x00\x46\xf8\x00\xb3\x0b\xf3\x02\x00\x47\xf8\x7d\x07\x08\xb6\x7c\x07\x48\xf8\x00\x00\x00\x00\x00\x00\x49\xf8\x24\x00\x43\x41\x4d\x45\x4a\xf8\x30\x36\x00\x00\x00\x00\x4b\xf8\x00\x00\x00\x00\x00\x00\x4c\xf8\x00\x00\x00\x00\x00\x00\x4d\xf8\x00\x00\x00\x00\x00\x00\x4e\xf8\x24\x00\x43\x41\x4d\x45\x4f\xf8\x30\x37\x00\x00\x00\x00'

KEEPALIVE='\x31\x31\x31\x31\x00\x00\x00\x00'

DVR=''
CAM=1
QUALITY='high'
USERNAME=''
PASSWORD=''
PAD='\x00\x00\x00\x00\x00\x00\x00\x00'
RECODE='bbe'
DURATION=0

for i in $@
do
 if [[ "$i" =~ ^--dvr= ]]
 then
  DVR="${i:6}"
 elif [[ "$i" =~ ^--cam= ]]
 then
  CAM="${i:6}"
 elif [[ "$i" =~ ^--quality= ]]
 then
  QUALITY="${i:10}"
 elif [[ "$i" =~ ^--username= ]]
 then
  USERNAME="${i:11}"
 elif [[ "$i" =~ ^--password= ]]
 then
  PASSWORD="${i:11}"
 elif [[ "$i" =~ ^--recode= ]]
 then
  RECODE="${i:9}"
 elif [[ "$i" =~ ^--duration= ]]
 then
  DURATION="${i:11}"
  if (( DURATION > 0 ))
  then
   (( DURATION += $(date +%s) ))
  fi
 else
  echo "Ismeretlen opció: $i" >/dev/stderr
  exit 1
 fi
done

[[ $DVR == '' ]] || [[ $USERNAME == '' ]] || [[ $PASSWORD == '' ]] && exit 1

USERNAME=$(echo -n $USERNAME | hexdump -ve '/1 "_%02x"' | sed 's/_/\\x/g')
LEN=$((32-${#USERNAME}))
USERNAME=${USERNAME:0:32}${PAD:0:$LEN}

PASSWORD=$(echo -n $PASSWORD | hexdump -ve '/1 "_%02x"' | sed 's/_/\\x/g')
LEN=$((32-${#PASSWORD}))
PASSWORD=${PASSWORD:0:32}${PAD:0:$LEN}

AUTH="\x31\x31\x31\x31\x88\x00\x00\x00\x01\x01\x00\x00\x02\x01\x00\x00\x01\x00\x00\x00\x78\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00${USERNAME}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00${PASSWORD}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x27\x9a\xf1\xa6\x00\x00\x04\x00\x00\x00"

CAM=$(( echo -n 0000 ; echo "obase=16; 2^(${CAM}-1)" | bc ) | sed -r 's/^.*(.{2})(.{2})$/\\x\2\\x\1/')
if [[ $QUALITY == low ]]
then
 CAM="\x31\x31\x31\x31\x34\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00${CAM}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
else
 CAM="\x31\x31\x31\x31\x34\x00\x00\x00\x03\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31\x31\x31\x31\x34\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x24\x00\x00\x00\x00\x00\x00\x00${CAM}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31\x31\x31\x31\x10\x00\x00\x00\x0c\x05\x00\x00\xd2\x04\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
fi

case "$RECODE" in
raw)
 sender | netcat "$DVR" 7000
 ;;
avi)
  { echo -ne '1111\x00\x00\x00\x00PACK' ; sender | netcat "$DVR" 7000 | bbe -b '/1111/:6' -e 'r 4 \x00\x00' | bbe -b ':/1111\x00\x00\x00\x00PACK/' -e 'L 8; D' ; } |\
  bbe -b '/1111\x00\x00\x00\x00/:36' -e D |\
  ffmpeg -f h264 -i - -vcodec copy -f avi -
 ;;
*)
  { echo -ne '1111\x00\x00\x00\x00PACK' ; sender | netcat "$DVR" 7000 | bbe -b '/1111/:6' -e 'r 4 \x00\x00' | bbe -b ':/1111\x00\x00\x00\x00PACK/' -e 'L 8; D' ; } |\
  bbe -b '/1111\x00\x00\x00\x00/:36' -e D
 ;;
esac

Következzék egy példa a program indítására! A 192.168.0.1 IP című eszköz 1-es számú kamerájának élőképét nézzük magas minőségben "jozsi" felhasználónévvel és a megfelelő jelszóval:

./techson.sh --dvr=192.168.0.1 --username=jozsi --password=jozsi_jelszava --quality=high --cam=1 | ffplay -f h264 -i -

Az éles képrögzítő szerveren egyébként nem teljesen így paraméterezem a szkriptet: A cron démon segítségével óránként újraindítom, és a --duration paramétert megadva 1 óra 2 percre korlátozom a futásidőt. A szkript standard outputját fájlba irányítom. A fájlok óránként keletkeznek, és egymást - a biztonság kedvéért - két perccel átfedő videoanyagot tartalmaznak.

Érdekességképpen írtam egy másik szkriptet, amely lehetővé teszi, hogy másik kamera képére váltsunk egy folyamatban levő stream-ben. Ehhez a futó nézőszkript által indított netcat processz PID-jére van szükségünk. Ennek a szkripten a meghívása a következőképpen történhet:


tamas@kisgep:~/Dokumentumok/biztonsági kamerák> ps -A | grep netcat
12681 pts/1 00:00:00 netcat
tamas@kisgep:~/Dokumentumok/biztonsági kamerák> ./changecam.sh --pid=12681 --quality=high --cam=5

Maga a szkript pedig így néz ki:


#!/bin/bash

CAM=1
QUALITY='high'

for i in $@
do
 if [[ "$i" =~ ^--pid= ]]
 then
  NETCATPID="${i:6}"
 elif [[ "$i" =~ ^--cam= ]]
 then
  CAM="${i:6}"
 elif [[ "$i" =~ ^--quality= ]]
 then
  QUALITY="${i:10}"
 else
  echo "Ismeretlen opció: $i" >/dev/stderr
  exit 1
 fi
done

(( NETCATPID > 0 )) || exit 1

CAM=$(( echo -n 0000 ; echo "obase=16; 2^(${CAM}-1)" | bc ) | sed -r 's/^.*(.{2})(.{2})$/\\x\2\\x\1/')
if [[ $QUALITY == low ]]
then
 CAM="\x31\x31\x31\x31\x34\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00${CAM}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
else
 CAM="\x31\x31\x31\x31\x34\x00\x00\x00\x03\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31\x31\x31\x31\x34\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x24\x00\x00\x00\x00\x00\x00\x00${CAM}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31\x31\x31\x31\x10\x00\x00\x00\x0c\x05\x00\x00\xd2\x04\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
fi

echo -ne "$CAM" > "/proc/$NETCATPID/fd/0"

Hozzászólások

Minden tiszteletem!
------------------------
{0} ok boto
boto ?

"Beállítható, hogy a mozgás kezdete előtt hány másodperccel induljon a felvétel ..."

És ezt hogy? :)

Egyébként leteszteltem, mi történik, ha hirtelen megszakad a kapcsolat a TechSon eszköz és a szkriptet futtató rögzítőszerver közötti kapcsolat, azaz mondjuk befut egy szabotőr az eszköznek helyet adó szobába, és szétveri, vagy simán kihúzza a konnektorból. Úgy találtam, hogy a kapcsolat megszakadása előtt kevesebb, mint egy másodperccel ér véget a lementett videofelvétel, ami nem rossz eredmény. Remélem, jó módszerrel mértem.

A metaadat esetleg rtp, MPEG transport stream? (utóbbi kevésbé valószínű)

nice! :D
Dahua is hasonlót csinál, van is rá opensource project, hogy mplayerbe lehessen Pipe-olni a képet:
http://tanidvr.sourceforge.net/

Látom itt a jelszó plaintextben utazik, én nem voltam ilyen szerencsés:
http://hup.hu/node/141894

Egyszer már nekiestem megIDA-zni, de nem jutottam messzire (az ASM tudásom csekély volt a problémához).

Nem sajnos nem jutottam előbbre. Írtam egy "szerver emulátort" hogy lehessen DVR nélkül is tesztelni. Behúztam IDA-ba, meg nagyjából meg is van hol van a DLL-ben az enkódoló rutin, de visszafejtenem még nem sikerült. Van egy ismerősöm aki pro x86 ASM-ben majd megkérem, hogy segítsen.

A kamerák belső HDD-re mentik a videót. Azt lehet tudni, hogy a kamerákon milyen rendszer fut? Van soros portja? A burkolatát megbontva lehet hogy hozzá lehet férni soros porthoz a panelján, de kívülről nem látható.
____________________________________
Ha vita van, számoljanak órajelciklusokat. Egyesével.