Java
13 August 2014

Еще раз о видеонаблюдении, камерах, RTSP, onvif. И «велосипед»!

Информация уже была на хабре: habrahabr.ru/post/115808 и habrahabr.ru/post/117735
Там описывается Motion-JPEG (MJPEG).
Мир не стоит на месте и видео наблюдение тоже. Всё чаще и чаще используются другие кодеки.
Тут описываю свой опыт в этом «мире».
Профессионалы ничего нового не узнают, другим может будет просто интересно.
Разрабатывалось всё в качестве обучения и тренировки.
Речь пойдет о RTP, RTSP, h264, mjpeg, onvif и всём вместе.
Перед прочтением обязательно прочитать статьи другого автора, указанные выше.

Что такое RTSP можно прочитать:

Особенность RTSP в том, что он сам по себе не передаёт нужные нам видео данные. После установки связи вся работа осуществляется по протоколу RTP (RFC).

По RTP протоколу нужно различать 2 вида передачи
  1. Non-Interleaved Mode (UDP)
  2. Interleaved Mode (TCP)


Non-Interleaved Mode.
RTSP устанавливает связь и передает в камеру информацию о том «куда слать» данные (UDP порты).
Пример общения RTSP

//INFO: connect to: rtsp://10.112.28.231:554/live1.sdp

OPTIONS rtsp://10.112.28.231:554/live1.sdp RTSP/1.0
CSeq: 1
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)

RTSP/1.0 200 OK
CSeq: 1
Date: Tue, Jan 15 2013 02:02:56 GMT
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER


DESCRIBE rtsp://10.112.28.231:554/live1.sdp RTSP/1.0
CSeq: 2
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Accept: application/sdp


RTSP/1.0 200 OK
CSeq: 2
Date: Tue, Jan 15 2013 02:02:56 GMT
Content-Base: rtsp://10.112.28.231/live1.sdp/
Content-Type: application/sdp
Content-Length: 667
//667 - Размер SDP пакета, о нем позже


SETUP rtsp://10.112.28.231:554/live1.sdp/track1 RTSP/1.0
CSeq: 3
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Transport: RTP/AVP;unicast;client_port=49501-49502


RTSP/1.0 200 OK
CSeq: 3
Date: Tue, Jan 15 2013 02:02:56 GMT
Transport: RTP/AVP;unicast;destination=10.112.28.33;source=10.112.28.231;client_port=49501-49502;server_port=6970-6971
Session: 7BFE9DAA


SETUP rtsp://10.112.28.231:554/live1.sdp/track2 RTSP/1.0
CSeq: 4
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Transport: RTP/AVP;unicast;client_port=49503-49504
Session: 7BFE9DAA


RTSP/1.0 200 OK
CSeq: 4
Date: Tue, Jan 15 2013 02:02:56 GMT
Transport: RTP/AVP;unicast;destination=10.112.28.33;source=10.112.28.231;client_port=49503-49504;server_port=6972-6973
Session: 7BFE9DAA


PLAY rtsp://10.112.28.231:554/live1.sdp RTSP/1.0
CSeq: 5
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Session: 7BFE9DAA
Range: npt=0.000-


RTSP/1.0 200 OK
CSeq: 5
Date: Tue, Jan 15 2013 02:02:56 GMT
Range: npt=0.000-
Session: 7BFE9DAA
RTP-Info: url=rtsp://10.112.28.231/live1.sdp/track1;seq=7746;rtptime=0,url=rtsp://10.112.28.231/live1.sdp/track2;seq=13715;rtptime=0


Запоминаем
Transport: RTP/AVP;unicast;destination=10.112.28.33;source=10.112.28.231;client_port=49501-49502;server_port=6970-6971

Interleaved Mode.
Разница с Non-Interleaved Mode в том что все пакеты будут сыпаться в этот же порт.
Пример:

OPTIONS rtsp://10.113.151.152:554/tcp_live/profile_token_0 RTSP/1.0
CSeq: 1
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)


RTSP/1.0 200 OK
CSeq: 1
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN, SET_PARAMETER


DESCRIBE rtsp://10.113.151.152:554/tcp_live/profile_token_0 RTSP/1.0
CSeq: 2
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Accept: application/sdp


RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: 316


SETUP rtsp://10.113.151.152:554/tcp_live/profile_token_0/video/h264 RTSP/1.0
CSeq: 3
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Transport: RTP/AVP/TCP;unicast;interleaved=0-1


RTSP/1.0 200 OK
CSeq: 3
Session: 52cd95de
Transport: RTP/AVP/TCP;interleaved=0-1;unicast


SETUP rtsp://10.113.151.152:554/tcp_live/profile_token_0/audio/pcma RTSP/1.0
CSeq: 4
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Transport: RTP/AVP/TCP;unicast;interleaved=2-3
Session: 52cd95de


RTSP/1.0 200 OK
CSeq: 4
Session: 52cd95de
Transport: RTP/AVP/TCP;interleaved=2-3;unicast


PLAY rtsp://10.113.151.152:554/tcp_live/profile_token_0 RTSP/1.0
CSeq: 5
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Session: 52cd95de
Range: npt=0.000-


RTSP/1.0 200 OK
CSeq: 5
Session: 52cd95de


Запоминаем
Transport: RTP/AVP/TCP;unicast;interleaved=0-1

Теперь смотрим что и как.
Камеры шлют видео и аудио в разные RTP потоки. 2n поток — данные, 2n+1 поток — RTCP.
На видео нам идет 0 и 1 канал, на аудио 2 и 3 канал.
Теперь смотрим
Transport: RTP/AVP;unicast;destination=10.112.28.33;source=10.112.28.231;client_port=49501-49502;server_port=6970-6971
Transport: RTP/AVP/TCP;unicast;interleaved=0-1

В первом случае указаны порты, во втором каналы.

С с Non-Interleaved Mode всё понятно. Просто RTP пакеты сыпятся в порты и их можно читать как то так:
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
s.receive(packet);


Проблемы начинаются с Interleaved mode.
По факту ни каких проблем быть не должно. По RFC мы ищем magic char "$", следующий байт — канал (он указывается в подключении 0-4 у нас) и 2 байта Length. Всего 4 байта.
Но есть не нормальные камеры. Например D-ling DCS-2103 «Досыпает» какие то данные после rtp пакета. frame дает размер 1448,
шлет 1448 фрейма, и после 827 байт какого то мусора. (Так делает Dlink DCS-2103 прошивка 1.00 и 1.20)

И такое у «них» происходит постоянно. Этим частенько страдают китайские камеры. Qihan (356) этим не страдали.
Кроме как пропускать этот мусор идей больше нет.
В RTP сыпятся полезные данные. При DESCRIBE RTSP возвращается SDP пакет
Примеры SDP (h264, mjpeg, mpeg4):
v=0
o=- 1357245962093293 1 IN IP4 10.112.28.231
s=RTSP/RTP stream 1 from DCS-2103
i=live1.sdp with v2.0
t=0 0
a=type:broadcast
a=control:*
a=range:npt=0-
a=x-qt-text-nam:RTSP/RTP stream 1 from DCS-2103
a=x-qt-text-inf:live1.sdp
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:1500
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=640028;sprop-parameter-sets=Z2QAKK2EBUViuKxUdCAqKxXFYqOhAVFYrisVHQgKisVxWKjoQFRWK4rFR0ICorFcVio6ECSFITk8nyfk/k/J8nm5s00IEkKQnJ5Pk/J/J+T5PNzZprQCgDLSpAAAAwHgAAAu4YEAAPQkAABEqjve+F4RCNQ=,aO48sA==
a=control:track1
m=audio 0 RTP/AVP 97
c=IN IP4 0.0.0.0
b=AS:64
a=rtpmap:97 G726-32/8000
a=control:track2

v=0
o=- 1357245962095633 1 IN IP4 10.112.28.231
s=RTSP/RTP stream 3 from DCS-2103
i=live3.sdp with v2.0
t=0 0
a=type:broadcast
a=control:*
a=range:npt=0-
a=x-qt-text-nam:RTSP/RTP stream 3 from DCS-2103
a=x-qt-text-inf:live3.sdp
m=video 0 RTP/AVP 26
c=IN IP4 0.0.0.0
b=AS:1500
a=x-dimensions:640,360
a=control:track1
m=audio 0 RTP/AVP 97
c=IN IP4 0.0.0.0
b=AS:64
a=rtpmap:97 G726-32/8000
a=control:track2

v=0
o=- 1357245962094966 1 IN IP4 10.112.28.231
s=RTSP/RTP stream 2 from DCS-2103
i=live2.sdp with v2.0
t=0 0
a=type:broadcast
a=control:*
a=range:npt=0-
a=x-qt-text-nam:RTSP/RTP stream 2 from DCS-2103
a=x-qt-text-inf:live2.sdp
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:1500
a=rtpmap:96 MP4V-ES/90000
a=fmtp:96 profile-level-id=1;config=000001B001000001B509000001010000012000845D4C29402320A21F
a=control:track1
m=audio 0 RTP/AVP 97
c=IN IP4 0.0.0.0
b=AS:64
a=rtpmap:97 G726-32/8000
a=control:track2


Прочитать про SDP
Так как мода была mjpeg и текущая на h264, то рассмотрим их.
С MJpeg всё предельно ясно. А вот с H264 начинаются различия в камерах.
Формат h264 состоит из блоков с NAL заголовками (7.4.1 NAL unit semantics).
Чтобы можно было декодировать h264 необходимо помимо данных самого h264 иметь данные SPS (Sequence parameter set) и PPS(Picture parameter set). Первый описывает последовательность, второй параметры картинки. Так как сам кодек h264 знаю очень плохо, то большего описания не будет. SPS имеет тип 7, PPS 8. Без них невозможно декодировать h264.
Самое интересное — Qihan шлет SPS и PPS прям в RTP пакетах, Dlink не шлет их в RTP пакетах. Но SPS и PPS шлется в SDP пакете в параметре sprop-parameter-sets в кодировке base64.
sprop-parameter-sets=Z2QAKK2EBUViuKxUdCAqKxXFYqOhAVFYrisVHQgKisVxWKjoQFRWK4rFR0ICorFcVio6ECSFITk8nyfk/k/J8nm5s00IEkKQnJ5Pk/J/J+T5PNzZprQCgDLSpAAAAwHgAAAu4YEAAPQkAABEqjve+F4RCNQ=,aO48sA==
Шлются они через запятую
Вариант декодирования.
//split по ','
sps = Base64.decode(props[0].getBytes());
pps = Base64.decode(props[1].getBytes());


Так как камеры 720p или 1080p, то в 1 RTP пакет ни jpeg фрейм, ни h264 фрейм не поместится, то они режутся на пакеты.
RTP Payload Format for JPEG-compressed Video
RTP Payload Format for H.264 Video

JPEG
RTP пакет содержит main JPEG header
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   | Type-specific |              Fragment Offset                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |      Type     |       Q       |     Width     |     Height    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

а дальше может варьироваться от Type и Q
if(getType() < 64){
            return JPEG_HEADER_SIZE;
        } else if(getType() < 128){
            //we have 3.1.7.  Restart Marker header
            return JPEG_HEADER_SIZE + JPEG_RESTART_MARKER_HEADER_SIZE;
        }

Для декодирования jpeg нужно знать или вычислить quantization tables.
В моих камерах quantization tables шли в стартовом пакете Jpeg, по этому они просто брались оттуда.
Все вычисления есть в RFC.
Последний пакет фрейма вычисляется по RTP header Marker bit. Если он 1, то это последний пакет фрейма.

H264
NAL Header
      +---------------+
      |0|1|2|3|4|5|6|7|
      +-+-+-+-+-+-+-+-+
      |F|NRI|  Type   |
      +---------------+


Single NAL Unit Packet
Это как раз SPS и PPS. Type=7 или Type=8
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |F|NRI|  Type   |                                               |
    +-+-+-+-+-+-+-+-+                                               |
    |                                                               |
    |               Bytes 2..n of a single NAL unit                 |
    |                                                               |
    |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                               :...OPTIONAL RTP padding        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


Если фрейм h264 не влезает в RTP пакет (1448 байт), то фрейм режется на фрагменты. (5.8. Fragmentation Units (FUs))
Type = 28
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | FU indicator  |   FU header   |                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
    |                                                               |
    |                         FU payload                            |
    |                                                               |
    |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                               :...OPTIONAL RTP padding        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Эти заголовки следуют сразу после RTP заголовка
public int getH264PayloadStart() {
        switch(getNAL().getType()){
            case NAL.FU_A:
                return rtp.getPayloadStart() + 2;
            case NAL.SPS:
            case NAL.PPS:
                return rtp.getPayloadStart();
            default:
                throw new NotImplementedException("NAL type " + getNAL().getType() + " not implemented");
        }
    }


Для декодера h264 NAL — нужная информация. Если идет фрагментация фрейма, то NAL нужно восстанавливать. (FU)
нужно взять первые 3 бита из FU indicator и слить их с 5 последними FU header.

Теперь самое главное — сохраняем поток.
Jpeg
public void writeRawJPEGtoStream(OutputStream out) throws IOException {
        //if(isMustBeZero()){
        if(isStart()){
            //first
            //System.out.println("first");
            byte[] headers = new byte[1024];
            int length = makeJpeg(headers);
            out.write(headers, 0, length);
            out.write(rtp.getBuffer(), getJPEGPayloadStart(), getJPEGPayloadLength());
        }else
        //if(getMarker()){
        if(isEnd()){
            //end
            //System.out.println("end");
            out.write(rtp.getBuffer(), getJPEGPayloadStart(), getJPEGPayloadLength());
            //EOI
        } else {
          //middle
            //System.out.println("middle");
            out.write(rtp.getBuffer(), getJPEGPayloadStart(), getJPEGPayloadLength());
        }
    }

h264
public static final byte[] NON_IDR_PICTURE = {0x00, 0x00, 0x00, 0x01};

public void writeRawH264toStream(OutputStream out) throws IOException, NotImplementedException {
        switch (nal.getType()){
            case NAL.FU_A:    //FU-A, 5.8.  Fragmentation Units (FUs)/rfc6184
                FUHeader fu = getFUHeader();

                if(fu.isFirst()){
                    //if(debug) System.out.println("first");
                    out.write(H264RTP.NON_IDR_PICTURE);
                    out.write(getReconstructedNal());
                    out.write(rtp.getBuffer(), getH264PayloadStart(), getH264PayloadLength());
                } else if(fu.isEnd()){
                    //if(debug) System.out.println("end");
                    out.write(rtp.getBuffer(), getH264PayloadStart(), getH264PayloadLength());
                } else{
                    //if(debug) System.out.println("middle");
                    out.write(rtp.getBuffer(), getH264PayloadStart(), getH264PayloadLength());
                }
                break;
            case NAL.SPS: //Sequence parameter set
            case NAL.PPS: //Picture parameter set
                //System.out.println("sps or pps write");
                out.write(H264RTP.NON_IDR_PICTURE);
                out.write(rtp.getBuffer(), rtp.getPayloadStart(), rtp.getPayloadLength());
                break;
            default:
                throw new NotImplementedException("NAL type " + getNAL().getType() + " not implemented");
        }
    }

NON_IDR_PICTURE — необходим для декодирования, «разделяем» фреймы. (h264)
Тут нужно меня поправить, так как это просто «костыль» и обоснований пока нет. Просто работает.
Получается такой поток: 00000001 + SPS + 00000001 + PPS + 00000001 + NAL…
erlyvideo: 0,0,0,1 — это префикс AnnexB записи H264. Это не часть H264 NAL-юнита, а разделитель между юнитами.

ну и обработка «всего» этого
while(!stop){
                IRaw raw = rtp;
                //читаем фрейм
                try {
                    while(!frame.fill(in));

                    //полюбому читаем rtp пакет
                    rtp.fill(in, frame.getLength());
                    try {
                        raw = rtp.getByPayload();
                    } catch (NotImplementedException e) {
                        if(log.isLoggable(Level.FINE)) log.fine("rtp seq=" + rtp.getSequence() + ": " + e.getMessage());
                    }
                } catch (SocketException e) {
                    log.warning(e.getMessage()); //socket closed?
                    break;
                }

                byte ch = frame.getChannel();

                //RTCP? //прошивка D-link DCS2103 1.00 слала RTCP и interleaved
                Source s = sources.get(source(ch));
                if(rtp.getPayloadType() == RTPWrapper.TYPE_RTCP){
                    byte[] rb = new byte[frame.getLength()];
                    System.arraycopy(buffer, 0, rb, 0, rb.length);
                    s.lastRTCP = new RTCP(rb, rb.length);    //save last rtsp
                    s.lastRTCPTime = System.currentTimeMillis();
                    System.out.println(frame.getLength());
                } else {
                    s.calculate(rtp); //вычисление для source параметров (для нужд RTCP)
                }

                if(os.length <= ch){
                    log.warning("Нужно больше out стримов: " + ch);
                    continue;
                }

                profiler.stop();
                counter.count(profiler.getLast(), frame.getLength() / 1000.0);
                //profiler.print(0);
                if(os[ch] == null) continue;

                //Нужна была синхронизация, так как os[ch] менялся, сейчас он постоянно rotator
                synchronized (os[ch]){
                    raw.writeRawToStream(os[ch]);
                }
            }

в 2х словах. Получаем RTSP Interleaved Frame (например Channel: 0x00, 1448 bytes), читаем 1448 байт, делаем writeRawToStream, полиморфизм делает свое дело.

Дальше это нужно обкатать.
Казалось бы что для поддержания потока RTSP нужно делать RTCP отчеты, но нет, всё оказалось проще
Dlink, Qihan, VLC просто «едят» GET_PARAMETER:
GET_PARAMETER rtsp://10.112.28.231:554/live3.sdp RTSP/1.0
CSeq: 7
User-Agent: LibVLC/2.1.4 (LIVE555 Streaming Media v2014.01.21)
Session: 327B23C6

шлем его раз в 55 секунд и всё.

Теперь сам велосипед
Просто программа в которую можно добавить ссылку на камеру (http или rtsp) и она будет сохранять поток. База SQLite. «Нормализация» потока через ffmpeg, просмотр через Vlc.
Нет переподключения после каких либо разрывов связи, файловых проблем и т.д.
Нет половины проверок и подобных штук.
Как выглядит
Кнопки
  1. Добавить
  2. Удалить
  3. Запустить
  4. Остановить
  5. Архив
  6. Настройка
  7. Выход

1

Настройки :)
2

Архив
  1. Посмотреть — запускает Vlc
  2. Склеить и посмотреть — клеит файлы и запускает Vlc
  3. Выход

3

При простом просмотре генерируется m3u файл и кормится в VLC
4

При склеивании ffmpeg клеит, после запускается VLC
5

Программа нарезает поток на файлы, интервал задается в настройках

Что делает ffmpeg:
Клеит
String command = String.format("%s -y -f concat -i concat.txt -codec copy concat.mp4",

«Нормализует» (просчитывает заголовки и т.д.)
String command = String.format("%s -i %s -codec copy %s",
                    settings.getFfmpegPath(),
                    settings.getFullTmpPath() + archive,
                    settings.getArchivePath() + "/" + settings.getRecPath() + "/" + archive + ".mp4")


На выходе куча файлов
6

По хорошему можно писать в любой OutputStream
Git hub
Дальнейшей жизни программы может и не быть. Возможно допишу когда нибудь RTP классы для звука. (так как увлекаюсь до сих пор SIP)

Ну и самое вкусное.
Есть стандарт видео наблюдения ONVIF
Есть профессиональные железки, которые с камерами работают только по нему.
Есть камеры, которые работают по нему (Qihan, он же Proline), а ссылки rtsp приходится гуглить.
Есть опенсорсный продукт Onvif device manager для управления подобными железяками.
Я же в программу добавил поддержку onvif без авторизации и с авторизацией.
7
Git hub

В 2х словах об Onvif: Это soap.
Работа простая. 1. Шлем POST-XML, 2. Получаем XML
Код на гитхабе. Ключ -s сохраняет все запросы и ответы XML.
пример запроса:
<ns3:Envelope xmlns:xmime="http://www.w3.org/2005/05/xmlmime"
xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:ns4="http://www.onvif.org/ver10/device/wsdl" 
xmlns:ns3="http://www.w3.org/2003/05/soap-envelope" 
xmlns:ns6="http://www.w3.org/2005/08/addressing" 
xmlns:ns5="http://www.onvif.org/ver10/schema" 
xmlns:ns8="http://docs.oasis-open.org/wsrf/bf-2" 
xmlns:ns7="http://docs.oasis-open.org/wsn/b-2" 
xmlns:ns13="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 
xmlns:ns9="http://docs.oasis-open.org/wsn/t-1" 
xmlns:ns12="http://www.onvif.org/ver10/media/wsdl" 
xmlns:ns11="http://www.w3.org/2004/08/xop/include" 
xmlns:ns14="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<ns3:Body><ns4:GetCapabilities>
<ns4:Category>All</ns4:Category>
</ns4:GetCapabilities>
</ns3:Body>
</ns3:Envelope>

Если пройтись по ссылкам выше, то можно получить всю документацию по Onvif.
Ответ:
<?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:wsa5="http://www.w3.org/2005/08/addressing" 
xmlns:xmime5="http://www.w3.org/2005/05/xmlmime" 
xmlns:xop="http://www.w3.org/2004/08/xop/include" 
xmlns:tt="http://www.onvif.org/ver10/schema" 
xmlns:tds="http://www.onvif.org/ver10/device/wsdl" 
xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" 
xmlns:tev="http://www.onvif.org/ver10/events/wsdl" 
xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" 
xmlns:trt="http://www.onvif.org/ver10/media/wsdl" 
xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" 
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" 
xmlns:ter="http://www.onvif.org/ver10/error" xmlns:tns1="http://www.onvif.org/ver10/topics" 
xmlns:wstop="http://docs.oasis-open.org/wsn/t-1">
<SOAP-ENV:Body>
<tds:GetCapabilitiesResponse>
<tds:Capabilities><tt:Device><tt:XAddr>http://10.112.28.231:80/onvif/device_service</tt:XAddr>
<tt:Network><tt:IPFilter>false</tt:IPFilter>
<tt:ZeroConfiguration>false</tt:ZeroConfiguration><tt:IPVersion6>false</tt:IPVersion6>
<tt:DynDNS>false</tt:DynDNS></tt:Network><tt:System><tt:DiscoveryResolve>false</tt:DiscoveryResolve>
<tt:DiscoveryBye>true</tt:DiscoveryBye><tt:RemoteDiscovery>false</tt:RemoteDiscovery>
<tt:SystemBackup>false</tt:SystemBackup><tt:SystemLogging>false</tt:SystemLogging>
<tt:FirmwareUpgrade>true</tt:FirmwareUpgrade><tt:SupportedVersions>
<tt:Major>1</tt:Major><tt:Minor>2</tt:Minor></tt:SupportedVersions>
<tt:Extension></tt:Extension></tt:System><tt:IO></tt:IO><tt:Security>
<tt:TLS1.1>true</tt:TLS1.1><tt:TLS1.2>false</tt:TLS1.2>
<tt:OnboardKeyGeneration>false</tt:OnboardKeyGeneration>
<tt:AccessPolicyConfig>false</tt:AccessPolicyConfig>
<tt:X.509Token>false</tt:X.509Token><tt:SAMLToken>false</tt:SAMLToken>
<tt:KerberosToken>false</tt:KerberosToken><tt:RELToken>false</tt:RELToken>
</tt:Security></tt:Device><tt:Events><tt:XAddr>http://10.112.28.231:80/onvif/device_service</tt:XAddr>
<tt:WSSubscriptionPolicySupport>false</tt:WSSubscriptionPolicySupport>
<tt:WSPullPointSupport>true</tt:WSPullPointSupport>
<tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>
</tt:Events><tt:Imaging><tt:XAddr>http://10.112.28.231:80/onvif/device_service</tt:XAddr>
</tt:Imaging><tt:Media><tt:XAddr>http://10.112.28.231:80/onvif/device_service</tt:XAddr>
<tt:StreamingCapabilities><tt:RTPMulticast>false</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities></tt:Media>
</tds:Capabilities></tds:GetCapabilitiesResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>

Дальнейшее общение по onvif без авторизации идет в этом же ключе.

А вот пример общения но уже с авторизацией
<ns3:Envelope xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:ns4="http://www.onvif.org/ver10/device/wsdl" xmlns:ns3="http://www.w3.org/2003/05/soap-envelope" 
xmlns:ns6="http://www.w3.org/2005/08/addressing" xmlns:ns5="http://www.onvif.org/ver10/schema" 
xmlns:ns8="http://docs.oasis-open.org/wsrf/bf-2" xmlns:ns7="http://docs.oasis-open.org/wsn/b-2" xmlns:ns13="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 
xmlns:ns9="http://docs.oasis-open.org/wsn/t-1" xmlns:ns12="http://www.onvif.org/ver10/media/wsdl" xmlns:ns11="http://www.w3.org/2004/08/xop/include" xmlns:ns14="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<ns3:Header>
	<Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ns3:mustUnderstand="1">
		<UsernameToken>
			<Username>admin</Username>
			<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">KSsJz8Lx0xPJd4pYdMuFblluNac=</Password>
			<Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">Y2FsY09udmlm</Nonce>
			<Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-01-15T08:00:57.000Z</Created>
		</UsernameToken>
	</Security>
</ns3:Header>
<ns3:Body><ns12:GetProfiles/></ns3:Body></ns3:Envelope>

Т.е. нужно слать заголовок. (тестилось на D-link DCS-2103, остальные камеры без авторизации работали, китай).

Timestamp (Created)
public static String getOnvifTimeStamp(DateTime dateTime){
        return String.format("%4d-%02d-%02dT%02d:%02d:%02d.000Z",
                dateTime.getDate().getYear(),
                dateTime.getDate().getMonth(),
                dateTime.getDate().getDay(),
                dateTime.getTime().getHour(),
                dateTime.getTime().getMinute(),
                dateTime.getTime().getSecond()
        );
    }

Nonce
public String getNonceDigest(){
        return base64(getNonce().getBytes());
    }

и пароль (Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) ))
public String getPasswordDigest(){
        //Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) )
        String line = getNonce() + timestamp + password;
        try {
            line = base64(sha1(line.getBytes()));
            return line;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }


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

PS Не надо писать в комментариях про организацию на большую букву «I». Их Server использует SQLite, SSL, avcodec (ffmpeg), а в папке \Resources есть божественный файлик с названием camera_list.json, но моя наглость не позволила его прикрутить к своей программе :) Но я не видел у них поддержку Onvif, видимо потому что они выпускают «свои» камеры. UPDATED: см комментарии от ivideon

Если прикрутить к программе OpenVPN и OpenCV, то будет забавное решение и «велосипед»
Ну и вот вам полезная ссылка на базу ссылок потоков камер

Git hub:

+17
146k 232
Comments 11
Top of the day