Назад в блог

ONVIF: как три компании договорились о языке для камер — и почему он до сих пор говорит на SOAP

История, устройство и реальная работа ONVIF: от соглашения 2008 года до Profile T и Profile V. WS-Discovery, профили, WS-Security, GetStreamUri — с примерами запросов и разбором того, где камеры ломают стандарт.

Введение

В статье про RTSP я описал протокол, который доставляет видео от камеры к клиенту. У RTSP есть ясная задача и ясные границы: у вас уже есть адрес вида rtsp://…, а дальше — DESCRIBE, SETUP, PLAY и поток RTP.

Но этот адрес откуда-то нужно взять. Как найти камеру в сети? Какие у неё есть потоки и чем главный отличается от дополнительного? Как узнать, какой путь и порт у нужного потока — у каждого производителя он свой? Как пройти авторизацию, не угадывая схему? RTSP на эти вопросы не отвечает: он начинается там, где они уже закрыты.

Закрывает их ONVIF. В RTSP-статье я назвал его надстройкой, которая берёт на себя поиск камер и авторизацию. Это верно, но неполно: ONVIF — это целый набор веб-сервисов. Через него можно искать устройства, читать и менять настройки, управлять поворотной оптикой (PTZ), получать события (движение, саботаж, срабатывание тревожных входов), работать с записью на самой камере. Видео он при этом не передаёт — для этого ONVIF отдаёт ссылку на тот самый RTSP.

Аналогия. Если RTSP — это телефонная линия, по которой можно говорить, то ONVIF — телефонный справочник, регистратура и правила вежливого обращения: он подскажет, кого набрать, как представиться и какой внутренний номер соответствует нужному отделу. Сам разговор — уже не его дело.

По данным ONVIF на начало 2026 года, в мире насчитывается более 35 000 изделий, прошедших подтверждение соответствия, и около 500 компаний-участников. Это не гарантия того, что любая камера заработает с любым клиентом с первой попытки — ниже будет много о том, где «стандарт» расходится с реальностью, — но это объясняет, почему без ONVIF сегодня почти невозможно построить систему, которая работает не с одной маркой камер.

Как читать примеры. Все запросы и ответы в статье составлены по официальным схемам ONVIF (WSDL/XSD, версия 26.06) и текстам спецификаций. Запросы и ответы Media и Media2 дополнительно проверены на соответствие этим схемам автоматически (ответы Device составлены по описанию типов, но отдельно не валидировались). Но это не трассы с конкретной камеры: реальные устройства отвечают с вариациями — другие префиксы пространств имён, другой порядок вспомогательных полей, лишние расширения. Места, которые я не проверял на живом железе, помечены словом «проверить». Полный список — в конце.

История: зачем понадобился общий язык

Мир до ONVIF

Это общеизвестная картина отрасли, а не цитата из одного источника, поэтому сформулирую осторожно. До появления общего стандарта почти у каждого производителя IP-камер были собственный HTTP API и собственный SDK. Система видеонаблюдения, которая хотела работать с камерами нескольких марок, писала отдельный драйвер под каждую. Стоимость интеграции росла вместе с числом производителей, а у производителя камер не было способа попасть в чужую систему, кроме как договариваться об интеграции вручную.

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

2008 год: три компании

ONVIF основали в 2008 году Axis Communications, Bosch Security Systems и Sony. Организация зарегистрирована 25 ноября 2008 года как некоммерческая корпорация в штате Делавэр (501(c)(6)). Первоначально название расшифровывалось как Open Network Video Interface Forum; когда область интересов вышла за пределы видео (например, в контроль доступа), длинное название отбросили, и осталась одна аббревиатура.

Важно, что стандарт с самого начала задумывался как интерфейс, а не как формат видео: договориться о том, как клиент находит устройство, спрашивает его возможности и просит поток. Кодеки, форматы и протоколы передачи при этом использовались уже существующие — RTP/RTSP, H.264 и так далее.

Был ли конкурент

Примерно в то же время — по данным открытых источников, в августе 2008 года — появилась ещё одна инициатива, PSIA (Physical Security Interoperability Alliance), с похожей целью и другим техническим подходом. Источники здесь в основном форумные и отраслевые блоги, поэтому ограничусь осторожным выводом: поддержку PSIA оказали отдельные производители, но массового распространения она, судя по всему, не получила, и сегодня именно ONVIF стал общепринятым языком.

От бумаги к работающему стандарту

Спецификация без проверки остаётся бумагой. Поэтому важнее самой первой версии оказалась последовательность шагов, которая сделала стандарт проверяемым:

ДатаСобытие
Ноябрь 2008Core Specification 1.0
Декабрь 2008Test Specification 1.0
Май 2009Тестовый инструмент и процесс подтверждения соответствия
Июль 2009Первые устройства, подтвердившие соответствие
Октябрь 2009100 участников
Январь 2011около 600 подтверждённых изделий
2018 / 202110 000 / 20 000 изделий
Начало 2026более 35 000 изделий

Ключевое слово здесь — conformance: устройство не просто заявляет «поддерживаю ONVIF», а проходит проверку тестовым инструментом, и результат публикуется. Актуальный и самый надёжный источник для проверки конкретной модели — официальный список conformant products. Подтверждение соответствия, впрочем, не отменяет проверки на собственном стенде: тест проверяет заявленные функции, а не все сочетания настроек реальной прошивки.

Профили: как стандарт научился говорить «обещаю»

Core Specification описывает очень много возможностей, и ни одна камера не реализует их все. Если бы стандарт требовал «поддержи всё», его не поддерживал бы никто. Если бы он требовал «поддержи что хочешь», клиент не мог бы ничего предполагать. Решением стали профили (Profiles): именованные наборы обязательных функций. Профиль — это обещание: «устройство с этим профилем умеет вот это».

ПрофильПоявилсяО чём обещание
Profile Sянварь 2012Потоковое видео: профили медиа, получение RTSP-ссылки, базовое управление
Profile Cмарт 2014Контроль доступа (двери, замки)
Profile Gиюнь 2014Запись и воспроизведение на самом устройстве
Profile Qдекабрь 2014Быстрая установка: обнаружение и первичная настройка
Profile Aиюль 2017Конфигурация контроля доступа
Profile Tоктябрь 2018Современное потоковое видео: Media2, H.265, Digest, события, метаданные
Profile Mиюнь 2021Метаданные и события аналитики
Profile Dиюль 2021Периферия контроля доступа
Profile Vиюль 2026 (Release Candidate)Облачное видео — см. последний раздел

Для видеонаблюдения на практике важны три: S (многолетний фундамент), T (его преемник) и G (запись на карту памяти камеры). Остальные профили касаются другой части мира: дверей, замков, считывателей.

Отдельно стоит запомнить: у профиля бывают две стороны — устройство и клиент. «Камера соответствует Profile T» и «VMS соответствует Profile T» — два разных обещания, которые проверяются отдельно. Первое не означает второго.

Почему именно SOAP

ONVIF построен на SOAP — XML поверх HTTP, с описанием интерфейса в WSDL. Сегодня это выглядит архаично: многословные конверты, пространства имён, клиенты, которые приходится писать на коленке. Однако для 2008 года выбор выглядит не случайным. Мотивы авторов стандарта нигде не сформулированы одной фразой, поэтому то, что ниже, — моя реконструкция, а не цитата.

Тогда в корпоративном мире сложился целый набор спецификаций WS-* (Web Services), и ONVIF почти ничего не придумывал заново — он собрал готовые блоки:

ЗадачаЧто взятоГде проявляется
Поиск устройств в сетиWS-Discovery (UDP multicast)Probe / ProbeMatch / Hello / Bye
Описание интерфейсаWSDL + XSDМашинно-читаемый контракт каждого сервиса
АвторизацияWS-Security, UsernameTokenЗаголовок wsse:Security в запросе
СобытияWS-BaseNotification + WS-TopicsПодписки, PullPoint, темы вида tns1:…

Главное достоинство такого выбора — типизированный контракт. WSDL и XSD описывают каждую операцию, каждое поле, допустимые значения. По ним можно сгенерировать клиентский код (так делают некоторые библиотеки) и автоматически проверить запрос — именно так проверены примеры в этой статье.

Платить за это приходится объёмом, сложностью и тем, что реализации «слегка расходятся» в деталях XML. Кроме того, спецификация живёт десять лет и остаётся обратно совместимой — иногда в ущерб аккуратности. Показательный пример: в схеме до сих пор есть значение MPV4-ES, а в комментарии прямо сказано, что на самом деле имелось в виду MP4V-ES, но ошибку в имени оставили ради обратной совместимости.

Устройство ONVIF

Сервисы

Камера с ONVIF — это не один «API», а набор сервисов, у каждого из которых свой адрес (XAddr) и своё пространство имён:

СервисЧто даётЗачем нашему узлу
DeviceИдентификация, время, пользователи, сеть, список остальных сервисовТочка входа и проверка часов
Media (ver10)Профили, кодеки, GetStreamUri в «старом» стилеОсновной путь для камер Profile S
Media2 (ver20)То же в новой редакции, с H.265Основной путь для камер Profile T
EventsПодписки и события: движение, саботаж, входыДешёвый триггер вместо анализа картинки
PTZПоворот, наклон, масштаб, пресетыТолько если нужна поворотная камера
ImagingЭкспозиция, фокус, ИК-подсветкаОбычно не нужен
AnalyticsПравила и детекторы на стороне камерыИсточник метаданных (Profile M)
Recording / Search / ReplayЗапись на самой камереРезервный источник при потере связи (Profile G)

Пространства имён главных сервисов (по схемам ONVIF):

ПрефиксПространство имён
tdshttp://www.onvif.org/ver10/device/wsdl
trthttp://www.onvif.org/ver10/media/wsdl
tr2http://www.onvif.org/ver20/media/wsdl
tthttp://www.onvif.org/ver10/schema

Адреса сервисов не фиксированы. Руководство для разработчиков (ONVIF Application Programmer’s Guide) прямо предупреждает: они зависят от производителя и устройства, поэтому их нужно получать динамически. Популярный путь /onvif/device_service встречается в ProbeMatch у многих камер, но это соглашение, а не требование.

ONVIF services and profiles map

Media и Media2: два поколения одного сервиса

У нас получилось два сервиса медиа не потому, что кому-то не хватило фантазии. Первый, Media (ver10), появился вместе с Profile S. Второй, Media2 (ver20), пришёл вместе с Profile T. Различия, которые видны в схемах:

  • H.265. В перечислении VideoEncoding, на которое опирается Media, — только JPEG, MPEG4 и H264. H.265 появляется лишь в Media2 (там кодек передаётся как медиа-подтип IANA: H264, H265).
  • Запрос ссылки. В Media клиент описывает поток структурой StreamSetup (тип потока и транспорт). В Media2 достаточно строки Protocol.
  • Безопасный транспорт. Среди возможностей Media2 — RTSPS/SRTP и RTSP поверх WebSocket (RTSPWebSocketUri). В текущей схеме Media (ver10) таких возможностей нет.

Для клиента вывод простой: сначала проверяем, есть ли у камеры Media2, и если есть — работаем через него; иначе откатываемся на Media.

Что обещает Profile T

Здесь приведено только то, что найдено в тексте Profile T v1.0, без дополнений «по памяти»:

  • Устройство обязано аутентифицировать HTTP- и RTSP-запросы по Digest.
  • Устройство обязано поддерживать GetServices, GetSystemDateAndTime, SetSystemDateAndTime, GetProfiles, GetStreamUri (через Media2) и SetSynchronizationPoint (запрос внеочередного ключевого кадра).
  • Хотя бы один из кодеков H.264 или H.265 — обязателен; какой именно — на усмотрение производителя.
  • Обязательны потоки по RTP/UDP, RTP/RTSP/HTTP/TCP и RTP/UDP multicast. HTTPS и WebSocket — условные, то есть не для всех.
  • Обязательны потоковая передача метаданных, OSD, события движения (Motion Alarm) и хотя бы один тип событий саботажа (Tampering).
  • Устройство должно допускать не менее двух подписок типа PullPoint (MaxPullPoints).
  • Клиент Profile T обязан определять сервисы через GetServices, а не через устаревший GetCapabilities.

Здесь есть неожиданный нюанс: HTTPS в Profile T необязателен. Если вы рассчитываете на шифрование канала, придётся проверять это отдельно (в 2024 году ONVIF выпустил TLS Configuration Add-on, но это отдельный документ, а не часть Profile T).

Как клиент находит камеру и просит поток

Самая полезная часть для практики — последовательность. Она почти не меняется от камеры к камере, и если реализовать её аккуратно, остальное — вопрос деталей.

ONVIF connection sequence

Шаг 1. Поиск: WS-Discovery

Клиент отправляет Probe — SOAP-сообщение по UDP на multicast-адрес 239.255.255.250, порт 3702 (для IPv6 — ff02::c). Любое устройство, которое слушает и подходит под запрос, отвечает ProbeMatch прямо клиенту.

<e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope"
            xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing"
            xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"
            xmlns:dn="http://www.onvif.org/ver10/network/wsdl">
  <e:Header>
    <w:MessageID>uuid:84ede3de-7dec-11d0-c360-f01234567890</w:MessageID>
    <w:To e:mustUnderstand="true">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>
    <w:Action e:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>
  </e:Header>
  <e:Body>
    <d:Probe>
      <d:Types>dn:NetworkVideoTransmitter</d:Types>
    </d:Probe>
  </e:Body>
</e:Envelope>

Устройство отвечает так:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope"
                   xmlns:wsadis="http://schemas.xmlsoap.org/ws/2004/08/addressing"
                   xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"
                   xmlns:dn="http://www.onvif.org/ver10/network/wsdl"
                   xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
  <SOAP-ENV:Header>
    <wsadis:MessageID>uuid:1b2c3d4e-0000-4000-8000-0123456789ab</wsadis:MessageID>
    <wsadis:RelatesTo>uuid:84ede3de-7dec-11d0-c360-f01234567890</wsadis:RelatesTo>
    <wsadis:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsadis:To>
    <wsadis:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches</wsadis:Action>
  </SOAP-ENV:Header>
  <SOAP-ENV:Body>
    <d:ProbeMatches>
      <d:ProbeMatch>
        <wsadis:EndpointReference>
          <wsadis:Address>urn:uuid:0f1e2d3c-0000-4000-8000-ba9876543210</wsadis:Address>
        </wsadis:EndpointReference>
        <d:Types>dn:NetworkVideoTransmitter tds:Device</d:Types>
        <d:Scopes>onvif://www.onvif.org/Profile/Streaming
                  onvif://www.onvif.org/Profile/T
                  onvif://www.onvif.org/name/Demo-Camera
                  onvif://www.onvif.org/hardware/DC-100</d:Scopes>
        <d:XAddrs>http://192.168.1.64/onvif/device_service</d:XAddrs>
        <d:MetadataVersion>1</d:MetadataVersion>
      </d:ProbeMatch>
    </d:ProbeMatches>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Что здесь важно:

  • XAddrs — адрес Device-сервиса. Элемент может содержать несколько адресов через пробел; клиент должен перебрать их и сохранить тот, который реально отвечает.
  • Scopes — метки вида onvif://www.onvif.org/…: имя, аппаратная модель, расположение и, что нам нужно, поддерживаемые профили. Устройство, соответствующее Profile T, обязано включить в список scope соответствующую запись. В примере выше — Profile/T и Profile/Streaming (эта запись встречается в примере из Core Specification; сопоставлять её именно с Profile S стоит только после проверки по тексту профиля).
  • Types — по Core Specification устройство обязано включить в Types тип tds:Device. Тип dn:NetworkVideoTransmitter часто используют клиенты; в Core Specification 19.06 он не упоминается. По логике WS-Discovery Probe без Types подходит всем устройствам, поэтому для широкого поиска можно отправить именно такой — или указать оба типа.

По умолчанию устройство находится в режиме discoverable, но Core Specification допускает и режим non-discoverable, в котором камера не слушает WS-Discovery вообще. Поэтому «камера не нашлась» не означает «в камере нет ONVIF».

Ещё одна деталь, объясняющая многие загадочные «молчания»: по Core Specification при ошибке в multicast-пакете устройство должно молча отбросить его, а не отвечать ошибкой — чтобы много камер не завалили сеть ответами. Поэтому нестандартно оформленный Probe не получает ничего: ни отказа, ни подсказки. В сообществе встречаются жалобы, что часть камер не терпит нестандартных префиксов пространств имён; проверить это стоит на своих моделях.

Шаг 2. Часы: GetSystemDateAndTime

Первый вызов после обнаружения — не GetProfiles, а GetSystemDateAndTime. У него две роли: он проверяет, что адрес из XAddrs вообще достижим, и сообщает время камеры. Согласно Core Specification, это вызов класса PRE_AUTH: пароль не нужен.

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
  <s:Body>
    <tds:GetSystemDateAndTime/>
  </s:Body>
</s:Envelope>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tds="http://www.onvif.org/ver10/device/wsdl"
            xmlns:tt="http://www.onvif.org/ver10/schema">
  <s:Body>
    <tds:GetSystemDateAndTimeResponse>
      <tds:SystemDateAndTime>
        <tt:DateTimeType>NTP</tt:DateTimeType>
        <tt:DaylightSavings>false</tt:DaylightSavings>
        <tt:TimeZone>
          <tt:TZ>CST-8</tt:TZ>
        </tt:TimeZone>
        <tt:UTCDateTime>
          <tt:Time><tt:Hour>8</tt:Hour><tt:Minute>15</tt:Minute><tt:Second>27</tt:Second></tt:Time>
          <tt:Date><tt:Year>2026</tt:Year><tt:Month>10</tt:Month><tt:Day>4</tt:Day></tt:Date>
        </tt:UTCDateTime>
        <tt:LocalDateTime>
          <tt:Time><tt:Hour>16</tt:Hour><tt:Minute>15</tt:Minute><tt:Second>27</tt:Second></tt:Time>
          <tt:Date><tt:Year>2026</tt:Year><tt:Month>10</tt:Month><tt:Day>4</tt:Day></tt:Date>
        </tt:LocalDateTime>
      </tds:SystemDateAndTime>
    </tds:GetSystemDateAndTimeResponse>
  </s:Body>
</s:Envelope>

Зачем это нужно, объясняется ниже, в разделе про авторизацию. Пока — главное: это тот самый вызов, которым стоит начинать общение с любой камерой, особенно с незнакомой.

Шаг 3. Сервисы: GetServices

Теперь можно спросить, что камера умеет и по каким адресам. GetServices тоже относится к PRE_AUTH. Есть и устаревший GetCapabilities с той же функцией; в WSDL он помечен как заменённый на GetServices, а Profile T требует от клиента именно GetServices.

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
  <s:Body>
    <tds:GetServices>
      <tds:IncludeCapability>false</tds:IncludeCapability>
    </tds:GetServices>
  </s:Body>
</s:Envelope>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tds="http://www.onvif.org/ver10/device/wsdl"
            xmlns:tt="http://www.onvif.org/ver10/schema">
  <s:Body>
    <tds:GetServicesResponse>
      <tds:Service>
        <tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>
        <tds:XAddr>http://192.168.1.64/onvif/device_service</tds:XAddr>
        <tds:Version><tt:Major>2</tt:Major><tt:Minor>60</tt:Minor></tds:Version>
      </tds:Service>
      <tds:Service>
        <tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
        <tds:XAddr>http://192.168.1.64/onvif/media_service</tds:XAddr>
        <tds:Version><tt:Major>2</tt:Major><tt:Minor>60</tt:Minor></tds:Version>
      </tds:Service>
      <tds:Service>
        <tds:Namespace>http://www.onvif.org/ver20/media/wsdl</tds:Namespace>
        <tds:XAddr>http://192.168.1.64/onvif/media2_service</tds:XAddr>
        <tds:Version><tt:Major>2</tt:Major><tt:Minor>60</tt:Minor></tds:Version>
      </tds:Service>
    </tds:GetServicesResponse>
  </s:Body>
</s:Envelope>

По Namespace можно понять, какие сервисы есть. Наличие http://www.onvif.org/ver20/media/wsdl означает Media2.

Шаг 4. Профили медиа: GetProfiles

Media Profile — это набор конфигураций, привязанных друг к другу: источник видео, видеокодер, при необходимости аудио, PTZ, метаданные. Камера обычно содержит несколько готовых профилей: для главного потока, для дополнительного.

Начиная с этого шага нужна авторизация (подробнее — ниже). Заголовок с подписью (формат подробнее разобран в разделе про авторизацию) добавляется в s:Header:

<s:Header>
  <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                 xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <wsse:UsernameToken>
      <wsse:Username>admin</wsse:Username>
      <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">O7QloJhZpZwZB0wE/cDlHXB6L3g=</wsse:Password>
      <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">LKqI6G/AikKCQrN0zqZFlg==</wsse:Nonce>
      <wsu:Created>2026-10-04T08:15:30Z</wsu:Created>
    </wsse:UsernameToken>
  </wsse:Security>
</s:Header>

А сам запрос выглядит так:

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tr2="http://www.onvif.org/ver20/media/wsdl">
  <s:Body>
    <tr2:GetProfiles>
      <tr2:Type>All</tr2:Type>
    </tr2:GetProfiles>
  </s:Body>
</s:Envelope>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tr2="http://www.onvif.org/ver20/media/wsdl"
            xmlns:tt="http://www.onvif.org/ver10/schema">
  <s:Body>
    <tr2:GetProfilesResponse>
      <tr2:Profiles token="profile_main" fixed="true">
        <tr2:Name>mainStream</tr2:Name>
        <tr2:Configurations>
          <tr2:VideoSource token="vsc_1">
            <tt:Name>VideoSourceConfig</tt:Name>
            <tt:UseCount>2</tt:UseCount>
            <tt:SourceToken>vs_1</tt:SourceToken>
            <tt:Bounds x="0" y="0" width="2560" height="1440"/>
          </tr2:VideoSource>
          <tr2:VideoEncoder token="venc_main" GovLength="50" Profile="Main">
            <tt:Name>VideoEncoder_Main</tt:Name>
            <tt:UseCount>1</tt:UseCount>
            <tt:Encoding>H265</tt:Encoding>
            <tt:Resolution><tt:Width>2560</tt:Width><tt:Height>1440</tt:Height></tt:Resolution>
            <tt:RateControl ConstantBitRate="false">
              <tt:FrameRateLimit>25</tt:FrameRateLimit>
              <tt:BitrateLimit>4096</tt:BitrateLimit>
            </tt:RateControl>
            <tt:Quality>5</tt:Quality>
          </tr2:VideoEncoder>
        </tr2:Configurations>
      </tr2:Profiles>
      <tr2:Profiles token="profile_sub" fixed="true">
        <tr2:Name>subStream</tr2:Name>
        <tr2:Configurations>
          <tr2:VideoSource token="vsc_1">
            <tt:Name>VideoSourceConfig</tt:Name>
            <tt:UseCount>2</tt:UseCount>
            <tt:SourceToken>vs_1</tt:SourceToken>
            <tt:Bounds x="0" y="0" width="2560" height="1440"/>
          </tr2:VideoSource>
          <tr2:VideoEncoder token="venc_sub" GovLength="25" Profile="Main">
            <tt:Name>VideoEncoder_Sub</tt:Name>
            <tt:UseCount>1</tt:UseCount>
            <tt:Encoding>H264</tt:Encoding>
            <tt:Resolution><tt:Width>640</tt:Width><tt:Height>360</tt:Height></tt:Resolution>
            <tt:RateControl ConstantBitRate="false">
              <tt:FrameRateLimit>10</tt:FrameRateLimit>
              <tt:BitrateLimit>512</tt:BitrateLimit>
            </tt:RateControl>
            <tt:Quality>3</tt:Quality>
          </tr2:VideoEncoder>
        </tr2:Configurations>
      </tr2:Profiles>
    </tr2:GetProfilesResponse>
  </s:Body>
</s:Envelope>

Что стоит заметить:

  • Название профиля (mainStream, subStream) — произвольная метка производителя. Выбирать поток нужно не по имени, а по параметрам кодера: Encoding, Resolution, FrameRateLimit, BitrateLimit. Токен (token) — это идентификатор, который передаётся дальше.
  • Атрибут GovLength — длина группы кадров. По описанию в схеме он определяет, как часто в потоке встречаются опорные (I) кадры. Для нас это важно практически: именно от него зависит, сколько ждать первой картинки после подключения. В Profile T для этого есть SetSynchronizationPoint, но проверять реальную задержку нужно на конкретных камерах — проверить.
  • fixed="true" значит, что профиль нельзя удалить.

Шаг 5. Адрес потока: GetStreamUri

Наконец, ради чего всё затевалось. Запрос в Media2:

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tr2="http://www.onvif.org/ver20/media/wsdl">
  <s:Body>
    <tr2:GetStreamUri>
      <tr2:Protocol>RTSP</tr2:Protocol>
      <tr2:ProfileToken>profile_main</tr2:ProfileToken>
    </tr2:GetStreamUri>
  </s:Body>
</s:Envelope>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tr2="http://www.onvif.org/ver20/media/wsdl">
  <s:Body>
    <tr2:GetStreamUriResponse>
      <tr2:Uri>rtsp://192.168.1.64:554/stream1</tr2:Uri>
    </tr2:GetStreamUriResponse>
  </s:Body>
</s:Envelope>

Значение RTSP в Protocol означает RTP поверх TCP (то есть чередующиеся interleaved-кадры внутри RTSP-соединения), а не «просто RTSP». Остальные значения Media2: RtspUnicast (RTP/UDP), RtspMulticast, RtspsUnicast, RtspsMulticast (SRTP) и RtspOverHttp (туннелирование).

В Media (ver10) тот же запрос выглядит иначе:

<s:Envelope xmlns:s="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">
  <s:Body>
    <trt:GetStreamUri>
      <trt:StreamSetup>
        <tt:Stream>RTP-Unicast</tt:Stream>
        <tt:Transport>
          <tt:Protocol>RTSP</tt:Protocol>
        </tt:Transport>
      </trt:StreamSetup>
      <trt:ProfileToken>profile_main</trt:ProfileToken>
    </trt:GetStreamUri>
  </s:Body>
</s:Envelope>
<s:Envelope xmlns:s="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">
  <s:Body>
    <trt:GetStreamUriResponse>
      <trt:MediaUri>
        <tt:Uri>rtsp://192.168.1.64:554/stream1</tt:Uri>
        <tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
        <tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
        <tt:Timeout>PT0S</tt:Timeout>
      </trt:MediaUri>
    </trt:GetStreamUriResponse>
  </s:Body>
</s:Envelope>

Две подробности, которые легко пропустить:

  • В ответе Media стоят поля InvalidAfterConnect=false, InvalidAfterReboot=false и Timeout=PT0S: по спецификации ссылка действует бессрочно, даже если профиль изменился. Её можно сохранить и не запрашивать при каждом старте.
  • В описании операции в WSDL Media значение типа потока написано как RTP_unicast, тогда как перечисление в XSD допускает RTP-Unicast. Это расхождение внутри самих документов; в запросе стоит использовать значение из XSD — именно по нему и проверяются схемы.

Дальше начинается территория статьи про RTSP: DESCRIBE, SETUP, PLAY, Digest уже на уровне RTSP. ONVIF свою часть сделал — вернул адрес.

Авторизация: самая болезненная часть

Два способа, один пользователь

По Core Specification все сервисы ONVIF должны быть защищены HTTP Digest (RFC 2617), с двумя исключениями: «унаследованные» устройства, которые поддерживают WS-UsernameToken, и TLS-авторизация клиента. Если устройство поддерживает оба способа, клиент может использовать любой: на уровне HTTP (Digest) или на уровне SOAP (заголовок wsse:Security). Пользователи общие.

СпособГде живётЧто нужно знать
HTTP DigestЗаголовки HTTPОбязателен для Profile T; Core Specification ссылается на RFC 2617, где базовый алгоритм — MD5
WS-UsernameTokenSOAP-заголовокОбязателен для устройств Profile S; требует точного времени и nonce
TLSТранспортРекомендуется Core Specification; в Profile T условный

Профиль S по его тексту (v1.1.1) требует от устройства WS-UsernameToken, а HTTP Digest для устройства — необязательная функция, при этом клиент обязан уметь и то, и другое. Сведения о тексте Profile S взяты из вторичной копии документа, поэтому формулировку стоит перепроверить на onvif.org. Практический вывод для клиента: поддерживать оба способа, иначе часть старых камер окажется недоступной.

Сама спецификация достаточно прямолинейно оценивает защиту: и Digest, и UsernameToken дают лишь «rudimentary level of security» (элементарный уровень), а там, где безопасность важна, рекомендуется настраивать доступ по TLS. Мой вывод для проектирования: аутентификация камеры защищает от ошибок и случайных подключений, но не от злоумышленника, уже находящегося в одной сети с камерой. Поэтому камеры лучше держать в отдельном VLAN — это отдельный разговор, и он связан с архитектурой Edge-узла.

Как отличить один способ от другого по ответу

Core Specification описывает поведение, по которому можно определить, какую авторизацию ждёт устройство:

  • Если устройство поддерживает Digest и клиент не прислал учётных данных, оно вернёт HTTP 401 — клиент понимает, что нужен Digest.
  • Если устройство поддерживает только UsernameToken, ответ другой: HTTP 400 и SOAP-ошибка ter:NotAuthorized с кодом s:Sender.
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:ter="http://www.onvif.org/ver10/error">
  <s:Body>
    <s:Fault>
      <s:Code>
        <s:Value>s:Sender</s:Value>
        <s:Subcode>
          <s:Value>ter:NotAuthorized</s:Value>
        </s:Subcode>
      </s:Code>
      <s:Reason>
        <s:Text xml:lang="en">Sender not authorized</s:Text>
      </s:Reason>
    </s:Fault>
  </s:Body>
</s:Envelope>

Клиент не должен отправлять одновременно HTTP-Digest и WS-UsernameToken: устройство сначала проверит HTTP-уровень, а затем — SOAP-уровень. Если вы смешаете оба, ошибка в любом из них приведёт к отказу.

Подпись UsernameToken: формула и время

Пароль в SOAP не передаётся: вместо него идёт дайджест:

PasswordDigest = Base64( SHA1( Nonce + Created + Password ) )

Здесь Nonce — случайные байты, которые клиент генерирует заново для каждого запроса (в XML они кодируются Base64, а в хеш входят сырые байты), Created — время запроса в UTC, Password — пароль. Core Specification требует, чтобы клиент использовал и nonce, и метку времени; сервер обязан отклонить токен, где нет любого из них.

Из-за Created возникает главная практическая проблема. Камера сравнивает время в запросе со своим, и если расхождение больше допуска, запрос отклоняется. Допуск стандартом не задан; в ответах разработчиков встречается значение около 5 секунд у Axis, а руководство для разработчиков ONVIF в примерах использует ту же «пятисекундную» оговорку. Часы камеры, которая не ходила к NTP-серверу, быстро уезжают.

Руководство предлагает аккуратное решение, которое меняет клиент, а не камеру:

  1. Вызвать GetSystemDateAndTime (без авторизации).
  2. Вычислить разницу между временем камеры и своим.
  3. Хранить эту разницу как поправку для этой камеры и подставлять её в Created.

