Pull to refresh

Видеонаблюдение от идеи до… идеи

Reading time 12 min
Views 30K
У меня есть основная работа, есть знакомые которые всегда хотят «начать новый бизнес». Они свой имеют, но всегда хотят расшириться. И вот каждый раз когда они этого хотят, они приходят ко мне за советами. Идея лежит на поверхности. «Мы занимаемся интернетом, а давайте заниматься еще чем нибудь на нашей теме». Так мы начали заниматься телефонией, продажей сетевого оборудования, хостингом, колокейшином. Постановка задачи всегда меня радовала. «Давай заниматься телефонией», «Давай заниматься хостингом». Ну и последнее что было «Давай заниматься видеонаблюдением».
Вот «Давай заниматься видеонаблюдением» это и было заветное ТЗ.
Что из этого получилось и опыт опишу в этой статье.

Так уж исторически сложилось, что я работаю в организациях где меня окружают пипродажники, а я Дартаньянтехнический специалист, как оказалось (со временем) широкого профиля.
За свой профессиональный путь я построил не один обслуживающий-технический отдел, но до уровня образованных(программистов) людей не доходило. По этому я всегда на виду, все вопросы по IT ко мне. Их (знакомых) уже много лет консультирую в области IT, являясь системным и бизнес аналитиком, бесплатным :). Теперь к теме.

Однажды люди пришли ко мне и сказали «мы хотим видеонаблюдение». Я был занят и не было желания развиваться в эту сторону, тогда и без этого хватало забот. Они нашли другого «специалиста», прозанимались 6 месяцев, закончилось это ни чем. Человек оказался «прекрасным», напел про МОЩЬ zoneminder и motion, то что облачно будет всё работать. За 6 месяцев ни одного готового решения, ни одного клиента.
Они устали, пришли ко мне. Так как я с ними работаю уже лет так 10, то почему бы еще раз не поучаствовать в авантюре. Предложение как всегда подкупало своей оригинальностью и новизной:
— Хотим заняться облачным видеонаблюдением. Вложений 0, оборудование купим, за разработку 0, деньги делим 50/50.
К слову так было всегда, и всегда деньги были 50/50, или 30/30/30 в зависимости от количества участвующих лиц.
«Почему бы и нет», подумал я. Опыт лишним не будет. (хотя толку от этого опыта, если ты специалист широкого профиля и раз в год меняешь этот профиль :)

Идея озвучена: «Хочу облачное видеонаблюдение».


Люди которые хотят что то продать, они всегда будут «что то» продавать.
Осталось придумать это «Что то» и оформить.

Облако. Денег 0, нагрузка большая. Значит нужно придумать прототип, который будет решать эти задачи.
Сначала взгляд упал на Openstack. Можно на лету расширять вычислительные мощи, но для запуска требуется несколько машин. Поигравшись с разными версиями Openstack пришлось искать более подходящее решение. Взгляд упал на Proxmox.
Замечательное решение, которое подходило под наши нужды на старте. И всегда те VM можно портировать в железо или другое облако.
В покупке железа я не успел принять участие, до моего появления была приобретена рабочая лошадка: Intel Core i5-3570K @ 3.40GHz
И вот на этом железе нужно было заняться облачным видонаблюдением. Абсурдная ситуация :) Но мы не сдаемся.
Придумывается, какая-никакая, система продажи. Видеонаблюдение в купе с системами безопасности и всевозможными оповещениями с завязкой на ЧОПы(выход есть на гос уровне).
К этому в будущем приклеилось
1) постоянная видеозапись
2) запись по движению
3) timelapse запись
4) оповещение по SMS
5) звонок из центра безопасности при обнаружении движения. Оператор проверяет запись, вызывает ЧОП, оповещает клиента.
ЧОП рынок в Москве будет расширяться из года в год. МиПолиция с каждым годом уменьшает объем охранного бизнеса. Так что всё это постепенно переползает на частников.

Общая система выглядела так:

image

Камеры приходят на какую либо железку, она может быть завязана с безопасностью. Эта железка может быть как софтварный клиент, так и хардварное решение на OpenWrt. RPC нарисовано не просто так. Идея была создать 2 модуля. Первый полностью консольный. Это позволило бы его вживлять в железо, второй модуль графический, который управляет консольным. Это позволило бы управлять железкой с любого места. С компа пользователя, с компа поддержки и т.д. Также первый модуль необходим для стандартизации потоков и стопкадров с камер. Такие железки нами уже обкатывались. Для прототипа подходили, опыт был. Ни кто не запрещал логику этой железки перенести к нам на сервер.

Теперь остаются танцы с бубном о том как зоопарк h264 загнать на диск, и в web + приделать детекторы и подобную лабуду, и всё это уместить на одно i5.
ZoneMinder может быть и хорошая система, но после добавления 4х камер она загрузила все 4 ядра системы более чем на 70%. От него сразу отказались.

Для прототипа взяли VLC. Разрабатывается давно, обкатан многими, мы его много раз использовали для некоторых проектов.
Есть как минимум 2 задачи которые нужно решить
1) выдать в сеть
2) записать поток

Зоопарк камер состоит из rtsp и http потоков. VLC всё умеет. Решили использовать VLM. Имеет управление по tlenet и http.
В input кормится ссылка, в output
"#{$transcode}std{access=http{mime={$this->mime}},mux=ts{use-key-frames},dst=*:{$this->getPort()}/{$this->path}}";
Получаем HTTP поток с любой входящей url.
HTTP нам удобен чем? Можно загнать в hginx, распределить нагрузку, зашифровать. отправить в https.
1 клиент — 1 vlc процесс.
Скриптовать прототип решили на PHP, так как разработчик один, времени мало, финансирования 0.
Команда запуска. (буду приводить готовый код, из него не сложно вытащить команду, если потребуется)
$vlc_ifs = "-I http --http-host=0.0.0.0 --http-port {$this->getHttpPort()} -I telnet --telnet-port {$this->getTelnetPort()}  --telnet-password ".TLPWD;
        if(VLC_USE_LOG)
            $vlc_logs = "--extraintf=http:logger --file-logging --log-verbose 0 --logfile {$this->getLogFile()}";
        else
            $vlc_logs = '--extraintf=http';
        //$vlc_shell = VLCBIN." --rtsp-tcp --ffmpeg-hw --http-reconnect --http-continuous --sout-keep ".VLCD." $vlc_ifs  --repeat --loop --network-caching ".VLCNETCACHE." --sout-mux-caching ".VLCSOUTCACHE." $vlc_vlm --pidfile {$this->getPidFile()} $vlc_logs \n";
        $vlc_shell = VLCBIN." --rtsp-tcp ".VLCD." $vlc_ifs --repeat --loop --live-caching ".VLC_LIVE_CACHE." --network-caching ".VLCNETCACHE." --sout-mux-caching ".VLCSOUTCACHE."  --sout-ts-dts-delay 400 $vlc_vlm --pidfile {$this->getPidFile()} $vlc_logs ";

        if($this->isValgrind()){
            $vlc_shell = "valgrind -v --trace-children=yes --log-file={$this->getValgrindFile()} --error-limit=no --leak-check=full $vlc_shell";
        }


Так как у нас уже есть live stream мы можем с него и писать.
return "#std{access=file{append},mux=ts{use-key-frames},dst=$filePath.avi}";

VLM создается просто
на камеру можно создавать 3 потока
1) live stream
2) rec stream
3) motion stream
$this->execute("new {$this->cam} broadcast enabled loop");
после можно play, stop делать
$command = "control {$this->cam} $command";
для rec достаточно, например, раз в 10 секунд делать stop, менять $filePath, делать play
Мы получаем набор файликов с записями. Для motion play и stop.
VLC также можно запускать и «у нас» и «у них».
Вопрос движения.
Есть инструмент motion. Он по Jpeg вычисляет движение. Камеры отдают стопкадр в Jpeg. Берем стопкадр, кормим в motion, на выходе получаем то что нужно.

