Автоматизация обработки видео-файлов с web-камер средствами shell

Понадобилось начальству в своё время организовать своими силами видео-наблюдение за некоторыми вещами и уложиться в минимальное финансирование. Задача автоматизировать это легла на плечи системного администратора, то есть – меня.
Дано: N – видео-камер D-Link 2102, физический двух-юнитовый сервер под сервер видео-наблюдения и удаленное файло-хранилище.
Результатом должна быть возможность пускать некоторых пользователей на сервер видеонаблюдения в онлайн режиме и организовать архив видеозаписей.

Под катом несколько скриптов, которые сильно помогли мне понять, как лучше писать код, зачем нужны многие вещи и как они решаются, а так же навести порядок в своей голове и очень надеюсь, что они помогут кому-то еще.
Проект писался достаточно сложно — настолько сильно я shell(bash) скрипты не изучал — не было надобности до этого.
Но, когда задача поставлена и в голове есть алгоритм решения — все скрипты были переделаны так, чтобы читая их через пол года у меня и моего преемника не было вопросов и желания переписать все с нуля.

UPD: пост периодически обновляется.


Дистрибутивом выбран Ubuntu-Server 10.04. Серверной частью для онлайн просмотра стал ZoneMinder.Как настраивать Ubuntu и ZoneMinder написано много документации, поэтому этот вопрос останется за пределами данного документа, мне бы хотелось рассказать про автоматизацию ведения архива.

Было решено, что в качестве основного приёмника записей с камер (минутные ролики, это логически оправданно с точки зрения минимизации потерь видеозаписей) будет сервер на котором крутится ZoneMinder. На сервере развернут Samba-server как оптимальный и простой способ решения приема видео с камер и связующее звено между камерами и файло-хранилищем.

Все файлы, касающиеся видео-архива, находятся в /home/ipcamera/camname.
/home/ipcamera/scripts – скрипты, выполняющиеся по расписанию в /etc/crontab раз в час.
/home/ipcamera/out – монтируемая с файло-хранилища шара, куда складывается обработанное видео.

В зависимостях у нас samba tools для монтирования раздела где хранится архив по самбе, и bc, как консольный калькулятор, плюс кодеки и avimerge.

Задачи:
1. Получить с камер видео (одноминутный ролик)
2. Слить 60 минутных роликов в один.
3. Выгрузить полученное часовое видео на файло-хранилище.
4. Удалить с сервера видео-наблюдения всё видео, которое старше 3х дней.
5. Периодически удалять из архива видео, которое старше 3х месяцев.
6. Дать доступ к архиву тому, кому надо и не дать тем, кому не надо.

На сетевом уровне камеры доступны для сервера видеонаблюдения, могут быть сконфигурированы через веб интерфейс, файло-хранилище так же доступно.

Все скрипты откомментированы по максиму.

В настройках камер выставляем настроенный в организации ntp сервер и время записи видео, к примеру с 9 до 18 в настройках самих камер.

Да, про качество записи я в курсе и этот вопрос не рассматривается в данной статье, как и законность таких само-сборных систем наблюдения.

Настройка Samba и Zoneminder тут тоже не рассматриваются, хотя без кусочка конфига Samba тут не обойтись, и он будет.

Первый конфиг — кусочек конфигурации планировщика.