Не нужно менять часы камеры только для того, чтобы с ней можно было общаться. Минимальный пример на Python (стандартная библиотека, без сторонних пакетов):

import base64, hashlib, os, re
from datetime import datetime, timedelta, timezone
from urllib.request import Request, urlopen

SOAP = "http://www.w3.org/2003/05/soap-envelope"

def envelope(body, header=""):
    return (f'<s:Envelope xmlns:s="{SOAP}"><s:Header>{header}</s:Header>'
            f'<s:Body>{body}</s:Body></s:Envelope>')

def device_time_offset(xaddr):
    """GetSystemDateAndTime needs no authentication: ask the camera what time it is."""
    body = '<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>'
    req = Request(xaddr, envelope(body).encode(),
                  {"Content-Type": "application/soap+xml; charset=utf-8"})
    xml = urlopen(req, timeout=5).read().decode()
    utc = re.search(r"<(?:\w+:)?UTCDateTime>(.*?)</(?:\w+:)?UTCDateTime>", xml, re.S).group(1)
    g = lambda tag: int(re.search(rf"<(?:\w+:)?{tag}>(\d+)<", utc).group(1))
    cam = datetime(g("Year"), g("Month"), g("Day"), g("Hour"), g("Minute"), g("Second"),
                   tzinfo=timezone.utc)
    return cam - datetime.now(timezone.utc)          # remember it; do NOT touch the camera clock

def username_token(user, password, offset=timedelta(0)):
    nonce = os.urandom(16)
    created = (datetime.now(timezone.utc) + offset).strftime("%Y-%m-%dT%H:%M:%SZ")
    digest = base64.b64encode(
        hashlib.sha1(nonce + created.encode() + password.encode()).digest()).decode()
    return (
        '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" '
        'xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">'
        f"<wsse:UsernameToken><wsse:Username>{user}</wsse:Username>"
        '<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">'
        f"{digest}</wsse:Password>"
        '<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">'
        f"{base64.b64encode(nonce).decode()}</wsse:Nonce>"
        f"<wsu:Created>{created}</wsu:Created></wsse:UsernameToken></wsse:Security>"
    )

Код проверен на локальном «макете камеры», который сверяет дайджест и отклоняет запрос при расхождении часов более 5 секунд: без поправки при разнице в 17 минут приходит HTTP 400, с поправкой запрос проходит. Формула дайджеста дополнительно сверена с независимой реализацией (библиотека zeep). Это проверка логики, а не гарантия работы с любой реальной камерой.

Камеры на практике: где стандарт заканчивается

Ниже — типичные проблемы. Здесь оставлены только те проблемы, для которых есть источник; остальные честно помечены как требующие проверки.

Проблема 1: часы камеры

Камера без NTP и с разъехавшимися часами не принимает UsernameToken, при этом отвечая невнятной ошибкой авторизации. В инструкции Digital Watchdog для камер Hikvision сказано, что расхождение более 10 минут может нарушить авторизацию.

Рекомендация. Всегда начинайте с GetSystemDateAndTime, храните поправку и логируйте её. Если поправка велика — это сигнал оператору «у камеры нет NTP», а не повод молча переписывать её часы.

Проблема 2: ONVIF по умолчанию выключен

По той же инструкции, в прошивках Hikvision начиная с версии 5.5.0 ONVIF по умолчанию отключён, а режим аутентификации называется «Digest & ws-username token». Включать его приходится вручную, и это нужно сделать до того, как вы начнёте считать камеру «неподдерживаемой».

Та же инструкция предупреждает о блокировке: многократные неверные попытки входа могут заблокировать камеру на 30 минут («Illegal Login Lock»).

Рекомендация. Различайте «не нашлась» (нет ответа на Probe), «ONVIF выключен» (порт не отвечает или нет сервиса) и «не авторизовалась» (401/400). После двух-трёх неудач — пауза и сообщение оператору, а не бесконечный цикл повторов: он может заблокировать камеру на полчаса.

Проблема 3: две разные ошибки авторизации

Как показано выше, 401 означает «нужен Digest», а 400 с NotAuthorized — «ждали UsernameToken». Не все клиенты это различают, а в логах это выглядит как просто «ошибка».

Рекомендация. Разбирайте ответ, а не только код: определяйте способ авторизации по первому ответу и запоминайте его для этой камеры. Используйте один способ за раз.

Проблема 4: адрес в ответе не тот

Согласно описанию GetServices в WSDL, схема и IP в XAddr должны совпадать с теми, по которым устройству отправили запрос. Это означает, что камера, которая возвращает внутренний адрес вместо того, по которому к ней обратились (например, из-за NAT или проброса портов), нарушает спецификацию. Для GetStreamUri такого прямого требования в изученных документах нет: там сказано лишь, что адрес бессрочный и не должен быть длиннее 128 октетов. Насколько часто встречается «неправильный IP» в ссылке на поток — проверить.

Рекомендация. Сравнивайте хост из ответа с тем, к которому вы подключались. Если они различаются — запишите это в лог. Подменять хост (оставляя путь и порт) стоит только как отдельное, явное, проверенное на конкретной камере решение.

Проблема 5: «главный» и «дополнительный» поток

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

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

Проблема 6: камера молчит на Probe

Причины, подтверждённые документами: устройство может быть в режиме non-discoverable; нестандартный Probe молча отбрасывается. Ещё одна, о которой пишут практики, но которая здесь не проверялась: multicast-обнаружение не проходит между подсетями и VLAN без специальной настройки — проверить на своей сети.

Рекомендация. Всегда оставляйте ручной режим: если оператор указал IP камеры, пробуйте GetSystemDateAndTime напрямую по http://<ip>/onvif/device_service, а при неудаче — по адресам из документации производителя. Это соглашение, а не гарантия.

Проблема 7: лимиты

Один подтверждённый лимит есть в Profile T: устройство обязано допускать минимум две подписки PullPoint. Больше — на усмотрение производителя, поэтому рассчитывать на много одновременных читателей событий нельзя. Для Media2 устройство сообщает MaximumNumberOfProfiles. Сколько одновременных RTSP-подключений выдержит конкретная камера, стандарт не определяет — проверить на своих моделях.

Рекомендация. Одно подключение к каждому потоку, а дальше — ретрансляция внутри узла. Так камера не зависит от числа потребителей. Это согласуется с архитектурой Edge-узла, где камеру читает один компонент (CameraManager).

Куда движется ONVIF

Конец Profile S

В октябре 2025 года ONVIF объявил, что завершает поддержку Profile S и рекомендует Profile T. Причина — устаревшая модель авторизации UsernameToken. По данным отраслевых блогов, новые подтверждения соответствия по Profile S принимаются до 31 марта 2027 года; точные сроки лучше проверить на onvif.org. Установленные камеры продолжат работать как работали, а значит, Profile S останется в парке на годы.

Для клиента это решает вопрос приоритета: Profile T — основа для новых устройств, Profile S — запасной вариант для старых.

Profile V: камера звонит домой

В июле 2026 года ONVIF объявил Release Candidate Profile V — профиля для облачного видео, который должен быть окончательно утверждён к концу 2026 года. Основная идея: устройство само устанавливает защищённое исходящее соединение (Uplink) с облачной системой, и ничего пробрасывать через NAT или поднимать VPN обычно не нужно. По данным анонса, живое видео и аудио идут по WebRTC, события отправляются в облако, а зашифрованные записи — в облачное хранилище; для авторизации требуется OAuth 2.0.

Подробности (версия спецификации Uplink, WebSocket Secure, mutual TLS) найдены только в блоге вендора и считаются вторичным источником. Сама спецификация сейчас доступна только участникам ONVIF для рецензии, поэтому всё, что сказано выше, — направление, а не факт. Но направление знакомое: архитектура Edge-узла тоже строится на исходящем соединении без входящих портов, и если Profile V утвердят в рассматриваемом виде, они окажутся близки по идее.

Что это значит для Edge-узла

Если свести всё сказанное к решениям для проекта:

  1. Profile T первым, Profile S запасным. Сначала GetServices; есть Media2 — работаем через него; нет — через Media.
  2. Начинать с GetSystemDateAndTime. Поправка времени хранится вместе с остальными данными камеры.
  3. Поддерживать оба способа авторизации и запоминать, какой нужен камере.
  4. Не хардкодить адреса сервисов. Сохранять XAddr из GetServices, а GetStreamUri вызывать один раз и сохранять ссылку.
  5. События движения и саботажа (в Profile T они обязательны) — дешёвый источник сигналов без анализа видео.
  6. Профиль V — не опора, а направление; слой, работающий с камерой, стоит отделить от остального так, чтобы замена не требовала переписывания.

Что я пока не проверял на живых камерах

Эта статья опирается на спецификации, схемы и документацию производителей, но не на измерения. Вот что остаётся открытым:

  • Как часто камеры возвращают неверный IP в GetStreamUri и GetServices.
  • Различия главного и дополнительного потоков и реальная задержка первого кадра при разных GovLength.
  • Работает ли WS-Discovery между VLAN и что именно для этого нужно настроить.
  • Допуск по времени у конкретных моделей: 5 секунд — лишь одно из встречающихся значений.
  • Сколько одновременных RTSP-соединений выдерживают конкретные камеры.
  • Какие именно префиксы и форматы XML принимают капризные реализации.
  • Актуальная формулировка Profile S (изучена вторичная копия) и точные сроки его вывода.
  • Всё, что касается Profile V: профиль ещё не утверждён.

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

Ключевые выводы

  1. ONVIF отвечает на вопросы, с которых начинается RTSP: где камера, что она умеет, какой адрес у потока и как авторизоваться. Видео он не передаёт.
  2. Стандарт возник из договорённости трёх компаний в 2008 году и стал рабочим, потому что рядом сразу появились тесты и процесс подтверждения соответствия.
  3. Профили — это обещания. S — прошлое, T — настоящее, V — возможное будущее. Обещание камеры и обещание клиента проверяются раздельно.
  4. Последовательность неизменна: Probe → GetSystemDateAndTime → GetServices → GetProfiles → GetStreamUri → RTSP.
  5. Время решает всё. Для UsernameToken нужны синхронные часы; самое аккуратное решение — не менять камеру, а хранить поправку на клиенте.
  6. Авторизация — самая хрупкая часть: два способа, два вида ошибок, блокировки после неудачных попыток. Различайте их и не повторяйте вслепую.
  7. Не доверяйте адресам из ответов и именам профилей — проверяйте и сравнивайте с тем, что вы знаете о камере.
  8. Всё, что не проверено на железе, должно быть помечено. Стандарт описывает идеальную камеру, и реальные устройства расходятся с ним в деталях.

Источники