Зоопарк у нас есть, нужно из этого сделать систему.
Так и делаем. Называем всё System и пытаемся сделать слабосвязанную систему, чтобы в будущем можно без потерь заменять элементы системы.
У нас есть камеры, Vlc, Motion, запись на диск, онлайн вещание.

Vlc и motion это unix процессы. Назовем их демонами. (abstract class Daemon)
Получается что с камеры нам нужно 2 потока. jpeg и h264.
В системе создаем сущности
user-dvr-cam-stream
user — пользователь
dvr-система, которая отвечает за демоны и камеры, связанные с демонами
cam — сущности камеры, хранит настройки физической камеры и виртуальные streams
stream — поток, любой абстрактный поток.

Straem можно запускать, останавливать, дисаблить, «дергать» в момент «обновления» системы, по этим эвентам он сам принимает решение о том что делать.

Получаем сущность System-common, которая состоит только из абстрактных объявлений.

Получаем общую фабрику построения системы.
abstract class AbstractFactory {
    private static $instance = null;

    /**
     * @var array
     */
    private $commands;

    /**
     * @return AbstractFactory
     */
    public static function getInstance(){
        if(self::$instance == null) self::$instance = new static;
        return self::$instance;
    }

    /**
     * permanentCommand
     * @param ICommand $command
     */
    protected function addPermanentCommand(ICommand $command){
        $this->commands[] = $command;
    }

    /**
     * @param ISystem $system
     */
    protected function addCommands(ISystem $system){
        foreach($this->commands as $command)
            $system->addPermanentCommand($command);
    }

    //todo buildSystem method
    /**
     * @return ISystem
     */
    public function createSystem(){
        return System::getInstance();
    }

    /**
     * @return array of Users
     */
    public function createUsers(){
        return array(AbstractFactory::getInstance()->createUser(1));
    }

    /**
     * @param int $id
     * @return IUser
     */
    public function createUser($id)
    {
        return new User($id);
    }

    /**
     * @param IUser $user
     * @return IDVR
     */
    public function createDvr(IUser $user)
    {
        $dvr = new Dvr($user);

        $cams = $this->createCams($dvr);    //new+add
        foreach($cams as $cam){
            /** @var $cam Cam */
            $dvr->addCam($cam);
        }

        $daemons = $this->createDaemons($dvr);

        foreach($daemons as $daemon){
            /** @var $daemon Daemon */
            $dvr->addDaemon($daemon);
        }

        return $dvr;
    }

    /**
     * @param DVR $dvr
     * @return array of Cams
     */
    abstract protected function createCams(DVR $dvr);

    /**
     * @param DVR $dvr
     * @return array of Daemons
     */
    abstract protected function createDaemons(DVR $dvr);

    /**
     * @param IDVR $dvr
     * @param ICamSettings $cs
     * @return ICam
     */
    public function createCam(IDVR $dvr, ICamSettings $cs)
    {
        return new Cam($dvr, $cs);
    }

    /**
     * @param $from
     * @param $to
     * @return MoveToNfsCommand
     */
    public function createMoveToNfsCommand($from, $to){
        return new MoveToNfsCommand($from, $to);
    }

    /**
     * @param ICam $cam
     * @return ICamStream
     */
    abstract public function createStream(ICam $cam);
} 



Это нам позволяет на камеру навешивать совершенно любые потоки и делать с ними что нам в голову придет. И позволяет хоть как то контролировать наш зоопарк. (прототип он такой)

Так как у нас рабочая лошадка это VLC, то и начинает подгонять VLC под нашу систему.
Создаем abstract class VlcStream extends Stream, class LiveVlcStream extends VlcStream.
VlcStream «дружит» с Vlm ($this->vlm = new HttpVlm($this->getVlcName(), 'localhost', HTSTART+$this->cam->getDVR()->getID());)
И позволяет обрабатывать start, stop
Далее
abstract class VlcReStream extends VlcStream
VlcReStream — на вход берет LiveVlcStream
class RecVlcStream extends VlcReStream — производит запись, «двигает» видео из Ram в Nfs

И создается class Vlc extends Daemon, он будет содержаться в DVR сущности.

DVR обрабатывает start и stop
public function start()
    {
        $this->log(__FUNCTION__);

        $this->startDaemons();
        $this->startCams();
    }

старт камеры запускает все stream связанные с камерой.
Т.е. Мы запускаем unix процессы Vlc, motion, делаем VLM через HTTP.
Остановка происходит в обратном порядке.
Вместо VLC можно использовать любую вещь, которая понравится.

Но у нас видеонаблюдение в облаке. Значит нужны Sql и подобные системы хранения данных.
Рабочая система получилась такая:

Фабрика
<?php
/**
 * Created by PhpStorm.
 * User: calc
 * Date: 16.06.14
 * Time: 10:56
 */

namespace system2;

/**
 * Class BBFactory
 * @package system2
 */
class BBFactory extends AbstractFactory {
    /**
     * @return ISystem
     */
    public function createSystem()
    {
        $system =  parent::createSystem();

        $e = new BBLogMotionEvent(Motion::EVENT_MOTION_START);
        $system->addEventHandler($e);

        $e = new BBLogMotionEvent(Motion::EVENT_MOTION_STOP);
        $system->addEventHandler($e);

        $e = new BBLogMotionEvent(Motion::EVENT_MOTION_DETECTED);
        $system->addEventHandler($e);

        $e = new BBLogMotionEvent(Motion::EVENT_CAMERA_LOSS);
        $system->addEventHandler($e);

        $recMotionEvent = new BBRecMotionEvent(Motion::EVENT_MOTION_START);
        $system->addEventHandler($recMotionEvent);
        $recMotionEvent = new BBRecMotionEvent(Motion::EVENT_MOTION_STOP);
        $system->addEventHandler($recMotionEvent);

        //удалить записи старше 30 дней при каждом update
        //$system->addPermanentCommand(new RotateRecCommand());
        $this->addPermanentCommand(new RotateRecCommand());

        $this->addCommands($system);

        return $system;
    }

    /**
     * @param DVR $dvr
     * @return array of Daemons
     */
    protected function createDaemons(DVR $dvr)
    {
        $vlc = new Vlc($dvr);
        $this->addPermanentCommand(new BBDaemonWatchdog($vlc));

        // нам необходимы только те камеры, в которых включен mtn
        $cams = $dvr->getCamIDs();

        $ids = array();
        foreach($cams as $id){
            $cam = $dvr->getCam($id);
            $cs = $cam->getSettings();
            /** @var $cs BBCamSettings  */
            if($cs->mtn) $ids[] = $id;
        }

        $motion = new Motion($dvr, $ids);
        if(count($cams))
            $this->addPermanentCommand(new BBDaemonWatchdog($motion));

        return array($vlc, $motion);
    }

    /**
     * @return array
     */
    public function createUsers()
    {
        $users = array();
        $db = \Database::getInstance();
        $q = "select id from users where banned=0";
        $r = $db->query($q);
        while(($row = $r->fetch_row())){
            try{
                $users[] = AbstractFactory::getInstance()->createUser($row[0]);
            }
            catch(\Exception $e){
                Log::getInstance()->put($e->getCode().' '.$e->getMessage()."\n".$e->getTraceAsString()."\n", __CLASS__, Log::ERROR);
            }
        }

        return $users;
    }

