[Из песочницы] Таймлапс собственными силами с облачного сервиса видеонаблюдения 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, но и это оказалось полезным.
Исходя из того, что мой компьютер никогда не выключается была выработана такая концепция:
- Windows планировщик раз в 30 минут запускает скрипт.
- Скрипт через API определяет uuid моих камер.
- Скрипт через API получает фото с камер и сохраняет в каталог.
- Скрипт раз в сутки формирует видео файл для каждой камеры.
- Каталог с исходными фотками и финальными видео привязан к облаку и расшарен.
- Родня когда хочет смотрит таймлапс и оперирует файлами как им хочется.
Для работы с API я написал пару функций: логирование и выполнение запросов к API серверу.
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
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? uuid камер в web интерфейсе не обнаружил. Хотел получить список своих камер при запросе к api.ipeye.ru:8111/devices/all, но моя связка логин/пароль не подходит.IPEYE: В качестве логина/пароля используются авторизационные данные пользователя API, доступ мы предоставляем на договорной основе.
Соответственно и список камер вы увидите не для своего логина, а для всех камер API пользователя.
Я: Сколько стоит получение доступа к API двух камер?
IPEYE: Там все сложнее, вам надо будет запустить свой сайт и используя наш APIдобавить на него камеры и далее работать с ними.
Стоит ли это делать ради 2-х камер?Я: Вообще, для понимания, мне доступ к API нужен только для автоматического скачивания скриншотов с камер. Для дальнейшего формирования таймлапса.
IPEYE: А чем в таком случае не устраивает api.ipeye.ru/doc#AppDeviceJPEGOnline?
Я: А где можно найти UUID моих камер?
IPEYE: Из адресной строки браузера, например. Этот же UUID фигурирует в «Коде для сайта» как параметр devcode.
Что мы имеем в сухом остатке? Действительно uuid камеры можно найти в личном кабинете, если присмотреться к GET параметрам. Есть метод /device/jpeg/online/: uuid/: name для получения скриншота, есть uuid, а название камеры мы и так знали.
Создаю функцию для сохранения изображений из потока.
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 вообще не было.
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"])
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 позволяет воспроизводить данные видео файлы в веб интерфейсе.
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. Также в скрипте учтена разница в часовых поясах, да и все оставшиеся функции написаны на быстрое масштабирование на другое количество камер.