Хабрахабр

[Из песочницы] Таймлапс собственными силами с облачного сервиса видеонаблюдения IPEYE

Недавно появилась задача ежедневно формировать таймлапс с пары камер видеонаблюдения, подключенных к IPEYE. Если вам интересно как с этим справился человек с минимальными знаниями питона или вы хотите мне указать на мои ошибки — добро пожаловать под кат…

Intro

Мой отец решил переехать и построить дом в другом регионе. Попросил меня подсобить с видеонаблюдением. Вводные данные:

  • Нет технического помещения.
  • Оборудование могут своровать.
  • Нужна качественная картинка.
  • Камеры должны быть уличными.
  • Нужно всего 2 камеры.
  • Очень хочется поворотные камеры с зумом.
  • Очень хочется мобильное приложение.

Взглянув на цены в магазинах на брендированное оборудование было принято решение приобрести noname PoE камеры со всеми плюшками на али. Камеры обошлись достаточно дешево — около 5тыс за штуку.

После того, как пришли камеры я попытался их подружить с разными сервисами так, чтобы всё работало, включая PTZ. Размещать видеорегистратор на стройке не хотелось, поэтому было принято решение использовать облачное решение. Думаю, что теперь всем будет понятно почему разговор пойдет именно про этот сервис. Из всех сервисов, что я попробовал, получилось подружить китайские камеры только с IPEYE.
На этом закончу интро.

Опыт с IPEYE

Сервис как сервис. Всё обещанное выполняет. Техподдержка на вопросы отвечает. Ключевой момент — можно указать ссылку на rstp поток, покрутить настройки PTZ и всё будет работать. Мобильное приложение на android работает. Есть возможность создать гостевых пользователей для своих камер и каждому родственнику раздать права доступа. Веб интерфейс в Vivaldi иногда глючит, в Chrome такие глюки проявляются реже. Немного подтупливает просмотр архива.
Архив с камеры можно скачать, но отрезком до 3 часов. Процедура достаточно затратная по времени.

И не хватает глубины архива. Вроде всё хорошо, но душой чувствуешь, что что-то не то. Увеличить глубину можно, но самый дорогой вариант — 12 месяцев будет обходится 25тыс рублей в год за одну камеру (при записи по детекции).

Давай что-нибудь придумаем?

Именно такой вопрос задал мне отец. Отец захотел запечатлеть все этапы строительства.
Какие есть варианты решения данной задачи? Можно ежедневно заходить в веб интерфейс и экспортировать несколько кусочков видео с каждой камеры. Кто будет выполнять такую муторную задачу? Никто! Несколько раз в день открывать трансляцию камер и делать скриншоты? Ну тоже маразм. Увеличить глубину архива до 1 года? Ну очень не дешевое решение. Гугл мне подсказал, что в этом сервисе уже встроена функция TimeLapse, но разрешение низкое и для архива на будущее не скачать 🙁

Было принято решение — написать что-то, что бы как-то сохранять скриншоты с камеры и формировать итоговое видео.

Disclaimer

Автор данного опуса не является программистом и не стремится им стать. Основы ООП знает поверхностно. Agile и т.п. не изучал. Он месяц назад посмотрел короткий видеокурс по питону и решил его применить для решения текущей задачи.

API

Очень приятно было обнаружить, что сервиса IPEYE публично доступно API. В API приведены примеры только для PHP, но и это оказалось полезным.

Исходя из того, что мой компьютер никогда не выключается была выработана такая концепция:

  1. Windows планировщик раз в 30 минут запускает скрипт.
  2. Скрипт через API определяет uuid моих камер.
  3. Скрипт через API получает фото с камер и сохраняет в каталог.
  4. Скрипт раз в сутки формирует видео файл для каждой камеры.
  5. Каталог с исходными фотками и финальными видео привязан к облаку и расшарен.
  6. Родня когда хочет смотрит таймлапс и оперирует файлами как им хочется.

Для работы с API я написал пару функций: логирование и выполнение запросов к API серверу.

writeLog()

def writeLog(logdata): if LogEnable == 1: log_time = datetime.now() log_time = log_time.isoformat(timespec='seconds') log_file = open(log_file_path, "a+") log_file.write(log_time + ": " + str(logdata) + "\n") log_file.close else: return True

getApiResponse()

def getApiResponse(method, api_uri): if method == "GET": try: r = requests.get(api_url + api_uri, timeout = api_timeout) r.raise_for_status() # включаем обработку HTTP ошибок в эксепшенах except requests.exceptions.Timeout: writeLog("Error. Timeout. Request Uri:" + api_uri) except requests.exceptions.TooManyRedirects: writeLog("Error. TooManyRedirects or bad URL. Request Uri:" + api_uri) except requests.exceptions.RequestException as e: writeLog("Error. Fatal error: " + str(e) + " Request Uri:" + api_uri) sys.exit(1) except requests.exceptions.HTTPError as e: writeLog("Error. HTTP error: " + str(e) + " Request Uri:" + api_uri) if method == "POST": try: r = requests.post(api_url + api_uri, timeout = api_timeout) r.raise_for_status() # включаем обработку HTTP ошибок в эксепшенах except requests.exceptions.Timeout: writeLog("Error. Timeout. Request Uri:" + api_uri) except requests.exceptions.TooManyRedirects: writeLog("Error. TooManyRedirects or bad URL. Request Uri:" + api_uri) except requests.exceptions.RequestException as e: writeLog("Error. Fatal error: " + str(e) + " Request Uri:" + api_uri) sys.exit(1) except requests.exceptions.HTTPError as e: writeLog("Error. HTTP error: " + str(e) + " Request Uri:" + api_uri) return r

Согласно API у нас есть возможность обратиться к /devices/all и получить инфу по всем потокам. Сразу же смутило, что не требуется авторизация, если верить документации по API… При запросе к /devices/all я получил ошибку:

Fatal error: 401 Client Error: Unauthorized for url: api.ipeye.ru:8111/devices/all

Я модифицировал свою функцию getApiResponse, что бы передавать свои учетные данные, но всё также я получал ошибку 401. Листинг функции не привожу, т.к. она в дальнейшем не пригодилась.

Саппорты разъяснили, что для использования API с авторизацией надо сперва заключить договор с IPEYE и настроить свой веб сервер. Пришлось обратиться в техподдержку по этому поводу. Что за веб сервер и для чего мне не разъяснили, но при этом они дали подсказку где взять uuid камер и обращаться к API без авторизации.

Переписка с саппортом

Возможно ли использование API? Я: Добрый день. Хотел получить список своих камер при запросе к api.ipeye.ru:8111/devices/all но моя связка логин/пароль не подходит. uuid камер в web интерфейсе не обнаружил.

IPEYE: В качестве логина/пароля используются авторизационные данные пользователя API, доступ мы предоставляем на договорной основе.

Соответственно и список камер вы увидите не для своего логина, а для всех камер API пользователя.

Я: Сколько стоит получение доступа к API двух камер?

IPEYE: Там все сложнее, вам надо будет запустить свой сайт и используя наш APIдобавить на него камеры и далее работать с ними.
Стоит ли это делать ради 2-х камер?

Для дальнейшего формирования таймлапса. Я: Вообще, для понимания, мне доступ к API нужен только для автоматического скачивания скриншотов с камер.

IPEYE: А чем в таком случае не устраивает api.ipeye.ru/doc#AppDeviceJPEGOnline?

Я: А где можно найти UUID моих камер?

Этот же UUID фигурирует в «Коде для сайта» как параметр devcode. IPEYE: Из адресной строки браузера, например.

Что мы имеем в сухом остатке? Действительно uuid камеры можно найти в личном кабинете, если присмотреться к GET параметрам. Есть метод /device/jpeg/online/:uuid/:name для получения скриншота, есть uuid, а название камеры мы и так знали.

Создаю функцию для сохранения изображений из потока.

saveJpegFromStream()

def saveJpegFromStream(uuid, name): # api_uri = "/device/thumb/online/" + uuid + "/1920/" + name api_uri = "/device/jpeg/online/" + uuid + "/" + name writeLog("Trying save Stream screenshot for camera: " + name) response = getApiResponse("GET", api_uri) content_type = response.headers.get('content-type') if content_type is None: writeLog("Nothing to save") return False if 'text' in content_type.lower() or 'html' in content_type.lower(): writeLog("Received text data: " + response.content.decode("utf-8")) return False else: filename = dirToSave + "\\" + name + "-" + today.strftime("%Y-%m-%d-%H-%M-%S") + ".jpg" screenshot = open(filename, "wb") screenshot.write(response.content) screenshot.close() writeLog("File saved as: " + filename) return True

Делаю запрос и понимаю, что что-то не то… Файл получили. Изображение с моей камеры, но размер подозрительно маленький — 66Кб. Смотрю свойства и понимаю, что 608*342 ну никак не 1920*1080. При запросе /device/thumb/online/uuid/1920/name получаю файл 1920*1080, но это просто растянутый до нужного масштаба предыдущий файл 608*342. Само собой меня такое положение вещей не устроило.

Можно отправлять что душе угодно. Также во время экспериментов обнаруживается, что параметр name никак не проверяется и не используется.

Мне пришлось обратиться к гуглу, т.к. После этого я сделал вывод, что самая полезная команда /device/url/rtsp/ — получение ссылки на RTSP поток. понимания как работать с RTSP вообще не было.

getStreamRTSP()

def getStreamRTSP(uuid, name): api_uri = "/device/url/rtsp/" + uuid writeLog("Trying get stream RTSP link for " + name + " " + uuid) response = json.loads(getApiResponse("GET", api_uri).text) writeLog("Stream RTSP link for " + name + ": " + str(response["message"])) return str(response["message"])

saveJpegFromRTSP

def saveJpegFromRTSP(name, rtspLink): writeLog("Trying save RTSP screenshot for camera: " + name) rtspClient = cv2.VideoCapture(rtspLink) if rtspClient.isOpened(): _,frame = rtspClient.read() rtspClient.release() # закрываем поток сразу после получения карда if _ and frame is not None: filename = dirToSave + "\\" + name + "-" + today.strftime("%Y-%m-%d-%H-%M-%S") + ".jpg" cv2.imwrite(filename, frame) writeLog("File saved as: " + filename) return True else: writeLog("Can't read RTSP stream") return False

После того как я написал пару функций выше — пришло осознание, что для выполнения начальной задачи использовать API вообще не было потребности 🙂 У меня есть прямая ссылка на rtsp поток камеры, а при помощи последней функции я могу захватить изображение с камеры. Но менять концепцию на данном шаге я не стал.

При работе через API есть 2 преимущества: сервер IPEYE инициализирует поток значительно быстрее чем камера, а также rtsp поток камеры может быть закрыт файрволлом.

jpg2mp4

Последним шагом осталось добавить все изображения одной камеры в видео. Я выбрал кодек mp4v, т.к. MEGA позволяет воспроизводить данные видео файлы в веб интерфейсе.

makeVideoFile()

def makeVideoFile(name): height = 1080 width = 1920 # video = cv2.VideoWriter(dirToSave + "\\Video" + name + ".avi", cv2.VideoWriter_fourcc(*'DIVX'), 1,(width,height)) video = cv2.VideoWriter(dirToSave + "\\Video" + name + ".mp4", cv2.VideoWriter_fourcc(*'mp4v'), 1,(width,height)) files = os.listdir(dirToSave) screenshots = list(filter(lambda x: x.startswith(name + "-"), files)) for screenshot in screenshots: origImage = cv2.imread(dirToSave + "\\" + screenshot) # Если изображение, пихуемое в видео поток, не соответсвует по габаритам потока - ничего не запихнется... Поэтому резайзим # Зачем ресайзить, если мы взяли до этого изображение из FullHD потока? Что бы была возможность добавить в видео изображение полученное через API heightOrig, widthOrig, channelsOrig = origImage.shape if height != heightOrig or width != widthOrig: img = cv2.resize(origImage, (width, height)) video.write(img) else: video.write(origImage) cv2.destroyAllWindows() video.release()

Для справки: 45 jpg файлов общим объемом 37,3Мб в формате видео занимают 16,9Мб.

Я постарался всё описать в формате истории, а не простом to do, т.к не хотел получить на выходе сухую статью. Спасибо, что прочитали мою первую публичную статью.

Буду рад замечаниям, т.к с питоном познакомился меньше месяца назад.

Также в скрипте учтена разница в часовых поясах, да и все оставшиеся функции написаны на быстрое масштабирование на другое количество камер. Полный текст скрипта с расширенными комментами можно найти на github.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»