Похоже, кое-где слететели отступы в форматировании, прошу меня простить
#cat /etc/crontab
5  9-20 * * 1-6 root  /home/ITC/ipcamera/scripts/split
# Собираем часовые ролики
10 9-20 * * 1-6 root  /home/ITC/ipcamera/scripts/upload
# Загрузка обработанного видео на файло-хранилище
30 20   * * 1-6 root  /home/ITC/ipcamera/scripts/clear
# Очистка локального сервера от устаревшего видео
0  21   * * 6   root  /home/ITC/ipcamera/scripts/archieve
# Чистим архив.
0  23   * * *   root  rm /var/spool/nullmailer/queue/*
# Чистим почтовую очередь каждый вечер – за пару дней mail spooler забивает раздел.

Пойдем по порядку:
Конфигурационный файл для всего этого безобразия.
#cat scripts.conf
# Основной каталог с общими каталогами для видео с камер.
camshare="/home/ipcamera/"
# Расширение файлов
filename="*.avi"
# Текущая дата для процедуры слияния
currentdate=`date +%Y%m%d"/"%H`
# Путь каталога, к которому монтируется удаленно, по smb, файло-хранилище  для выходных файлов.
outfiles="out/"
# Lock-файл, индикатор что программа уже запущенна.
lockfile=`basename $0`".lock"
# Файл журнала.
logfile="logfile"
# Временный файл
tmpfile=`basename $0`".tmp"
# Путь на удаленном сервере для монтирования локально
upload_share_name="//filearchieve/web_camera_video"
# Локальный путь для выгрузки
upload_share_path="/home/ipcamera/out"
# Имя пользователя для подключения к каталогу выгрузки
upload_share_user="ipcamerauser"
# Пароль
upload_share_passwd="ipcamerapassword"
# Команда монтирования
share_mount_command="mount -t cifs"
# Период хранения архива, в месяцах.
Age=3

Файлик с описанием функций, чтобы разгрузить скрипты, и унифицировать переменные
#cat functions.sh
# Библиотека функций для скриптов обработки видеонаблюдения.
# ver 1.12.
### Общие переменные для всех скриптов вынесены в файл конфигурации и подключаются тут.
. /home/ipcamera/scripts/scripts.conf
# Запись сообщения в журнал
log_msg() {
        echo `date +%Y"/"%m"/"%d" "%T`" Скрипт "`basename $0`": $1." >> $camshare$logfile
}
# Создание файла блокировки при запуске скрипта - флага о том что скрипт уже запущен и выполняется.
lock_on() {
        # Проверка, есть ли файл блокировки, если да - запись в лог и выход.
    if [ -f $camshare$lockfile ]; then
        echo `date +%d"/"%m"/"%Y" "%T`" Присутствует файл блокировки, возможно $0 уже запущен? программа завершает работу."
        log_msg "Присутствует файл блокировки, программа завершает работу"
        exit
      else
        # Если файла блокировки нет - создаем.
        touch $camshare$lockfile
        echo `date +%d"/"%m"/"%Y" "%T`" Создаем файл блокировки"
    fi
}

# Удаление файла блокировки при окончании работы скрипта.
lock_off() {
    if [ -f $camshare$lockfile ]; then
        # Удаляем файл блокировки.
        rm $camshare$lockfile
        echo `date +%d"/"%m"/"%Y" "%T`" Файл блокировки успешно удалён."
      else
        # Выводим сообщение о том, что файла блокировки уже нет.
        echo `date +%d"/"%m"/"%Y" "%T`" Файл блокировки не найден."
    fi
}
# Удаление временного файла после окончания работы скрипта.
clear_tmp() {
    if [ -f $camshare$tmpfile ]; then
        # Удаляем временный файл.
        rm $camshare$tmpfile
        echo `date +%d"/"%m"/"%Y" "%T`" Временный файл $tmpfile удалён."
      else
        # Временный файл не существует.
        echo `date +%d"/"%m"/"%Y" "%T`" Временный файл $tmpfile не найден."
    fi
}
# Монтирование каталога для выгрузки.
upload_share_mount() {
    # Проверка, примонтирован ли каталог для выгрузки.
    test=`mount | grep "$upload_share_path"`
    if [ "$?" -eq "0" ] ; then
        # Если примонтирован - ничего не делаем.
        echo "Хранилище $upload_share_path ужё подключено."
    else
        # Монтируем каталог выгрузки.
        `$share_mount_command $upload_share_name $upload_share_path -o user=$upload_share_user"%"$upload_share_passwd`
        # Проверка результата.
        if [ "$?" -eq "0" ] ; then
                # Проверка закончилась положительно.
                echo "Хранилище $upload_share_path только что было успешно подключено."
            else
                # Запись в журнал об отрицательном результате и выход.
                echo "ОШИБКА: Хранилище $upload_share_path не было подключено. Завершение работы."
                log_msg "ОШИБКА: Хранилище $upload_share_path не было подключено. Завершение работы."
                exit
        fi
    fi
}
# Отключение примонтированного каталога для выгрузки.
upload_share_umount() {
    # Проверка, подключен ли каталог для выгрузки.
    test=`mount | grep "$upload_share_path"`
    if [  "$?" -eq "0" ] ; then
        # Результат положительный, отключаем.
        `umount "$upload_share_path"`
        echo "Хранилище $upload_share_path только что было успешно отдключено."
    else
        # Выводим сообщение, что каталог уже отключен.
        echo "Хранилище $upload_share_path уже отключено."
    fi
}

Файлик, выполняющий слияние минутных видеороликов с камер (этот параметр в камерах, к сожалению, изменить нельзя, но это целесообразно с точки зрения минимизации потерь при записи и обрыве линка между камерой и принимающим сервером)
#cat split

#!/bin/sh

## Скрипт выполняет слияние всех видеофайлов в каталоге за прошедший час.

#ver 1.0.

# Подключаем общую для всех скриптов библиотеку функций.
. /home/ipcamera/scripts/functions.sh

lock_on
log_msg "Запущен"

# Ищем все каталоги по шаблону, убирая из вывода выходные файлы и файлы текущего часа во временный файл.
find $camshare -type d | awk {'FS="/"} {print"/"$2"/"$3"/"$4"/"$5"/"$6"/"$7"/"$8}' | grep -v '/$' | grep -v '$outfiles' | grep -v $currentdate > $camshare$tmpfile

# Переходим в найденный каталог, где сортируем файлы, выполняем их слияние и удаляем исходный файл.
for i in `cat $camshare$tmpfile` ; do cd $i ; [ -f $i.avi ] || avimerge -i `ls | sort` -o $i.avi ; rm -fr $i ; done

clear_tmp
log_msg "Завершён"
lock_off

Скрипт, выгружающий все обработанное видео на файло-хранилище.
#cat upload

#!/bin/sh
. /home/ipcamera/scripts/functions.sh

## Скрипт выполняет загрузку файлов, прошедших слияние, в каталог внешнего сервера.

#ver 2.2.

lock_on
log_msg "Запущен"
upload_share_mount

# Ищем все файлы по шаблону AA.avi, убирая из вывода каталоги, выходные файлы и текущую дату во временный файл.
find "$camshare" -type f -name '[0-9][0-9].avi' | awk '{FS="/"} {print"/"$2"/"$3"/"$4"/"$5"/"$6"/"$7"/"$8}' | grep -v "/$" | grep -v "$outfiles" | grep -v "$currentdate" > "$camshare$tmpfile"

# Копируем файлы с проверкой на присутствие каталога с названием города/датой - в случае отсутствия создаем каталог камеры и каталог с датой создания.
for source in `cat $camshare$tmpfile` ;
do
    city=`echo "$source" | awk -F / '{ print $5 }'`
    date=`echo "$source" | awk -F / '{ print $7 }'`
    fname=`echo "$source" | awk -F / '{ print $8 }'`
    [ -d "$camshare$outfiles$city" ] || mkdir "$camshare$outfiles$city" | echo "Каталог для $city создан."
    [ -d "$camshare$outfiles$city/$date" ] || mkdir "$camshare$outfiles$city/$date" | echo "Каталог $camshare$outfiles$city/$date создан."
    [ -f "$camshare$outfiles$city/$date/$fname" ] || cp "$source" "$camshare$outfiles$city/$date/$fname"
done

clear_tmp
upload_share_umount
log_msg "Завершен"
lock_off

Теперь – очистка сервера видеонаблюдения от файлов старее 3х дней
#cat clear

#!/bin/sh
## Скрипт, удаляющий все видеофайлы старше 3х дней локально на видеосервере.
#ver 1.0
# Подключаем общую для всех скриптов библиотеку функций.
. /home/ipcamera/scripts/functions.sh

lock_on
log_msg "Запущен"
upload_share_mount

# Основная программа
offset="172800" # 3 полных суток в секундах.# TODO вывести это в общие переменные
unixdate=`date +%s` # текущая дата в скундах.
timediff=`echo "$unixdate"-"$offset" | bc` # разница между текущим временем в секундах и offset'ом.

# Ищем все файлы по шаблону AA.avi, убирая из вывода каталоги, выходные файлы и файлы текущей даты во временный файл.
find "$camshare" -name '[0-9][0-9].avi' | awk '{FS="/"} {print"/"$2"/"$3"/"$4"/"$5"/"$6"/"$7"/"$8}' | grep -v '/$' | grep -v "$outfiles" | grep -v `date +%Y%m%d` > $camshare$tmpfile

# В цикле проходим построчно по временному файлу.
for source in `cat "$camshare$tmpfile"` ;
do
    city=`echo "$source" | awk -F / '{ print $5 }'` # выделяем в переменную "город".
    date=`echo "$source" | awk -F / '{ print $7 }'` # -"- "дата".
    fname=`echo "$source" | awk -F / '{ print $8 }'` # -"- "имя файла".
    filedate=`ls -l --time-style=long-iso "$source" | awk '{print $6}'` # дата файла.
    unixfiledate=`date +%s -d"$filedate"` # переводим дату файла в секунды.
    outfilename="$camshare$outfiles$city/$date/$fname" # формируем имя выходного файла.

    # Если файл уже выгружен на фтп сервер, удаляем его с сервера наблюдения, если он старше timediff'а, если нет - оставляем.
    if [ -f "$outfilename" ] ; then
    echo "$outfilename присутствует в хранилище."
        if [ "$unixfiledate" -lt "$timediff" ] ; then
        echo "$source удален."
            rm "$source"
        else
            echo "$source" "$unixfiledate пропущен."
        fi
    else
        echo "Файл $outfilename не существует."
    fi
done

# Удаляем пустые каталоги.
find "$camshare" -type d -empty | grep 'video' | xargs rm -fr {}

upload_share_umount
clear_tmp
log_msg "Завершен"
lock_off

И на закуску
#cat archieve
#!/bin/sh

## Скрипт, удаляющий все видеофайлы старше определённого периода на сервере видеоархива.

#ver 1.0.
# Подключаем общую для всех скриптов библиотеку функций.
. /home/ipcamera/scripts/functions.sh

lock_on

log_msg "Запущен"

upload_share_mount

# Основная программа.

Year=`date +%Y`
Month=`date +%m`
Old=`echo "$Month"-"$Age"|bc`

find "$camshare" -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'| grep "$outfiles" > $camshare$tmpfile

# В цикле проходим построчно по временному файлу.
for source in `cat "$camshare$tmpfile"` ;

do
    ddate=`echo "$source" | awk -F / '{ print $7 }'`
    Y=`echo $ddate | head -c4`
    M=`echo $ddate | head -c6 | tail -c2`
    D=`echo -n $ddate | tail -c2`

    if [ $Y -eq `date +%Y`  ] ; then
#       echo "$source создан в текущем году."
            if [ "$M" -le "$Old"  ] ; then
#               echo "Каталог $source старый, можно удалять."
                rm -rf $source
            else
                echo "Имя каталога $source находится в пределах хранимого промежутка ($Age месяца), поэтому пропущен."
            fi
        else
#           echo "Каталог $source создан не в этом году и требуется его обработка вручную."
            # TODO : вставить проверку mtime и переименовывать, пока 2010 год не трогаем.
            echo $source >> $camshare/2010.log
    fi
done

    #TODO вставить обработку и процедуру переименования каталога и его содержимого.

upload_share_umount
clear_tmp
log_msg "Завершен"
lock_off

Дело в том, что иногда на камерах слетает дата и они начинают писать в 2010 год, но по времени создания файлов можно понять, когда же фактически они были созданы и переименовать их как надо, именно на эту процедуру меня пока не хватило.

про ресурсоемкость:
#uptime
17:49:27 up 2:21, 1 user, load average: 4.27, 4.25, 4.24
больше загружен процессор, чем память. это ZoneMinder старается.

По логам:
На порядка 10 камер сейчас время выполнения такое (HP Proliant DL560 G1, 1Gb памяти, Xeon 2х2188.804 MHz):
2011/10/29 20:30:01 Скрипт clear: Запущен.
2011/10/29 20:30:11 Скрипт clear: Завершен.
2011/10/29 21:00:01 Скрипт archieve: Запущен.
2011/10/29 21:00:12 Скрипт archieve: Завершен.
2011/10/31 09:05:01 Скрипт split: Запущен.
2011/10/31 09:05:30 Скрипт split: Завершён.
2011/10/31 09:10:02 Скрипт upload: Запущен.
2011/10/31 09:10:16 Скрипт upload: Завершен.

Создание файловой шары для каждой камеры
#cat mkshare

#!/bin/bash

## Скрипт создает каталог для новой камеры.

# Version name: 2.0.

# Все используемые функции описаны в подключенной библиотеке functions.sh.

# Подключаем общую библиотеку.
. /home/ipcamera/scripts/functions.sh

lock_on
log_msg "Запущен"

# Если аргументов при запуске скрипта нет - выходим с ошибкой.
if [ "$1" != "" ]; then

        mkdir "$camshare$1" # Создаем каталог для новой камеры
        chmod 777 "$camshare$1" # Меняем права доступа

        # Дописываем в конфиг-файл samba сервера нужные строки.
        echo -e "\n[$1]\n  comment = $1\n  browseable = yes\n  path = $camshare$1\n  printable = no\n  guest ok = yes\n  read only =no\n  create mask = 0700" >> /etc/samba/smb.conf

        # Перезапускаем samba.
        service smbd restart

        # Выводим дополнительное напоминание.
        echo "!!! Не забудьте проверить наличие IP адреса камеры в Samba ACL, находится в начале секции с описаниями общих каталогов для камер, и перезагрузить samba сервер: sudo service smbd restart"

        log_msg "Общий каталог $1 успешно создан."

    else

        # Просим после имени скрипта ввести название каталога для камеры.
        echo "Введите имя каталога после $0."

    fi

Кусочек конфига самбы:
allow hosts = 10.0.0.1, 10.0.0.2
# сюда пишем айпишники всех камер и всех компьютеров сотрудников, которые имеют доступ к архиву.

[camname1]
  comment = camname1
  browseable = yes
  path = /home/ipcamera/camname1
  printable = no
  guest ok = yes
  read only = no

В камерах сейчас настроен сервер по IP адресу и имя camname.

##TODO список.
1. Проверка, установлены ли нужные пакеты, возможно сборка в deb всего этого безобразия.
2. Мониторинг доступности камер и оповещение ответственных.
3. Локализация скриптов.
4. deb пакет и нормальный man/texinfo.
5. Наверное стоило бы убрать вывод многих сообщений в &> /dev/null но я с этим пока не разбирался.
6. Дописать код, ответственный за проверку зависимостей и запрос на установку оных (возможна связь с пунктом 4)
7. Поскольку самба интегрирована с доменом, возможно организовать доступ по логинам сотрудников, но поскольку камеры с доменными логинами не очень хорошо работают — есть ли смысл опять же.
8. Пока не реализован мониторинг камер на живость линка к ним (ping).
9. Пока не реализован мониторинг того, что камера сбросилась или просто не получила время по ntp и начала писать видео в 2010м году.

На последние два этапа — локализацию переменных на разные языки, нормальную документацию и пакет меня тоже не хватило, да и есть ли смысл, если фактически скрипты выполняются автоматически и на сервер я захожу хорошо если раз в месяц — когда в голове полностью созрел весь алгоритм работы, и соответственно проверки на ошибки, вмешательство оператора стало практически не нужным, сегодня у сервера был uptime в несколько месяцев, и была выполненная первая перезагрузка по обновлению ядра, samba и Kerberos, которые ответственны за привязку сервера к Active Directory.

Единственное, что меня смущает — переполнение раздела с почтой, потому как по хорошему весь вывод надо подавлять, кроме сообщений в log файл.

На этом спасибо, надеюсь я Вам помог.
С удовольствием приму рецепты по оптимизации скриптов и развитию идеи в дальнейшем.

Исходники доступны тут: на GitHub
Поделиться публикацией

Комментарии 16

    +3
    сейчас постараюсь переформатировать.
    в предпросмотре топик выглядел гораздо симпатичнее, прошу прощения
      +1
      Огромное спасибо, как раз встал вопрос о записи нескольких камер.
        0
        Будут идеи — пишите, будем придумывать дальше.
        Есть у меня мысль по развитию проекта
        0
        Мне подсказали что нет фактической проверки, отмонтировалась ли шара на файло-хранилище вот в этом куске:
         test=`mount | grep "$upload_share_path"`
            if [  "$?" -eq "0" ] ; then
                # Результат положительный, отключаем.
                `umount "$upload_share_path"`
                echo "Хранилище $upload_share_path только что было успешно отдключено."
        

        фактически надо вставить блок проверки.
          0
          Сколько скриптов ради того, что в erlyvideo уже работает и отлажено!
            0
            Чем motion не угодил? Умеет делать все красиво, гибок. Справляется с числом 1+ камер. Пишет только когда в поле видимости камеры происходит действие.
              0
              я тоже за motion.

              ## Скрипт, удаляющий все видеофайлы старше определённого периода на сервере видеоархива.
              15 5 * * * find /mnt/sdb1/cam/cam_14/*.avi -mtime +90 -type f -exec rm -rf {} \; >> /dev/null 2>&1
                0
                6 локальных аналоговых камер ( 6 тюнеров на bttv) + 8 разнообразных отдающих mjpeg по сети + 4 вебки usb на uvc +
                все это крутится на машинке собранной из остаточного дэсктопного железа, проц E7500,2gb,6X2TB(mdraid mirror)
                  0
                  C обработкой 2010 года…
                  Таким однострочным скриптом и я могу, но там надо по каждому файлу проходиться, это писалось как раз для того, чтобы потом переименовывать файлы из 2010 года.
                  0
                  Как быть со звуком?
                    0
                    Как настроите камеры.
                    ну и строку перекодирования тоже, соответственно.
                    Если каналы позволяет, можно хоть FullHD писать :)
                      0
                      Не встречалось решения для приёма v4l и звука(синхронно)?
                        0
                        Нет. Это более другой класс решений, видимо… hand-made решения они такие…
                    0
                    у меня звук пишется с line-in поэтому проблем нет…
                      0
                      Тоже, как вариант, если оборудование позволяет
                      У нас вынесенные камеры, там только ethernet + 220v.

                      0
                      А как обрабатывать видео, чтобы фильтровать ролики, в которых ошибочно сработал датчик движения, например, на изменение освещённости или разные снежинки отдельные падают и т.п.?

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое