Еще раз о видеонаблюдении, камерах, RTSP, onvif. И «велосипед»!
Информация уже была на хабре: habrahabr.ru/post/115808/ и habrahabr.ru/post/117735/Там описывается Motion-JPEG (MJPEG).Мир не стоит на месте и видео наблюдение тоже. Всё чаще и чаще используются другие кодеки.Тут описываю свой опыт в этом «мире».Профессионалы ничего нового не узнают, другим может будет просто интересно.Разрабатывалось всё в качестве обучения и тренировки.Речь пойдет о RTP, RTSP, h264, mjpeg, onvif и всём вместе.Перед прочтением обязательно прочитать статьи другого автора, указанные выше.Что такое RTSP можно прочитать: Особенность RTSP в том, что он сам по себе не передаёт нужные нам видео данные. После установки связи вся работа осуществляется по протоколу RTP (RFC).По RTP протоколу нужно различать 2 вида передачи
Non-Interleaved Mode (UDP) 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 VideoRTP Payload Format for H.264 Video
JPEGRTP пакет содержит 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, то это последний пакет фрейма.H264NAL 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 = {0×00, 0×00, 0×00, 0×01};
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…ну и обработка «всего» этого
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: 0×00, 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
Архив
Посмотреть — запускает Vlc Склеить и посмотреть — клеит файлы и запускает Vlc Выход 3При простом просмотре генерируется m3u файл и кормится в VLC4
При склеивании ffmpeg клеит, после запускается VLC5
Программа нарезает поток на файлы, интервал задается в настройках
Что делает 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
По хорошему можно писать в любой OutputStreamGit hubДальнейшей жизни программы может и не быть. Возможно допишу когда нибудь RTP классы для звука. (так как увлекаюсь до сих пор SIP)
Ну и самое вкусное.Есть стандарт видео наблюдения ONVIFЕсть профессиональные железки, которые с камерами работают только по нему.Есть камеры, которые работают по нему (Qihan, он же Proline), а ссылки rtsp приходится гуглить.Есть опенсорсный продукт Onvif device manager для управления подобными железяками.Я же в программу добавил поддержку onvif без авторизации и с авторизацией.7Git hub
В 2х словах об Onvif: Это soap.Работа простая. 1. Шлем POST-XML, 2. Получаем XMLКод на гитхабе. Ключ -s сохраняет все запросы и ответы XML.пример запроса:
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, видимо потому что они выпускают «свои» камеры.
Если прикрутить к программе OpenVPN и OpenCV, то будет забавное решение и «велосипед«Ну и вот вам полезная ссылка на базу ссылок потоков камер
Git hub: