Так получилось, что у меня в доме давно используются видеокамеры наблюдения.
Сначала это было всего лишь «посмотреть, что там перед воротами», потом — неплохо бы контролировать что делают кошки‑собаки, ну и в общем, получилось довольно много в разных местах.

Много, но недостаточно: например, однажды вышла глупая ситуация, когда закончился пакет корма для собаки, при этом все считали что в кладовке уже лежит запасной, а оказалось — не лежит!
Пришлось срочно ехать в магазин за маленьким пакетиком «не того», пока привезут «тот».
А всего‑то надо было просто заглянуть в кладовку заранее — но это же надо идти туда...

Аналогично — когда нужно просто заглянуть в гараж, например, или еще в какое хозяйственное помещение.
Конечно, камеры там тоже можно повесить, но возникает неожиданнная проблема: неудобно смотреть!

Во‑первых, у регистратора всего 16 «окошек». То есть, сколько их не перелистывай — еще одну камеру туда не подключить, это ограниченный ресурс. Можно поставить еще один регистратор — но тогда надо будет при просмотре переключаться еще и между ними, что добавляет неудобств.
Во‑вторых, через регистратор вообще не очень удобно смотреть — это телевизор, мышь управления — намного удобнее через приложение на планшете, «прибитом к стене гвоздями» — но тогда переключаться будет еще неудобнее.
Запускать же новое приложение на смартфоне — нудно и долго.

Да и подключать, занимать канал ради того, что не нужно записывать и хранить, а только иногда заглядывать «здесь и сейчас» — как‑то неправильно.
Хотелось бы просто «проходя мимо экрана — тыкнуть пальцем и увидеть» — тем более что как раз для этого есть планшет с экраном мониторинга систем (не Home Assistant) — туда и вывести.

Причем, видео даже не нужно — достаточно снимка. К счастью, многие камеры дают такую возможность, получить текущий скриншот. Остаётся только это настроить.

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

Для начала — посмотрим что там за порты открыты на камере:

nmap  192.1168..1.10
...
  80/tcp   open  http 
  554/tcp  open  rtsp 
  8899/tcp open  ospf-lite  

Порт 80 — это вебинтерфейс, он не интересует, потому что скорее всего там предложат установить ActiveX компонент, который работает далеко не в каждом браузере и только под Виндовс.
Порт 554 — это видеопоток RTSP, но сейчас он нам не нужен.
А вот 8899 — это ONVIF, туда можно отправлять запросы.

Например, можно запросить список профилей для камеры:

curl 192.168.1.10:8899/onvif/Media \
  -d '<?xml version="1.0" encoding="UTF-8"?>
  <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
    <soap:Body><trt:GetProfiles/></soap:Body>
  </soap:Envelope>'

Но это неточно. Запрос может быть /onvif/device_service, или /onvif/media_service, или не тот и не другой, или тот и другой, или вообще URL not found.

Если повезло — ответ будет примерно таким:

Многабукв
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2000/10/XMLSchema" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:ns1="http://www.w3.org/2005/05/xmlmime" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:ns7="http://docs.oasis-open.org/wsrf/r-2" xmlns:ns2="http://docs.oasis-open.org/wsrf/bf-2" xmlns:dndl="http://www.onvif.org/ver10/network/wsdl/DiscoveryLookupBinding" xmlns:dnrd="http://www.onvif.org/ver10/network/wsdl/RemoteDiscoveryBinding" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:ns10="http://www.onvif.org/ver10/replay/wsdl" xmlns:ns11="http://www.onvif.org/ver10/search/wsdl" xmlns:ns13="http://www.onvif.org/ver20/analytics/wsdl/RuleEngineBinding" xmlns:ns14="http://www.onvif.org/ver20/analytics/wsdl/AnalyticsEngineBinding" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:ns15="http://www.onvif.org/ver10/events/wsdl/PullPointSubscriptionBinding" xmlns:ns16="http://www.onvif.org/ver10/events/wsdl/EventBinding" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:ns17="http://www.onvif.org/ver10/events/wsdl/SubscriptionManagerBinding" xmlns:ns18="http://www.onvif.org/ver10/events/wsdl/NotificationProducerBinding" xmlns:ns19="http://www.onvif.org/ver10/events/wsdl/NotificationConsumerBinding" xmlns:ns20="http://www.onvif.org/ver10/events/wsdl/PullPointBinding" xmlns:ns21="http://www.onvif.org/ver10/events/wsdl/CreatePullPointBinding" xmlns:ns22="http://www.onvif.org/ver10/events/wsdl/PausableSubscriptionManagerBinding" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:ns3="http://www.onvif.org/ver10/analyticsdevice/wsdl" xmlns:ns4="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:ns5="http://www.onvif.org/ver10/display/wsdl" xmlns:ns8="http://www.onvif.org/ver10/receiver/wsdl" xmlns:ns9="http://www.onvif.org/ver10/recording/wsdl" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:trt2="http://www.onvif.org/ver20/media/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tnsn="http://www.eventextension.com/2011/event/topics"><SOAP-ENV:Body><trt:GetProfilesResponse><trt:Profiles fixed="true" token="000"><tt:Name>Profile_000</tt:Name><tt:VideoSourceConfiguration token="000"><tt:Name>VideoS_000</tt:Name><tt:UseCount>3</tt:UseCount><tt:SourceToken>000</tt:SourceToken><tt:Bounds height="1520" width="2592" y="0" x="0"></tt:Bounds></tt:VideoSourceConfiguration><tt:AudioSourceConfiguration token="000"><tt:Name>Audio_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:SourceToken>000</tt:SourceToken></tt:AudioSourceConfiguration><tt:VideoEncoderConfiguration token="000"><tt:Name>VideoE_000</tt:Name><tt:UseCount>1</tt:UseCount><tt:Encoding>H264</tt:Encoding><tt:Resolution><tt:Width>2304</tt:Width><tt:Height>1296</tt:Height></tt:Resolution><tt:Quality>4</tt:Quality><tt:RateControl><tt:FrameRateLimit>16</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>2774</tt:BitrateLimit></tt:RateControl><tt:H264><tt:GovLength>2</tt:GovLength><tt:H264Profile>High</tt:H264Profile></tt:H264><tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>224.1.2.3</tt:IPv4Address></tt:Address><tt:Port>0</tt:Port><tt:TTL>0</tt:TTL><tt:AutoStart>false</tt:AutoStart></tt:Multicast><tt:SessionTimeout>PT10S</tt:SessionTimeout></tt:VideoEncoderConfiguration><tt:AudioEncoderConfiguration token="000"><tt:Name>AudioE_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:Encoding>G711</tt:Encoding><tt:Bitrate>64</tt:Bitrate><tt:SampleRate>8</tt:SampleRate><tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>224.1.2.3</tt:IPv4Address></tt:Address><tt:Port>0</tt:Port><tt:TTL>0</tt:TTL><tt:AutoStart>false</tt:AutoStart></tt:Multicast><tt:SessionTimeout>PT10S</tt:SessionTimeout></tt:AudioEncoderConfiguration><tt:VideoAnalyticsConfiguration token="000"><tt:Name>Analytics_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:AnalyticsEngineConfiguration><tt:AnalyticsModule Type="tt:CellMotionEngine" Name="MyCellMotionEngine"><tt:Parameters><tt:SimpleItem Value="50" Name="Sensitivity"></tt:SimpleItem><tt:ElementItem Name="Layout"><tt:CellLayout Columns="22" Rows="18"><tt:Transformation><tt:Translate x="-1.0" y="-1.0" /><tt:Scale x="0.09090" y="0.111111" /></tt:Transformation></tt:CellLayout></tt:ElementItem></tt:Parameters></tt:AnalyticsModule><tt:AnalyticsModule Type="tt:TamperEngine" Name="MyTamperEngine"><tt:Parameters><tt:SimpleItem Value="50" Name="Sensitivity"></tt:SimpleItem><tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/></tt:Polygon></tt:PolygonConfiguration></tt:ElementItem><tt:ElementItem Name="Transform"><tt:Transformation><tt:Translate x="-1.0" y="-1.0"/><tt:Scale x="0.001250" y="0.001667"/></tt:Transformation></tt:ElementItem></tt:Parameters></tt:AnalyticsModule></tt:AnalyticsEngineConfiguration><tt:RuleEngineConfiguration><tt:Rule Type="tt:CellMotionDetector" Name="MyMotionDetectorRule"><tt:Parameters><tt:SimpleItem Value="+QACwAAD/wAADP8AADD/ABHAAH8AAfwAP/AH/8A//wH//D/1/wDw" Name="ActiveCells"></tt:SimpleItem><tt:SimpleItem Value="1000" Name="AlarmOffDelay"></tt:SimpleItem><tt:SimpleItem Value="1000" Name="AlarmOnDelay"></tt:SimpleItem><tt:SimpleItem Value="4" Name="MinCount"></tt:SimpleItem></tt:Parameters></tt:Rule><tt:Rule Type="tt:TamperDetector" Name="MyTamperDetectorRule"><tt:Parameters><tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/></tt:Polygon></tt:PolygonConfiguration></tt:ElementItem></tt:Parameters></tt:Rule></tt:RuleEngineConfiguration></tt:VideoAnalyticsConfiguration><tt:PTZConfiguration token="000"><tt:Name>PTZ_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:NodeToken>000</tt:NodeToken><tt:DefaultRelativePanTiltTranslationSpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace</tt:DefaultRelativePanTiltTranslationSpace><tt:DefaultRelativeZoomTranslationSpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace</tt:DefaultRelativeZoomTranslationSpace><tt:DefaultContinuousPanTiltVelocitySpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace</tt:DefaultContinuousPanTiltVelocitySpace><tt:DefaultContinuousZoomVelocitySpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace</tt:DefaultContinuousZoomVelocitySpace><tt:DefaultPTZSpeed><tt:PanTilt space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace" y="1" x="1"></tt:PanTilt><tt:Zoom space="http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace" x="1"></tt:Zoom></tt:DefaultPTZSpeed><tt:DefaultPTZTimeout>PT1S</tt:DefaultPTZTimeout><tt:PanTiltLimits><tt:Range><tt:URI>http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace</tt:URI><tt:XRange><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:XRange><tt:YRange><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:YRange></tt:Range></tt:PanTiltLimits><tt:ZoomLimits><tt:Range><tt:URI>http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace</tt:URI><tt:XRange><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:XRange></tt:Range></tt:ZoomLimits></tt:PTZConfiguration></trt:Profiles><trt:Profiles fixed="true" token="001"><tt:Name>Profile_001</tt:Name><tt:VideoSourceConfiguration token="000"><tt:Name>VideoS_000</tt:Name><tt:UseCount>3</tt:UseCount><tt:SourceToken>000</tt:SourceToken><tt:Bounds height="1520" width="2592" y="0" x="0"></tt:Bounds></tt:VideoSourceConfiguration><tt:AudioSourceConfiguration token="000"><tt:Name>Audio_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:SourceToken>000</tt:SourceToken></tt:AudioSourceConfiguration><tt:VideoEncoderConfiguration token="001"><tt:Name>VideoE_001</tt:Name><tt:UseCount>1</tt:UseCount><tt:Encoding>H264</tt:Encoding><tt:Resolution><tt:Width>704</tt:Width><tt:Height>576</tt:Height></tt:Resolution><tt:Quality>4</tt:Quality><tt:RateControl><tt:FrameRateLimit>25</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>998</tt:BitrateLimit></tt:RateControl><tt:H264><tt:GovLength>2</tt:GovLength><tt:H264Profile>High</tt:H264Profile></tt:H264><tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>224.1.2.3</tt:IPv4Address></tt:Address><tt:Port>0</tt:Port><tt:TTL>0</tt:TTL><tt:AutoStart>false</tt:AutoStart></tt:Multicast><tt:SessionTimeout>PT10S</tt:SessionTimeout></tt:VideoEncoderConfiguration><tt:AudioEncoderConfiguration token="000"><tt:Name>AudioE_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:Encoding>G711</tt:Encoding><tt:Bitrate>64</tt:Bitrate><tt:SampleRate>8</tt:SampleRate><tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>224.1.2.3</tt:IPv4Address></tt:Address><tt:Port>0</tt:Port><tt:TTL>0</tt:TTL><tt:AutoStart>false</tt:AutoStart></tt:Multicast><tt:SessionTimeout>PT10S</tt:SessionTimeout></tt:AudioEncoderConfiguration><tt:VideoAnalyticsConfiguration token="000"><tt:Name>Analytics_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:AnalyticsEngineConfiguration><tt:AnalyticsModule Type="tt:CellMotionEngine" Name="MyCellMotionEngine"><tt:Parameters><tt:SimpleItem Value="50" Name="Sensitivity"></tt:SimpleItem><tt:ElementItem Name="Layout"><tt:CellLayout Columns="22" Rows="18"><tt:Transformation><tt:Translate x="-1.0" y="-1.0" /><tt:Scale x="0.09090" y="0.111111" /></tt:Transformation></tt:CellLayout></tt:ElementItem></tt:Parameters></tt:AnalyticsModule><tt:AnalyticsModule Type="tt:TamperEngine" Name="MyTamperEngine"><tt:Parameters><tt:SimpleItem Value="50" Name="Sensitivity"></tt:SimpleItem><tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/></tt:Polygon></tt:PolygonConfiguration></tt:ElementItem><tt:ElementItem Name="Transform"><tt:Transformation><tt:Translate x="-1.0" y="-1.0"/><tt:Scale x="0.001250" y="0.001667"/></tt:Transformation></tt:ElementItem></tt:Parameters></tt:AnalyticsModule></tt:AnalyticsEngineConfiguration><tt:RuleEngineConfiguration><tt:Rule Type="tt:CellMotionDetector" Name="MyMotionDetectorRule"><tt:Parameters><tt:SimpleItem Value="+QACwAAD/wAADP8AADD/ABHA1000" Name="ActiveCells"></tt:SimpleItem><tt:SimpleItem Value="1000" Name="AlarmOffDelay"></tt:SimpleItem><tt:SimpleItem Value="1000" Name="AlarmOnDelay"></tt:SimpleItem><tt:SimpleItem Value="4" Name="MinCount"></tt:SimpleItem></tt:Parameters></tt:Rule><tt:Rule Type="tt:TamperDetector" Name="MyTamperDetectorRule"><tt:Parameters><tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/><tt:Point x="0" y="0"/></tt:Polygon></tt:PolygonConfiguration></tt:ElementItem></tt:Parameters></tt:Rule></tt:RuleEngineConfiguration></tt:VideoAnalyticsConfiguration><tt:PTZConfiguration token="000"><tt:Name>PTZ_000</tt:Name><tt:UseCount>2</tt:UseCount><tt:NodeToken>000</tt:NodeToken><tt:DefaultRelativePanTiltTranslationSpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace</tt:DefaultRelativePanTiltTranslationSpace><tt:DefaultRelativeZoomTranslationSpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace</tt:DefaultRelativeZoomTranslationSpace><tt:DefaultContinuousPanTiltVelocitySpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace</tt:DefaultContinuousPanTiltVelocitySpace><tt:DefaultContinuousZoomVelocitySpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace</tt:DefaultContinuousZoomVelocitySpace><tt:DefaultPTZSpeed><tt:PanTilt space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace" y="1" x="1"></tt:PanTilt><tt:Zoom space="http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace" x="1"></tt:Zoom></tt:DefaultPTZSpeed><tt:DefaultPTZTimeout>PT1S</tt:DefaultPTZTimeout><tt:PanTiltLimits><tt:Range><tt:URI>http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace</tt:URI><tt:XRange><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:XRange><tt:YRange><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:YRange></tt:Range></tt:PanTiltLimits><tt:ZoomLimits><tt:Range><tt:URI>http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace</tt:URI><tt:XRange><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:XRange></tt:Range></tt:ZoomLimits></tt:PTZConfiguration></trt:Profiles></trt:GetProfilesResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>

Как видите, очень подробно и многословно — но не факт, что оно всё соответствует реальности, например, у этой конкретной камеры вообще нет PTZ, хотя оно упоминается.
Из всего этого нужно только Profile token — строка типа «000» (которая иногда может быть «Profile_1», или просто «0» — поэтому и приходится ее запрашивать)

Теперь запросим URL snapshot:

curl 192.168.1.10:8899/onvif/Media \
  -d '<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:trt="http://www.onvif.org/ver10/media/wsdl"
xmlns:tt="http://www.onvif.org/ver10/schema">
 <soap:Body>
 <trt:GetSnapshotUri>
 <trt:ProfileToken>000</trt:ProfileToken>
 </trt:GetSnapshotUri>
 </soap:Body>
</soap:Envelope> '

В ответ еще пара страниц XML‑текста, значимая часть которого сводится к строке

<tt:Uri>http://192.168.1.10/webcapture.jpg?command=snap&amp;channel=1&amp;user=admin&amp;password=uyyTCCjO </tt:Uri>

Причем даже с логином‑паролем (который на самом деле может быть вшит в ответ и не проверяться при запросе. А может и проверяться, как повезет).

Конкретно вот эта камера, на примере которой сейчас пишу, картинку дает, и даже логин‑пароль проверяет при этом, но заметьте — на ONVIF‑запрос никакого пароля не требовалось... Безопасность такая безопасность...

То есть теперь, теоретически, можно сделать запрос по указаному URL и получить текущую картинку. А можно и не получить, без обьяснения причин.

Казалось бы, дальше всё просто: делаем в веб‑интерфейсе страницу, например, кладовки, при переходе на которую будет показана картинка.
То самое «просто тыкнул пальцем и посмотрел»:

<html>
...
<img src="http://192.168.1.10/webcapture.jpg?command=snap&channel=1&user=admin&password=uyyTCCjO" style="width:200px">
...
</html>

«Не надо торопиться!» ©

Современные браузеры не хотят одновременно работать с https и http, а сам «сайт» умного дома пришлось сделать через https, чтобы запустить его в полноэкранном режиме, как PWA.
Поэтому пришлось добавить прокси‑запрос: браузер запрашивает картинку не у камеры, а у сервера, и сервер сам запрашивает ее у камеры.

<html>
...
<img src="/getpict?url=http://192.168.1.10/webcapture.jpg?command=snap&amp;channel=1&amp;user=admin&amp;password=uyyTCCjO" style="width:200px">
...
</html>

Для этого нужно только добавить метод, в Perl Mojolicious это делается так:

sub getpict {
  my $c = shift;

  my $url = $c->param('url');

  return $c->render(text => 'Missing or invalid URL', status => 400)
      unless $url && $url =~ m{^https?://};

  my $tx = $c->ua->get($url);

  return $c->render(text => 'Upstream error: ' . $tx->error->{message}, status => 502)
    if my $err = $tx->error and !$tx->res->code;

  my $res = $tx->result;

  $c->res->headers->content_type($res->headers->content_type);
  $c->res->code($res->code);

  $c->render(data => $res->body);

}

И вот теперь — всё.

Нажимаем иконку «кладовки» — открывается страничка с картинкой. Посмотрели — закрыли. Минимум усилий, и ходить лишний раз никуда не нужно.
Можно навесить кнопки включения света, например — но это уже совсем другая история.

Комментарии (10)


  1. MountainGoat
    13.10.2025 22:05

    У меня проблема на пункт раньше. Купил камеру по совету с хабра. Вот держу я камеру в руке. Подал питание. Как её заставить подключиться к моему вайфаю? Я был уверен, что если воткнуть её в ПК по USB, то там будет или сеть или диск с настройками - не, нифига.


    1. JBFW Автор
      13.10.2025 22:05

      Там обычно на бумажке написано что делать: типа "установите приложение на смартфон, подключитесь к сети камеры, покажите ей нарисовавшийся qr код, потом настраивайте"

      А проводом - это проводом


  1. Yumado
    13.10.2025 22:05

    Исходя из текста, и конечно свой опыт.

    Главное в камере не пиксели и другие важные технические характеристики, а прошивка.

    В "китайских" она базовая, под чип. Проблемы кстати известные, в таких прошивках не устранены, да зачем и так купят, цена соответствующая. Веб морда всего многообразия камер отличается только цветом.

    Лучше такие камеры, "китайские" не брать. Там и софт на регистраторах такой же, "базовый".

    Кривой, не возможно настроить запись по времени, по движению, потеря настроек, потеряет диск в указанный в настройках, не узнаешь, пока не зайдешь в архив. И т.п.

    Интерфейс жуть, UI там специально чтоб бесить.

    Вывод у меня лично такой, есть адаптированные камеры "импортозамещенные" с русскими прошивками, не русифицированными, а именно русскими. Не буду названий писать, найдете как искать надеюсь понятно. Там же ПО для регистраторов, вменяемое.

    Да, дороже чем "китай", платите за нормальную прошивку, казалось бы зачем. Железо одинаковое.

    Но потерянное время на настройки, не понятные зависания, потеря архива такого не стоит.


    1. JBFW Автор
      13.10.2025 22:05

      Про китайскую прошивку отдельно напишу, но в целом не все так страшно.

      Они есть совсем плохие, и есть вполне рабочие. Только не надо выставлять их в интернет без защиты!

      А так - зависания и потерь дисков за все годы ни разу не видел. Если что-то не работает - оно сразу не работает, если работает - будет работать, пока железо не накроется.

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


    1. akod67
      13.10.2025 22:05

      Брал на стройку пачку китайского дешмана. Дорогое туда ставить смысла нет, беспокоиться только, что сопрут. А сопрут дешман - ну и бог с ним. Реолинк кажется был. Софт да, страшной, DHCP, который без предупреждений переключается в статику доставил. Но жить можно. настраивается ведь один раз камера. Регистратор не покупал, собрал на iSpy Agent. Вполне себе решение. Камеры вот только ночью детализацию совсем минимальную имеют. Зато ледом хорошо территорию освещают, использую как уличное освещение :)


  1. NikaLapka
    13.10.2025 22:05

    Пользуясь случаем, может кто подскажет, аналогичный формат запроса для вкл/выкл камеры(приватного режима)?


    1. JBFW Автор
      13.10.2025 22:05

      Механическое реле на линию питания + блок управления этим реле, от выключателя до удаленного управления. Единственный надёжный способ.

      Остальное будет исключительно на доверии прошивке камеры.


  1. Newcss
    13.10.2025 22:05

    Есть проект frigate nvr, создавался изначально для HomeAssistant, со временем вырос в отдельный продукт. Для тех кому нужно быстро, качественно и просто. Получить можно не только скриншот но и прикрутить ИИ для анализа того что происходит в кадре. Еще неплохое решение ffmpeg + rtsp позволяет так же получать поток и делать скриншоты. Это на случай когда до начинки камеры не добраться или у нее отсутсвует штатный мнэанизм суриншотов.


    1. akod67
      13.10.2025 22:05

      Делаю тоже самое с помощью iSpy Agent - он получает стрим, пишет видео на диск (почти не пользуюсь), а так же закидывает скрины по FTP на домашний сервачок и вот уже скрины в юи я и показываю. Можно внешнии ИИ ещё подключить, но пока негде развернуть, да и надобности особой нет.


  1. vigfam
    13.10.2025 22:05

    Еще неплохое решение ffmpeg + rtsp

    Я просто оставлю это тут, вдруг кому понадобится - ffmpeg-ом из rtsp и в архивы, и в hls - чтобы смотреть почти в реалтайме через браузер, задержка ~10 сек:

    ffmpeg -re -y -hide_banner -loglevel quiet -nostats -fflags +genpts \ -rtsp_transport tcp -i rtsp://admin:admin@ВАШ_IP_потока:554/ВАШ_ЭНДПОИНТ -f segment \ -segment_time 900 -segment_format mp4 -an -vcodec copy \ -reset_timestamps 1 -strftime 1 "ВАШ_ПУТЬ_К_АРХИВАМ/cam4_%Y-%m-%d-%H.%M.%S.mp4" -c copy \ -f hls -hls_flags delete_segments -segment_list_size 2 \ -hls_time 2 -hls_list_size 2 \ -segment_list_flags +live -hls_segment_filename /tmp/hls/outf%03d.ts /tmp/hls/playlistf.m3u8 2>&1 >>/ПУТЬ_К_ЛОГУ/ffmpg.log &