    /**
     * @param DVR $dvr
     * @return array
     */
    protected function createCams(DVR $dvr){
        $db = \Database::getInstance();
        $q = mysql::getQuery(mysql::CAM_SETTINGS, array('{dvr_id}' => $dvr->getID()));
        $r = $db->query($q);

        $cams = array();
        while(($row = $r->fetch_object('system2\BBCamSettings')) != null){
            /** @var BBCamSettings $row */

            //$dvr->addCam(new BBCam($this, $row));
            $cam = $this->createCam($dvr, $row);
            $cams[] = $cam;

            //Так как у нас Motion создает раз в минуту картинку, то создаем таймлапсы
            $timelapse = new BBArchiveTimelapseCommand($cam);
            $this->addPermanentCommand($timelapse);
        }
        return $cams;
    }

    /**
     * @param ICam $cam
     * @return ICamStream
     */
    public function createStream(ICam $cam)
    {
        $stream = new Streams($cam);

        $cs = $cam->getSettings();
        /** @var $cs BBCamSettings */
        $motion = new MotionStream($cam, $cs);
        $motion->setEnabled($cs->live && $cs->mtn);
        $stream->addStream($motion);

        $live = new BBLiveStream($cam);
        $live->setTestInputCommand(new BBTestInputFailSaveCommand($cam, $live));
        $live->setEnabled($cs->live);
        $stream->addStream($live);

        $hls = new HLSVlcStream($cam, $live);
        $hls->setEnabled($cs->live);
        $stream->addStream($hls);
        //$this->streams[] = new FlvVlcReStream($this, $live);

        //nginx rtmp stream
        //$this->streams[] = new RtmpVlcReStream($this, $live);

        $rec = new BBRecStream($cam, $live);
        $rec->setEnabled($cs->live && $cs->rec);
        $rec->setTestInputCommand(new BBTestInputFailSaveCommand($cam, $rec));
        $stream->addStream($rec);

        $mtn = new BBRecStream($cam, $live, TIME_LOCK_RECORD, Path::MOTION);
        $mtn->setEnabled($cs->live && $cs->mtn && BBRecMotionEvent::isMotion($cam));
        $mtn->setTestInputCommand(new BBTestInputFailSaveCommand($cam, $mtn));
        $stream->addStream($mtn, Path::MOTION);

        //motion flv stream
        $flv = new UrlFlvVlcStream($cam, "http://localhost:".(MOTION_STREAM_PORT + $cam->getID()));
        $flv->setEnabled($cs->live);
        $stream->addStream($flv);

        return $stream;
    }
} 




createSystem, добавляем интересующие нас эвенты. За их обработку отвечает класс System.
EVENT_MOTION_START, EVENT_MOTION_STOP, EVENT_MOTION_DETECTED, EVENT_CAMERA_LOSS
это всего лишь то, что вызывается из конфига moton
thread
# Command to be executed when an event starts. (default: none)
# An event starts at first motion detected after a period of no motion defined by event_gap
on_event_start php ~/dvr/bin/system2/main.php event {user_id} {cam_id} motion_start 0 "%Y-%m-%d %T"

# Command to be executed when an event ends after a period of no motion
# (default: none). The period of no motion is defined by option event_gap.
on_event_end php ~/dvr/bin/system2/main.php event {user_id} {cam_id} motion_stop 0 "%Y-%m-%d %T"

# Command to be executed when a picture (.ppm|.jpg) is saved (default: none)
# To give the filename as an argument to a command append it with %f
; on_picture_save value

# Command to be executed when a motion frame is detected (default: none)
; on_motion_detected php ~/dvr/bin/system2/main.php event {user_id} {cam_id} motion_detected 0 "%Y-%m-%d %T"

# Command to be executed when motion in a predefined area is detected
# Check option 'area_detect'.   (default: none)
; on_area_detected value

# Command to be executed when a movie file (.mpg|.avi) is created. (default: none)
# To give the filename as an argument to a command append it with %f
; on_movie_start value

# Command to be executed when a movie file (.mpg|.avi) is closed. (default: none)
# To give the filename as an argument to a command append it with %f
; on_movie_end value

# Command to be executed when a camera can't be opened or if it is lost
# NOTE: There is situations when motion don't detect a lost camera!
# It depends on the driver, some drivers dosn't detect a lost camera at all
# Some hangs the motion thread. Some even hangs the PC! (default: none)
on_camera_lost php ~/dvr/bin/system2/main.php event {user_id} {cam_id} motion_camera_lost 0 "%Y-%m-%d %T"


там идет вызов main.php event uid cid event_name параметры
System класс это обрабатывает.
По cron раз в 10 минут (раз в любое количество мину) вызывается System-update.
По update идет выполнение update всех подсистем и
$this->executeCommands();
$this->executePermanentCommands();

Команды это то, что накидали подсистемы, и постоянные команды, такие как RotateRecCommand — отвечает за удаление старых записей.

При создании демонов идет добавление, например, BBDaemonWatchdog, она следит не упал ли демон.

Пользователи уже создаются из БД.
Потом камеры из БД. Добавляется постоянная команда на создание таймлапса. Которая, в свою очередь, срабатывает только раз в TIME_LOCK_TIMELAPSE (8 часов) и сует информацию в Mysql

Последнее — самое интересно, это потоки (streams)
создается поток motion, live, hls (HTTP Live Stream), rec, mtn, flv.
live — обычный http
rec, mtn — потоки для записи
hls и flv для вывода в браузер.

Github: github.com/Calc86/dvr/tree/master/bin/system2

Логика есть. Нужна «физика».
Камер много, потоков много, процессор один.
Вся запись производится в ramdisk, hls в ramdisk, настройка vlc идет таким образом чтобы не было никакого транскодинга. Чистый поток забрали, чистый поток сохранили. Перемещение с ram диска на хранилище (в прототипе было NFS) шло через ffmpeg codec copy
Это позволяло выводить в браузере через html5 записи.
3 вида записей. Полная запись с нарезкой раз в 10 минут, запись по движению и timelaps запись за 8 часов где в секунду выводится 3 минуты.

Построение системы планировалось независимое размещение DVR (машин для записи), даже географически с минимальным изменением кода.

Что было реализовано
1) все желаемые нами записи
2) вывод в web, hls, flash, записей в html5, из-за зоопарка кодеков h264 live в h264 пока не пошел в web, вывод в mjpeg (позволяет смотреть на старых нокиях онлайн видео)
3) web интерфейс этой страшной штуки с https, nginx security url и подобным.
Остановились на добавление камер через Onvif

Почему проект заглох.
1) плохая продажа (отсутствие продажника и системы продаж)
2) отсутствие финансирования (админ как минимум нужен на такую систему)
3) отсутствие свободного времени
4) совершенно бесплатная разработка
5) не желание приобрести камеры на тест
6) не желание поставить бесплатные камеры на тест, хотя с управой района договоренность была.
7) я меняю постоянное место работы. Не будет возможности заниматься этим.
8) Покупка дизайна и покупка нарезки, но в будущем нарезали своими силами.
9) Отсутствие человека, который постоянно мониторил бы систему (нашли баги как в motion, так и в vlc, но решаются они администрированием системы)
10) «Продажники» за год не придумали ни одной системы продаж и тарификации.
11) Я занимался много чем еще параллельно основной работе, созданию видеонаблюдения и личной жизнью.
12) Все забили болт на свои обговоренные обязанности :)

Но у нас было 3 клиента, им нравилось. Деньги за систему не брали, деньги брали только за работу по установке камер.

Как оно выглядело :)
image

Если тема пойдет, то опишу более подробно о VLC и подводных камнях. И о «мифической» железке и опытов с ней тоже.
Tags:
Hubs:
+19
Comments 6
Comments Comments 6

Articles