Захват видео с сетевых камер, часть 2

  • Tutorial

В первой своей статье «измерение расстояния до объекта и его скорости» я рассмотрел захват изображений с веб-камер через Video4Linux2 и через DirectX. В следующей статье «захват видео с сетевых камер, часть 1» я рассмотрел как работать с сетевыми Motion-JPEG камерами. Сейчас я поведаю Вам о захвате изображений с сетевых RTSP камер, в частности поток Motion-JPEG по RTSP.

Задача эта более сложная нежели Motion-JPEG по HTTP, так как необходимо больше действий, больше подключений, но взамен мы получаем большую гибкость, скорость, функциональность и даже некую универсальность. Честно говоря, RTSP для простых задач избыточен, но я не сомневаюсь, что найдутся ситуации, где он будет необходим.

Что такое RTSP


RTSP расшифровывается как Real Time Streaming Protocol — потоковый протокол реального времени — по сути это протокол управления вещанием, он позволяет выполнять несколько команд, такие как «старт», «стоп», «переход на определённое время». Протокол этот подобен HTTP в реализации, тоже есть заголовки, тоже всё передаётся в текстовом виде. Вот основные его команды из спецификации:
  • OPTIONS — возвращает список поддерживаемых методов (OPTIONS, DESCRIBE и т.д.);
  • DESCRIBE — запрос описания контента, описывает каждый трек в формате SDP;
  • SETUP — запрос установки соединений и транспорта для потоков;
  • PLAY — старт вещания;
  • TEARDOWN — остановка вещания.
И особенность RTSP в том, что он сам по себе не передаёт нужные нам видео данные! Целый протокол только для установления связи. Здесь просматривается аналогия с MVC, идёт разделение между данными и их описанием.

Рабочей лошадкой является другой протокол: RTP — Real-time Transport Protocol — транспортный протокол реального времени. С его помощью и передаются нужные нам данные. Стоит отметить, что с этим протоколом очень даже приятно работать, дело в том, что он облегчает клиентскому ПО восстановление данных после их фрагментации на канальном уровне. А также несёт в себе ещё несколько полезных полей: формат передаваемых данных, временную метку и поле синхронизации (если передаётся, например, одновременно аудио и видео). Хотя этот протокол может работать по TCP, его обычно используют с UDP из-за его ориентированности на скорость. То есть RTP данные это UDP датаграмма с заголовком и полезными данными медиа-контента (payload).

Казалось бы нам больше ничего и не нужно. Подключаемся по RTSP, забираем по RTP. Но не тут-то было, умные дяди придумали третий протокол: RTCP — Real-time Transport Control Protocol — протокол контроля за транспортом в реальном времени. Этот протокол служит для определения качества сервиса, с его помощью клиент и сервер знают как хорошо или плохо идёт передача контента. В соответствии с этими данными сервер, например, может понизить битрейт или вообще перейти на другой кодек.

Принято, что RTP использует чётный номер порта, а RTCP следующий нечётный.

Пример общения по RTSP

У меня только один источник RTSP потока — камера eVidence APIX Box M1, поэтому все примеры относятся к ней.

Ниже лог общения между плеером VLC (он правда мне очень помогает в моих исследованиях) и этой камерой. Первый запрос от VLC на порт 554 камеры. Ответ через пустую строку и начинается с «RTSP/1.0».

01: OPTIONS rtsp://192.168.0.254/jpeg RTSP/1.0
02: CSeq: 1
03: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24)
04: 
05: RTSP/1.0 200 OK
06: CSeq: 1
07: Date: Fri, Apr 23 2010 19:54:20 GMT
08: Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE
09: 
10: DESCRIBE rtsp://192.168.0.254/jpeg RTSP/1.0
11: CSeq: 2
12: Accept: application/sdp
13: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24)
14: 
15: RTSP/1.0 200 OK
16: CSeq: 2
17: Date: Fri, Apr 23 2010 19:54:20 GMT
18: Content-Base: rtsp://192.168.0.254/jpeg/
19: Content-Type: application/sdp
20: Content-Length: 442
21: x-Accept-Dynamic-Rate: 1
22: 
23: v=0
24: o=- 1272052389382023 1 IN IP4 0.0.0.0
25: s=Session streamed by "nessyMediaServer"
26: i=jpeg
27: t=0 0
28: a=tool:LIVE555 Streaming Media v2008.04.09
29: a=type:broadcast
30: a=control:*
31: a=range:npt=0-
32: a=x-qt-text-nam:Session streamed by "nessyMediaServer"
33: a=x-qt-text-inf:jpeg
34: m=video 0 RTP/AVP 26
35: c=IN IP4 0.0.0.0
36: a=control:track1
37: a=cliprect:0,0,720,1280
38: a=framerate:25.000000
39: m=audio 7878 RTP/AVP 0
40: a=rtpmap:0 PCMU/8000/1
41: a=control:track2
42: 
43: 
44: SETUP rtsp://192.168.0.254/jpeg/track1 RTSP/1.0
45: CSeq: 3
46: Transport: RTP/AVP;unicast;client_port=41760-41761
47: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24)
48: 
49: RTSP/1.0 200 OK
50: CSeq: 3
51: Cache-Control: must-revalidate
52: Date: Fri, Apr 23 2010 19:54:20 GMT
53: Transport: RTP/AVP;unicast;destination=192.168.0.4;source=192.168.0.254;client_port=41760-41761;
            server_port=6970-6971
54: Session: 1
55: x-Transport-Options: late-tolerance=1.400000
56: x-Dynamic-Rate: 1
57: 
58: SETUP rtsp://192.168.0.254/jpeg/track2 RTSP/1.0
59: CSeq: 4
60: Transport: RTP/AVP;unicast;client_port=7878-7879
61: Session: 1
62: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24)
63: 
64: RTSP/1.0 200 OK
65: CSeq: 4
66: Cache-Control: must-revalidate
67: Date: Fri, Apr 23 2010 19:54:20 GMT
68: Transport: RTP/AVP;unicast;destination=192.168.0.4;source=192.168.0.254;client_port=7878-7879;
            server_port=6972-6973
69: Session: 1
70: x-Transport-Options: late-tolerance=1.400000
71: x-Dynamic-Rate: 1
72: 
73: PLAY rtsp://192.168.0.254/jpeg/ RTSP/1.0
74: CSeq: 5
75: Session: 1
76: Range: npt=0.000-
77: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24)
78: 
79: RTSP/1.0 200 OK
80: CSeq: 5
81: Date: Fri, Apr 23 2010 19:54:20 GMT
82: Range: npt=0.000-
83: Session: 1
84: RTP-Info: url=rtsp://192.168.0.254/jpeg/track1;seq=20730;
            rtptime=3869319494,url=rtsp://192.168.0.254/jpeg/track2;seq=33509;rtptime=3066362516
85: 
86: # В этот момент начинается передача контента и следующая команда вызывается для остановки вещания
87: 
88: TEARDOWN rtsp://192.168.0.254/jpeg/ RTSP/1.0
89: CSeq: 6
90: Session: 1
91: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24)
92: 
93: RTSP/1.0 200 OK
94: CSeq: 6
95: Date: Fri, Apr 23 2010 19:54:25 GMT

Первым делом VLC спрашивает камеру:
— А что я вообще могу с тобой делать? (OPTIONS)
— И тебе привет. А можешь ты меня просить сделать любое из OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY и PAUSE.
— Ладно, тогда скажи мне что у тебя есть по запросу "/jpeg"? (DESCRIBE)
— Тут у меня видео первой дорожкой идёт, M-JPEG, а второй дорожкой идёт аудио простое.
— Интересно глянуть на видео, первую дорожку, отсыпь мне его, пожалуйста в карман номер 41760, а шелуху всякую можешь в карман номер 41761 скидывать. (SETUP track1)
— ОК, по твоей команде…
— И звук тоже хочу послушать, сыпь в 7878, 7879 карманы. (SETUP track2)
— Да без проблем.
— Ну, посыпали. (PLAY)
Через некоторое время:
— Ладно, хватит, насмотрелся. (TEARDOWN)
— Как скажешь.

На этом небольшое лирическое отступление заканчивается. В первом запросе "OPTIONS rtsp://192.168.0.254/jpeg RTSP/1.0" напоминает "GET /jpeg HTTP/1.1" в том смысле, что с этого начинается разговор, а так у протокола HTTP тоже есть метод OPTIONS. Здесь 192.168.0.254 — это IP адрес моей камеры. CSeq отражает порядковый номер запроса, ответ от сервера должен содержать тот же самый CSeq.

А ответ от сервера начинается с "RTSP/1.0 200 OK", это прямо как "HTTP/1.1 200 OK" — знак, что всё хорошо: запрос принят, запрос понятен и не было никаких проблем в его реализации. И прямым текстом следует перечисление всех доступных методов.

Далее мы собираем информацию о том, что нас ждёт по запросу /jpeg, ведь мы именно за ним и пришли по ссылке "rtsp://192.168.0.254/jpeg". Также указываем, что хотим получить ответ в виде SDP (строка 12).

В ответ нам приходит RTSP заголовок с указанием Content-Type и Content-Length, а после заголовка через пустую строку непосредственно сам контент в формате SDP:

v=0
o=- 1272052389382023 1 IN IP4 0.0.0.0
s=Session streamed by "nessyMediaServer"
i=jpeg
t=0 0
a=tool:LIVE555 Streaming Media v2008.04.09
a=type:broadcast
a=control:*
a=range:npt=0-
a=x-qt-text-nam:Session streamed by "nessyMediaServer"
a=x-qt-text-inf:jpeg
m=video 0 RTP/AVP 26
c=IN IP4 0.0.0.0
a=control:track1
a=cliprect:0,0,720,1280
a=framerate:25.000000
m=audio 7878 RTP/AVP 0
a=rtpmap:0 PCMU/8000/1
a=control:track2

Здесь всё достаточно очевидно. Нужны нам следующие строки:

# Для видео
m=video 0 RTP/AVP 26 # Транспорт потока RTP/AVP, порт любой, видео формат 26, что соответствует Motion-JPEG
a=control:track1 # Название трека
a=cliprect:0,0,720,1280 # Отсюда вытаскиваем разрешение
a=framerate:25.000000 # И частота кадров если нам понадобится

# Для аудио
m=audio 7878 RTP/AVP 0 # Порт 7878, транспорт и формат аудио, 0 - PCM
a=control:track2 # Название трека

Если мы хотим получать только видео, то из аудио данных мы игнорируем всё, кроме названия трека. Он нам нужен, чтобы настроить поток, но нас никто не заставляет этот поток принимать, однако камера отказывается работать, если игнорировать аудио полностью (если делать SETUP только для видео трека).

Честно говоря, я не знаю как будут реагировать разные камеры, если пренебрегать номером порта для аудио потока (7878), ведь мы его указываем с командой SETUP.

Далее идут два запроса SETUP, c указанием портов, на которые мы бы хотели принимать видео и аудио потоки. Первое число — порт для RTP, второе — для RTCP. В ответе камеры содержится информация о портах, с ними можно сверяться, чтобы удостовериться, что всё настроено правильно. Ещё нам необходимо запомнить идентификатор Session. Мы должны будем указывать его во всех последующих вызовах.

После команды PLAY начнётся передача видео на порт 41760 и аудио на порт 7878. И по команде TEARDOWN вещание прекращается, соединение разрывается.

MJPEG over RTP

К нам приходят RTP пакеты, нам их нужно расшифровать. Для этого я приведу здесь таблицу такого пакета с описанием всех полей.
+ Bit offset 0-1 2 3 4-7 8 9-15 16-31
0 V P X CC M PT Sequence Number
32 Timestamp
64 SSRC Identifier
96 … CSRC Identifiers …
96+(CC×32) Extension Header ID Extension Header Length (EHL)
96+(CC×32)+(X×32) … Extension Header …
96+(CC×32)+(X×32)+(X×EHL) Payload

  1. V (Version): (2) версия протокола. Сейчас номер версии 2.
  2. P (Padding, Дополнение): (1) используется в случаях, когда RTP-пакет дополняется пустыми байтами в конце, например для алгоритмов шифрования.
  3. X (Extension, Расширение): (1) указывает на наличие расширенного заголовка, определяется приложением. В нашем случае это не используется.
  4. CC (CSRC Count): (4) содержит количество CSRC-идентификаторов. Нами тоже не используется.
  5. M (Marker): (1) используется на уровне приложения, в нашем случае этот бит выставляется в единицу, если RTP пакет содержит окончание JPEG кадра.
  6. PT (Payload Type): (7) указывает формат полезной нагрузки — передаваемых данных. Для MJPEG это 26.
  7. Sequence Number: (16) номер RTP пакета, используется для обнаружения потерянных пакетов.
  8. Timestamp (32): временная метка, в нашем случае 90000 герцовая (90000 = 1 секунда).
  9. SSRC (Synchronization Source): (32) идентификатор синхронизатора, как смешно бы это не звучало. Определяет источник потока.
  10. CSRC (Contributing Source): (32) идентификаторы дополнительных источников, используется когда у нас поток идёт с нескольких мест.
  11. Extension Header ID: (16) идентификатор расширения, если оно у нас есть надо знать что оно из себя представляет. В нашем случае не используется.
  12. Extension Header Length: (16) длинна этого заголовка в байтах.
  13. Extension Header (Заголовок Расширения): сам заголовок. Содержимое может быть самым разным, зависит от контекста.
  14. Payload (Нагрузка): полезные данные — те самые наши JPEG кадры. Фрагментированные, конечно.
Поля начиная с CSRC необязательные. Для передачи MJPEG с камер они не используются, на сколько я знаю.

Переносимся на один уровень инкапсуляции выше. Теперь стоит задача преобразовать получаемые видео данные в полноценное JPEG изображение. В случае MJPEG по HTTP всё просто — вырезаем кусок потока и работаем с ним сразу как с JPEG изображением. В случае же RTP изображение передаётся не полностью, JPEG заголовок опускается для экономии трафика. Его необходимо восстановить самостоятельно из прилагаемых данных.

Спецификация RTP Payload for MJPEG описана в RFC2435. Я также приведу Вам таблицу с описанием всех полей формата:
+ Bit offset 0-7 8-15 16-23 24-31
0 Type-specific Fragment Offset
32 Type Q Width Height
if Type in 64..127 Restart Marker header
if Q in 128..255 MBZ Precision Length
Quantization Table Data

  1. Type-specific (Зависит от типа): (8) смысл поля зависит от реализации, в нашем случае не применяется.
  2. Fragment Offset (Смещение фрагмента): (24) указывает на положение текущего фрагмента кадра во всём кадре.
  3. Type (Тип): (8) от типа зависит как восстанавливается изображение.
  4. Q (Quality): (8) качество изображения.
  5. Width: (8) ширина кадра.
  6. Height: (8) и высота.
  7. Restart Marker header (Заголовок маркеров RST): (32) используется при декодировании JPEG, если применяются RST маркеры. Не знаю используют их камеры или нет, но я этот заголовок игнорирую. Это поле появляется только при Type от 64 до 127.
  8. Quantization Table Data (Таблицы квантинизации): если они присутствуют, то не нужно их отдельно вычислять. А нужны они для правильного воссоздания картинки из JPEG данных. Если эти таблицы не правильные, то изображение будет с неправильными цветами и контрастами. Таблиц должно быть две: Luma и Chroma для яркости и цветности соответственно.
  9. MBZ, Precision, Length: (32) параметры таблиц квантинизации, я их игнорирую, Length задаю равным 128 — две таблицы по 64 байт. В ином случае я не знаю как с ними работать.
Заголовок маркеров RST и таблицы квантинизации могут и не присутствовать. Если нет первого, то очень хорошо, так как на другое я и не рассчитываю. Если нет второго — нужные таблицы вычисляются исходя из параметра Q.

RTCP пакет содержит в себе некоторое подмножество, он бывает четырёх типов: 201 — отчёт источника, 202 — отчёт получателя, 203 — описание источников и 204 — назначение определяется приложением. Мы должны принимать в первую очередь 201 тип, затем отправлять 202 тип. 203 и 204 необязательны, но я их тоже учитываю. В одном UDP пакете может быть несколько RTCP пакетов.

Все типы имеют похожую структуру. Начинается любой RTCP пакет со следующих данных:
+ Bit offset 0-1 2 3-7 8-15 16-31
0 Version Padding SC or RC or Subtype Packet Type Length

  1. Version: (2) версия RTP.
  2. Padding: (1) то же самое, что и для RTP.
  3. SC or RC or Subtype: (5) в зависимости от типа может быть количеством источников (Sources Count) или количеством получателей (Receivers Count) включенных в отчёт получателя и источника соответсвенно. Если это APP пакет, то это поле определяет подтип такого пакета.
  4. Packet Type: (8) тип пакета, 201 — отчёт источника (Sender's Report SS), 202 — отчёт получателя (Receiver's Report RR), 203 — описание источников (Source Description SDES) и 204 — назначение определяется приложением (APP).
  5. Length: (16) размер следующих за заголовком данных, измеряется в 32 битных единицах.
Далее я не буду приводить поля для каждого подтипа, их можно посмотреть в RFC3550. Скажу лишь что SS и RR типы несут в себе информацию об отправленных/полученных пакетах и о временных задержках. SDES в себе несёт разные текстовые поля, определяющие источник, такие как его имя, email, телефон, местонахождение и т.п.

На этом введение заканчивается.

Python MJPEG over RTSP client


Вот мы и добрались до питона. Клиент состоит из нескольких файлов, main.py содержит в себе callback функцию, которая обрабатывает получаемые изображения, также он запускает механизмы сетевого фреймворка Twisted и хранит в себе параметры подключения к камере. Все листинги я привожу укороченными, полную версию можно скачать по ссылке в конце статьи.
main.py
20:	def processImage(img):
21:	    'This function is invoked by the MJPEG Client protocol'
22:	    # Process image
23:	    # Just save it as a file in this example
24:	    f = open('frame.jpg', 'wb')
25:	    f.write(img)
26:	    f.close()
27:	
28:	def main():
29:	    print 'Python M-JPEG Over RSTP Client 0.1'
30:	    config = {'request': '/jpeg',
31:	          'login': '',
32:	          'password': 'admin',
33:	          'ip': '192.168.0.252',
34:	          'port': 554,
35:	          'udp_port': 41760,
36:	          'callback': processImage}
37:	    # Prepare RTP MJPEG client (technically it's a server)
38:	    reactor.listenUDP(config['udp_port'], rtp_mjpeg_client.RTP_MJPEG_Client(config))
39:	    reactor.listenUDP(config['udp_port'] + 1, rtcp_client.RTCP_Client()) # RTCP
40:	    # And RSTP client
41:	    reactor.connectTCP(config['ip'], config['port'], rtsp_client.RTSPFactory(config))
42:	    # Run both of them
43:	    reactor.run()
44:	    # On exit:
45:	    print 'Python M-JPEG Client stopped.'

В принципе можно работать и без реализации RTCP протокола и приёма аудио данных. В этом случае камера разрывает соединение через примерно минуту. Приходится всё время переподключаться, это делается автоматически, поэтому проблем не доставляет. Однако для статьи я дописал RTCP часть и сделал заготовку для приёма аудио данных.

Следующим важным файлом является rtsp_client.py. Он самый запутанный, но его цель очевидна — правильно установить соединение, описанное выше.
rtsp_client.py
012:	class RTSPClient(Protocol):
013:	    def __init__(self):
014:	        self.config = {}
015:	        self.wait_description = False
016:	
017:	    def connectionMade(self):
018:	        self.session = 1
019:	        # Authorization part
020:	        if self.config['login']:
021:	            authstring = 'Authorization: Basic ' + b64encode(self.config['login']+':'+self.config['password']) + '\r\n'
022:	        else:
023:	            authstring = ''
024:	        # send OPTIONS request
025:	        to_send = """\
026:	OPTIONS rtsp://""" + self.config['ip'] + self.config['request'] + """ RTSP/1.0\r
027:	""" + authstring + """CSeq: 1\r
028:	User-Agent: Python MJPEG Client\r
029:	\r
030:	"""
031:	        self.transport.write(to_send)
032:	        if debug:
033:	            print 'We say:\n', to_send
034:	    
035:	    def dataReceived(self, data):
036:	        if debug:
037:	            print 'Server said:\n', data
038:	        # Unify input data
039:	        data_ln = data.lower().strip().split('\r\n', 5)
040:	        # Next behaviour is relevant to CSeq
041:	        # which defines current conversation state
042:	        if data_ln[0] == 'rtsp/1.0 200 ok' or self.wait_description:
043:	            # There might be an audio stream
044:	            if 'audio_track' in self.config:
045:	                cseq_audio = 1
046:	            else:
047:	                cseq_audio = 0
048:	            to_send = ''
049:	            if 'cseq: 1' in data_ln:
050:	                # CSeq 1 -> DESCRIBE
051:	                to_send = """\
052:	DESCRIBE rtsp://""" + self.config['ip'] + self.config['request'] + """ RTSP/1.0\r
053:	CSeq: 2\r
054:	Accept: application/sdp\r
055:	User-Agent: Python MJPEG Client\r
056:	\r
057:	"""
058:	            elif 'cseq: 2' in data_ln or self.wait_description:
059:	                # CSeq 2 -> Parse SDP and then SETUP
060:	                data_sp = data.lower().strip().split('\r\n\r\n', 1)
061:	                # wait_description is used when SDP is sent in another UDP
062:	                # packet
063:	                if len(data_sp) == 2 or self.wait_description:
064:	                    # SDP parsing
065:	                    video = audio = False
066:	                    is_MJPEG = False
067:	                    video_track = ''
068:	                    audio_track = ''
069:	                    if len(data_sp) == 2:
070:	                        s = data_sp[1].lower()
071:	                    elif self.wait_description:
072:	                        s = data.lower()
073:	                    for line in s.strip().split('\r\n'):
074:	                        if line.startswith('m=video'):
075:	                            video = True
076:	                            audio = False
077:	                            if line.endswith('26'):
078:	                                is_MJPEG = True
079:	                        if line.startswith('m=audio'):
080:	                            video = False
081:	                            audio = True
082:	                            self.config['udp_port_audio'] = int(line.split(' ')[1])
083:	                        if video:
084:	                            params = line.split(':', 1)
085:	                            if params[0] == 'a=control':
086:	                                video_track = params[1]
087:	                        if audio:
088:	                            params = line.split(':', 1)
089:	                            if params[0] == 'a=control':
090:	                                audio_track = params[1]
091:	                    if not is_MJPEG:
092:	                        print "Stream", self.config['ip'] + self.config['request'], 'is not an MJPEG stream!'
093:	                    if video_track: self.config['video_track'] = 'rtsp://' + self.config['ip'] + self.config['request'] + '/' + basename(video_track)
094:	                    if audio_track: self.config['audio_track'] = 'rtsp://' + self.config['ip'] + self.config['request'] + '/' + basename(audio_track)
095:	                    to_send = """\
096:	SETUP """ + self.config['video_track'] + """ RTSP/1.0\r
097:	CSeq: 3\r
098:	Transport: RTP/AVP;unicast;client_port=""" + str(self.config['udp_port']) + """-"""+ str(self.config['udp_port'] + 1) + """\r
099:	User-Agent: Python MJPEG Client\r
100:	\r
101:	"""
102:	                    self.wait_description = False
103:	                else:
104:	                    # Do not have SDP in the first UDP packet, wait for it
105:	                    self.wait_description = True
106:	            elif "cseq: 3" in data_ln and 'audio_track' in self.config:
107:	                # CSeq 3 -> SETUP audio if present
108:	                self.session = data_ln[5].strip().split(' ')[1]
109:	                to_send = """\
110:	SETUP """ + self.config['audio_track'] + """ RTSP/1.0\r
111:	CSeq: 4\r
112:	Transport: RTP/AVP;unicast;client_port=""" + str(self.config['udp_port_audio']) + """-"""+ str(self.config['udp_port_audio'] + 1) + """\r
113:	Session: """ + self.session + """\r
114:	User-Agent: Python MJPEG Client\r
115:	\r
116:	"""
117:	                reactor.listenUDP(self.config['udp_port_audio'], rtp_audio_client.RTP_AUDIO_Client(self.config))
118:	                reactor.listenUDP(self.config['udp_port_audio'] + 1, rtcp_client.RTCP_Client()) # RTCP
119:	            elif "cseq: "+str(3+cseq_audio) in data_ln:
120:	                # PLAY
121:	                to_send = """\
122:	PLAY rtsp://""" + self.config['ip'] + self.config['request'] + """/ RTSP/1.0\r
123:	CSeq: """ + str(4+cseq_audio) + """\r
124:	Session: """ + self.session + """\r
125:	Range: npt=0.000-\r
126:	User-Agent: Python MJPEG Client\r
127:	\r
128:	"""
129:	            elif "cseq: "+str(4+cseq_audio) in data_ln:
130:	                if debug:
131:	                    print 'PLAY'
132:	                pass
133:	                
134:	            elif "cseq: "+str(5+cseq_audio) in data_ln:
135:	                if debug:
136:	                    print 'TEARDOWN'
137:	                pass
138:	
139:	            if to_send:
140:	                self.transport.write(to_send)
141:	                if debug:
142:	                    print 'We say:\n', to_send

В случае присутсвия аудио трека, этот модуль также запускает rtp_audio_client.py и соответствующий RTCP клиент.

После успешного соединения за работу принимается rtp_mjpeg_client.py, обрабатывая входящий поток данных.
rtp_mjpeg_client.py
08:	class RTP_MJPEG_Client(DatagramProtocol):
09:	    def __init__(self, config):
10:	        self.config = config
11:	        # Previous fragment sequence number
12:	        self.prevSeq = -1
13:	        self.lost_packet = 0
14:	        # Object that deals with JPEGs
15:	        self.jpeg = rfc2435jpeg.RFC2435JPEG()
16:	
17:	    def datagramReceived(self, datagram, address):
18:	        # When we get a datagram, parse it
19:	        rtp_dg = rtp_datagram.RTPDatagram()
20:	        rtp_dg.Datagram = datagram
21:	        rtp_dg.parse()
22:	        # Check for lost packets
23:	        if self.prevSeq != -1:
24:	            if (rtp_dg.SequenceNumber != self.prevSeq + 1) and rtp_dg.SequenceNumber != 0:
25:	                self.lost_packet = 1
26:	        self.prevSeq = rtp_dg.SequenceNumber
27:	        # Handle Payload
28:	        if rtp_dg.PayloadType == 26: # JPEG compressed video
29:	            self.jpeg.Datagram = rtp_dg.Payload
30:	            self.jpeg.parse()
31:	            # Marker = 1 if we just received the last fragment
32:	            if rtp_dg.Marker:
33:	                if not self.lost_packet:
34:	                    # Obtain complete JPEG image and give it to the
35:	                    # callback function
36:	                    self.jpeg.makeJpeg()
37:	                    self.config['callback'](self.jpeg.JpegImage)
38:	                else:
39:	                    #print "RTP packet lost"
40:	                    self.lost_packet = 0
41:	                    self.jpeg.JpegPayload = ""

Он прост в понимании. Каждый раз, когда мы принимаем очередную датаграмму, мы парсим её с помощью модуля rtp_datagram.py, а результат скармливаем модулю rfc2435jpeg.py, который создаёт полноценное JPEG изображение. Далее мы ждём появления маркера rtp_dg.Marker и как он появится вызываем callback функцию с восстановленным изображением.

Парсер RTP датаграм выглядит вот так:
rtp_datagram.py
26:	    def parse(self):        
27:	        Ver_P_X_CC, M_PT, self.SequenceNumber, self.Timestamp, self.SyncSourceIdentifier = unpack('!BBHII', self.Datagram[:12])
28:	        self.Version =      (Ver_P_X_CC & 0b11000000) >> 6
29:	        self.Padding =      (Ver_P_X_CC & 0b00100000) >> 5
30:	        self.Extension =    (Ver_P_X_CC & 0b00010000) >> 4
31:	        self.CSRCCount =     Ver_P_X_CC & 0b00001111
32:	        self.Marker =       (M_PT & 0b10000000) >> 7
33:	        self.PayloadType =   M_PT & 0b01111111
34:	        i = 0
35:	        for i in range(0, self.CSRCCount, 4):
36:	            self.CSRS.append(unpack('!I', self.Datagram[12+i:16+i]))
37:	        if self.Extension:
38:	            i = self.CSRCCount * 4
39:	            (self.ExtensionHeaderID, self.ExtensionHeaderLength) = unpack('!HH', self.Datagram[12+i:16+i])
40:	            self.ExtensionHeader = self.Datagram[16+i:16+i+self.ExtensionHeaderLength]
41:	            i += 4 + self.ExtensionHeaderLength
42:	        self.Payload = self.Datagram[12+i:]

Модуль восстановления JPEG достаточно большой, так как содержит в себе несколько таблиц и довольно длинную функцию генерации заголовка. Поэтому я их здесь опущу, предоставив только функции парсинга полезной нагрузки RTP и создания окончательного JPEG изображения.
rfc2435jpeg.py
287:	    def parse(self):
288:	        HOffset = 0
289:	        LOffset = 0
290:	        # Straightforward parsing
291:	        (self.TypeSpecific,
292:	        HOffset, #3 byte offset
293:	        LOffset,
294:	        self.Type,
295:	        self.Q,
296:	        self.Width,
297:	        self.Height) = unpack('!BBHBBBB', self.Datagram[:8])
298:	        self.Offest = (HOffset << 16) + LOffset
299:	        self.Width = self.Width << 3
300:	        self.Height = self.Height << 3
301:	        
302:	        # Check if we have Restart Marker header
303:	        if 64 <= self.Type <= 127:
304:	            # TODO: make use of that header
305:	            self.RM_Header = self.Datagram[8:12]
306:	            rm_i = 4 # Make offset for JPEG Header
307:	        else:
308:	            rm_i = 0
309:	        
310:	        # Check if we have Quantinization Tables embedded into JPEG Header
311:	        # Only the first fragment will have it
312:	        if self.Q > 127 and not self.JpegPayload:
313:	            self.JpegPayload = self.Datagram[rm_i+8+132:]
314:	            QT_Header = self.Datagram[rm_i+8:rm_i+140]
315:	            (self.QT_MBZ,
316:	             self.QT_Precision,
317:	             self.QT_Length) = unpack('!BBH', QT_Header[:4])
318:	            self.QT_luma = string2list(QT_Header[4:68])
319:	            self.QT_chroma = string2list(QT_Header[68:132])
320:	        else:
321:	            self.JpegPayload += self.Datagram[rm_i+8:]
322:	        # Clear tables. Q might be dynamic.
323:	        if self.Q <= 127:
324:	            self.QT_luma = []
325:	            self.QT_chroma = []
326:	            
327:	    def makeJpeg(self):
328:	        lqt = []
329:	        cqt = []
330:	        dri = 0
331:	        # Use exsisting tables or generate ours
332:	        if self.QT_luma:
333:	            lqt=self.QT_luma
334:	            cqt=self.QT_chroma
335:	        else:
336:	            MakeTables(self.Q,lqt,cqt)        
337:	        JPEGHdr = []
338:	        # Make a complete JPEG header
339:	        MakeHeaders(JPEGHdr, self.Type, int(self.Width), int(self.Height), lqt, cqt, dri)
340:	        self.JpegHeader = list2string(JPEGHdr)
341:	        # And a complete JPEG image
342:	        self.JpegImage = self.JpegHeader + self.JpegPayload
343:	        self.JpegPayload = ''
344:	        self.JpegHeader = ''
345:	        self.Datagram = ''

Я также реализовал модуль приёма аудио данных rtp_audio_client.py, но не стал их преобразовывать в проигрываемые данные. Если кому-нибудь это будет необходимо я в этом файле сделал набросок как всё должно быть. Нужно только организовать парсинг на подобии rfc2435jpeg.py. С аудио данными легче, так как они не фрагментированны. Каждая посылка несёт в себе достаточно данных для воспроизведения. Приводить этот модуль здесь не буду, так как статья и так уж очень длинная (поскорей бы реализовали хабрафолд).

Для корректной работы нам нужно принимать и отсылать RTCP пакеты, принимаем Sender's Reports, отсылаем Receiver's Reports. Для упрощения задачи мы будем отсылать наши RR сразу после приёма SR от камеры и будем в них закладывать идеализированные данные о том, что всё хорошо.
rtcp_client.py
09:	class RTCP_Client(DatagramProtocol):
10:	    def __init__(self):
11:	        # Object that deals with RTCP datagrams
12:	        self.rtcp = rtcp_datagram.RTCPDatagram()
13:	    def datagramReceived(self, datagram, address):
14:	        # SSRC Report received
15:	        self.rtcp.Datagram = datagram
16:	        self.rtcp.parse()
17:	        # Send back our Receiver Report
18:	        # saying that everything is fine
19:	        RR = self.rtcp.generateRR()
20:	        self.transport.write(RR, address)

А вот модуль работы непосредственно с RTCP датаграмами. Он получился тоже достаточно большим.
rtcp_datagram.py
049:	    def parse(self):
050:	        # RTCP parsing is complete
051:	        # including SDES, BYE and APP
052:	        # RTCP Header
053:	        (Ver_P_RC,
054:	        PacketType,
055:	        Length) = unpack('!BBH', self.Datagram[:4])
056:	        Version = (Ver_P_RC & 0b11000000) >> 6
057:	        Padding = (Ver_P_RC & 0b00100000) >> 5
058:	        # Byte offset
059:	        off = 4
060:	        # Sender's Report
061:	        if PacketType == 200:
062:	            # Sender's information
063:	            (self.SSRC_sender,
064:	            self.NTP_TimestampH,
065:	            self.NTP_TimestampL,
066:	            self.RTP_Timestamp,
067:	            self.SenderPacketCount,
068:	            self.SenderOctetCount) = unpack('!IIIIII', self.Datagram[off: off + 24])
069:	            off += 24
070:	            ReceptionCount = Ver_P_RC & 0b00011111
071:	            if debug:
072:	                print 'SDES: SR from', str(self.SSRC_sender)
073:	            # Included Receiver Reports
074:	            self.Reports = []
075:	            i = 0
076:	            for i in range(ReceptionCount):
077:	                self.Reports.append(Report())
078:	                self.Reports[i].SSRC,
079:	                self.Reports[i].FractionLost,
080:	                self.Reports[i].CumulativeNumberOfPacketsLostH,
081:	                self.Reports[i].CumulativeNumberOfPacketsLostL,
082:	                self.Reports[i].ExtendedHighestSequenceNumberReceived,
083:	                self.Reports[i].InterarrivalJitter,
084:	                self.Reports[i].LastSR,
085:	                self.Reports[i].DelaySinceLastSR = unpack('!IBBHIIII', self.Datagram[off: off + 24])
086:	                off += 24
087:	        # Source Description (SDES)
088:	        elif PacketType == 202:
089:	            # RC now is SC
090:	            SSRCCount = Ver_P_RC & 0b00011111
091:	            self.SourceDescriptions = []
092:	            i = 0
093:	            for i in range(SSRCCount):
094:	                self.SourceDescriptions.append(SDES())
095:	                SSRC, = unpack('!I', self.Datagram[off: off + 4])
096:	                off += 4
097:	                self.SourceDescriptions[i].SSRC = SSRC
098:	                SDES_Item = -1
099:	                # Go on the list of descriptions
100:	                while SDES_Item != 0:
101:	                    SDES_Item, = unpack('!B', self.Datagram[off])
102:	                    off += 1
103:	                    if SDES_Item != 0:
104:	                        SDES_Length, = unpack('!B', self.Datagram[off])
105:	                        off += 1
106:	                        Value = self.Datagram[off: off + SDES_Length]
107:	                        off += SDES_Length
108:	                        if debug:
109:	                            print 'SDES:', SDES_Item, Value
110:	                    if SDES_Item == 1:
111:	                        self.SourceDescriptions[i].CNAME = Value
112:	                    elif SDES_Item == 2:
113:	                        self.SourceDescriptions[i].NAME = Value
114:	                    elif SDES_Item == 3:
115:	                        self.SourceDescriptions[i].EMAIL = Value
116:	                    elif SDES_Item == 4:
117:	                        self.SourceDescriptions[i].PHONE = Value
118:	                    elif SDES_Item == 5:
119:	                        self.SourceDescriptions[i].LOC = Value
120:	                    elif SDES_Item == 6:
121:	                        self.SourceDescriptions[i].TOOL = Value
122:	                    elif SDES_Item == 7:
123:	                        self.SourceDescriptions[i].NOTE = Value
124:	                    elif SDES_Item == 8:
125:	                        self.SourceDescriptions[i].PRIV = Value
126:	                        # Extra parsing for PRIV is needed
127:	                    elif SDES_Item == 0:
128:	                        # End of list. Padding to 32 bits
129:	                        while (off % 4):
130:	                            off += 1
131:	        # BYE Packet
132:	        elif PacketType == 203:
133:	            SSRCCount = Ver_P_RC & 0b00011111
134:	            i = 0
135:	            for i in range(SSRCCount):
136:	                SSRC, = unpack('!I', self.Datagram[off: off + 4])
137:	                off += 4
138:	                print 'SDES: SSRC ' + str(SSRC) + ' is saying goodbye.'
139:	        # Application specific packet
140:	        elif PacketType == 204:
141:	            Subtype = Ver_P_RC & 0b00011111
142:	            SSRC, = unpack('!I', self.Datagram[off: off + 4])
143:	            Name = self.Datagram[off + 4: off + 8]
144:	            AppData = self.Datagram[off + 8: off + Length]
145:	            print 'SDES: APP Packet "' + Name + '" from SSRC ' + str(SSRC) + '.'
146:	            off += Length
147:	        # Check if there is something else in the datagram        
148:	        if self.Datagram[off:]:
149:	            self.Datagram = self.Datagram[off:]
150:	            self.parse()
151:	    
152:	    def generateRR(self):
153:	        # Ver 2, Pad 0, RC 1
154:	        Ver_P_RC = 0b10000001
155:	        # PT 201, Length 7, SSRC 0xF00F - let it be our ID
156:	        Header = pack('!BBHI', Ver_P_RC, 201, 7, 0x0000F00F)
157:	        NTP_32 = (self.NTP_TimestampH & 0x0000FFFF) + ((self.NTP_TimestampL & 0xFFFF0000) >> 16)
158:	        # No lost packets, no delay in receiving data, RR sent right after receiving SR
159:	        # Instead of self.SenderPacketCount should be proper value
160:	        ReceiverReport = pack('!IBBHIIII', self.SSRC_sender, 0, 0, 0, self.SenderPacketCount, 1, NTP_32, 1)
161:	        return Header + ReceiverReport

Парсинг строго согласно RFC. Использую функцию unpack для конвертирования данных в численные переменные, по массиву данных перемещаюсь с помощью переменной off, которая содержит текущее смещение.

А вот и ссылка: Python MJPEG over RTSP client.

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

Полезно почитать

  1. Multimedia over the Internet
  2. Список RTP профилей для аудио и видео
На этом статье конец, а кто осилил — молодец!
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 39

    +2
    Все очень интересно, хотя и тяжело для восприятия (особенно листинги на питоне). За подробное описание протоколов спасибо! Буду знать, как работает моя сетевая камера для видеонаблюдения — может, чего своего напишу для работы с ней (в стандартной программе не хватает детектора движения с настройками)
      0
      У меня есть долгосрочное желание написать свою систему видеонаблюдения. Простая система у меня уже есть, записывает видео с video4inux2 устройств с помощью ffmpeg. Хочу сделать её максимально модульной, чтобы кто угодно мог писать расширения для неё. Основная проблема в грамотном дизайне архитектуры.
        0
        > Основная проблема в грамотном дизайне архитектуры.

        Судя по коду, врядли получится, извините.
          0
          Да, этот код сначала создавался без какой бы то нибыло проработки, особенно видно в rtsp_client.py. По хорошему переписать его надо. Плюс хорошо было бы отойти от использования питоновских листов в пользую простых массивов. Многие моменты отсутствуют необходимые для законченных решений, но этот код работает и используется. А вот построение архитектуры является моей основной проблемой.
            +1
            rtsp_client.py это вообще похапня какая-то, если уж начистоту.

            Но вообще непонятно, глядя в код, а чего, собственно, хочет автор?

            Заморочиться на голом python'e врукопашную собственным rtsp клиентом в целях самообразования и «покопаться»? Тогда зачем читерствует twisted'ом, который снимает изрядную долю мороки.

            А если автор хочет написать работающую систему для решения конкретных проблем, то почему не flumotion, почему не gstreamer? В такой код автор сам опухнет h264 и ffmpeg прикручивать, чего уж там «кто угодно мог писать расширения»…
              0
              Стояли вполне конкретные задачи, использовать собственный клиент оказалось легче и надёжднее, чем подключать готовые решения (я правда пробывал, включая OpenRTSP, gstreamer и ffmpeg). А когда код обрёл некую универсальность, то решил поделиться им с общественностью.
              Твистед как раз для облегчения рутины, считаете нужно было пользоваться socket?

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

              Кстати, к этому коду h264 прикрутить не сложно, в смыле сохранение видео потока без декодирования, который бы проигрывался в любом плеере. А ffmpeg у меня сейчас работает с модулем на С, который контролируется питоновским скриптом.

              В любом случае спасибо за здоровую критику.
                0
                вот поэтому прежде чем выложить на хабр что-то из кода надо сто раз подумать. оформить как на выставку чтобы такие вот задроты не заипали своим перфекционизъмомъ.

                  +1
                  У задротов прямо сейчас python код 24х365 тащит видео с 32 (пока) камер в разных концах города, перекодирует на лету в h264, раздает в браузеры и на мобильные устройства, пишет в архивы, хранит 2 недели и с сайта раздает архив любопытным или просто тем, кого въедут на перекрестке. Задроты как бы немного в теме и, в принципе, им было бы что на эту тему сказать, но… Раз не надо, то не надо.

                  А если кому-то вот такие вот…

                  051: to_send = """\
                  052: DESCRIBE rtsp://""" + self.config['ip'] + self.config['request'] + """ RTSP/1.0\r
                  053: CSeq: 2\r
                  054: Accept: application/sdp\r
                  055: User-Agent: Python MJPEG Client\r
                  056: \r
                  057: """

                  … изыски не кажутся похапнёй, то флаг им в руки.
                    0
                    Надо
                      0
                      Python, Gstreamer, x264, всё склеено собственным кодом (обвязки, мониторинг, обработка всяких реконнектов и т.п.)

                      32 камеры тащит машина (Sun Fire X4150) 8 ядер, 4 гига. Основная нагрузка — транскодинг (камеры отдают _не_ h264, приходится на лету перекодировать в то, что понимают флэш и яблоки).

                      Веб-морда на Pylons. нужный кусок архива по требованию достается из архива (дата-время начала, продолжительность куска).

                      Примерно такой же код у нас параллельно пишет (из эфира) и транслирует в городской пиринг 16 телеканалов. Отличается только непосредственно модуль забора сигнала, там естественно не с камеры по ip.

                      Архитектурно: несколько процессов на сущность (камера или канал) — хваталка сигнала, перекодировалка в то_что_нужно, писалка, раздавалка. Почему не потоки? Потому что потоки тут не нужны. Упал процесс, обслуживающий одну из камер, да черт с ним, остальных не тронуло.

                      Почему не vlc, «который все это умеет»? Потому что это дырявое ведро. Ненадежное в реальной жизни. И, кстати, в сухом остатке более прожорливое.

                      Самонаписанный код, аптаймы по полгода, пока админ не остановит. Аптаймы не машин, а именно самих процессов — никаких костылей типа «vlc в бесконечном цикле, если что, то рестартуем, и если памяти полтора гига сожрал, тоже рестартуем».

                      Код показать, естественно, не смогу.
                        0
                        Раз уж вы описали свою систему, можно несколько вопросов?
                        Скажите как у вас идёт обмен иформацией (те же видео данные) между процессами?
                        У меня сейчас система тоже многопроцессорная, настройка модуля захвата с v4l2 устройства после запуска осуществляется через стандартный ввод, а получение сырых видеоданных для обработки производится по UDP через lo или реальный канал, если захват идёт с удалённой машины (в этом случае уже после кодера).
                        Есть же ещё shared memory (которым, например, пользуется zoneminder) хотелось бы узнать на чём вы остановились.
                        Что вы используете в качестве сетевой прослойки: socket, twisted, gevent?
                        У вас кодеры тоже на питоне написаны?
                        Обёртка к gstreamer сразу на питоне или на C сначала?
                          0
                          > Скажите как у вас идёт обмен иформацией (те же видео данные) между процессами?

                          По UDP, всё, как у Вас.

                          > Есть же ещё shared memory (которым, например, пользуется zoneminder) хотелось бы узнать на чём вы остановились.

                          Изначально нам не хватало мощности (мы начали несколько лет назад, процессоры были другими) сложить весь транскодинг в одну машину. Поэтому (я там писал, что кроме камер кодируем еще телеканалы) делали так: 1 канал = 1 тазик. То есть захват шел «с удаленной машины», а стало быть shared memory в пролете. Ну а потом уже «зачем переделывать, если работает». Работает неплохо, плюс возможность при нужде утащить один из кубиков конструктора на другую машину, ниче не отломав при этом.

                          > Что вы используете в качестве сетевой прослойки: socket, twisted, gevent?

                          udpsink в gstreamer'е :)

                          > У вас кодеры тоже на питоне написаны?

                          Не совсем понял вопрос. С одной стороны да, процесс называется encoder.py :) С другой стороны, никто на голом питоне кодировщик в H264 не реализовывал, естественно. Просто соответствующим образом построенный pipeline для gstreamer'а, с элементом x264enc где-то в середине.

                          > Обёртка к gstreamer сразу на питоне или на C сначала?

                          Сразу gst-python, ниже лезть не понадобилось.
                            0
                            > Не совсем понял вопрос.
                            Это по сути один был вопрос про обёртку gstreamer. Надо было на одной строке их записать.

                            Спасибо, полезная информация.
                              0
                              Из подводных камней не могу не упомянуть только необходимость тщательного тестирования перед любым апгрейдом gstreamer, x264 и т.п. пакетов в дистрибутиве. Лучше всего, убедившись, что всё работает как надо, просто поставить on hold, жестоко зафиксировав установленные версии для пакетного менеджера. Иначе в один прекрасный день apt, pacman и иже с ними что-нибудь обновят, после чего кодировщик запросто начнет кушать раза в полтора больше CPU. Мы по загрузке ходили по лезвию ножа, едва укладываясь в железо, и 32 кодировщика на одной машине. Поэтому возрастание обжорства даже на 10% могло вылезти боком. 32*10% = «упёрлись и перестали успевать».

                              Поэтому лучше зафиксировать и не дышать. Если появятся веские основания для апдейта, то можно как следует проверить (в идеале сделать lvm снэпшот, чтобы в случае чего откатиться обратно вообще без геморроя), обновить и зафиксировать снова.
                                0
                                Благодарю.
                                Кстати, на наших машинах максимум 8 камер кодируются, в mpeg4, c x264 того же результата получить не удалось, сложно разобраться во всех этих тонких настройках кодека, битрейт получается ниже, картинка четче, но ресурсо затраты выше, если же их уменьшать, качество падает ниже mpeg4 варианта. Машины не серверные, только то, что есть в обычных магазинах.
                                А если 16 камер, то только уже при просмотре могут появиться артефакты (начиная где-то с 14 или 15 камеры).
                                Кодировать 32 камеры это, конечно, зверь машина.
                                  0
                                  У нас размер меньше. Это чисто мониторинг пробок, поэтому никакой нужды сжимать полный PAL D1 (и тем паче 720p) как бы и нет. Движется улица или стоит, прекрасно видно в CIF.

                                  Тем более, что если просто так, «воровского форса ради» повысить разрешение до такой степени, что невзначай станут видны номера машин, можно огрести уже чисто юридических проблем.

                                  Типа личные данные, публичный доступ, чёкаво, ктопозволил!
                          0
                          Как архив устроен?
                          Как храните, с какой глубиной, как выдаете фрагмент архива?
                        0
                        тогда что ж мы читаем его статью, а не Вашу? Вы не торопитесь её писать? так не надо п%%деть тогда.
                        >>Основная проблема в грамотном дизайне архитектуры.
                        Судя по коду, врядли получится, извините.

                        Вы категорический неправы в этом, (даже если не считать это простым хамством!).
                        из-за такого мудозвонства люди не выкладывают свой код (вполне рабочий), шлифуя его неделями, чтобы потом не страшно было сказать перед такой аудиторией как Вы «да вот на коленке тут наваял за пару часов».
                        я уверен, что это неправильный подход. неконструктивный как минимум.

                          0
                          > тогда что ж мы читаем его статью, а не Вашу?

                          habrahabr.ru/blogs/image_processing/117735/#comment_3848756
                          А кодом сверкать мне NDA не позволит, как легко догадаться.

                          > Вы категорический неправы в этом, (даже если не считать это простым хамством!).

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

                          > Я не говорю, что хочу создать систему основываясь на приведённом в статье коде,
                          > в этом случае действительно стоит по максимуму задействовать готовые библиотеки.
                    0
                    Если честно, то вам до _архитектуры_ ещё года 3-4. Это время уйдет на то, что бы понять, что это вообще всё была плохая идея.

                    Подобный процессинг на питоне имеет смысл только когда вы не ставите перед собой цель обслужить большое количество клиентов (> 500, если хочется какой-нибудь цифры).

                    То, что написано тут: github.com/erlyvideo/erlyvideo/ прожило уже немало времени и внедрений, что бы претендовать на звание «архитектуры».
                      0
                      Где же вы раньше были.
                  +1
                  Замечательно что вы хотите написать что-то свое! У меня вопрос, чем вас zoneminder не устроил?
                    0
                    По большей части им были не довольны клиенты: не удобен в работе, не захватывает аудио. И может, конечно, это я его настраивать не умею, но при сбоях питания очень часто портится база так, что не посмотреть сохранённые ранее записи. Да и сама запись в кучу последовательных JPEG файлов несколько огорчает (даже с учётом возможности дельнейшего перекодирования, всёравно не феншуй). Сейчас где позволяет бесплатная лицензия (до 4 каналов) я использую AVReg (бывший linux-DVR), а если регистратор платный ставят, то обычно это либо GLOBOSS, либо Domination, но я ими не занимаюсь.

                    Так что поле свободных регистраторов ещё далеко не перепахано.
                      0
                      Да, пожалуй соглашусь. В ряде случаев слайдшоу не очень удобно, и на моей практике браузер через несколько часов падает.

                      Свой проект планируете выпускать под свободной лицензией?
                        0
                        Из браузеров по моим личным наблюдениям лучше всех с MJPEG справляется хром (может ещё и сафари или Konqueror, не пробывал их).

                        Да, по свободной.
                  0
                  Я писал свой детектор с помощью OpenCV. Все достаточно медленно, но в принципе реализуемо. Надо только сделать буфер чтоб при наличии движения включать запись некоторое время назад.
                  +3
                  Просто о сложном. Спасибо, добавил в избранное!

                  P.S. C праздником всех!
                    +1
                    Тоже года два назад копался с RTSP.
                    Камера была с IVA, хотел вытащить метаданные по IVA — ну с этим проблем не было.
                    Но не смог запустить декодирование h.264 видео под DirectShow там надо правильно настроить декодер вот с этим где-то я застопорился. Потом появились другие задачи, бросил.
                    Понятно, что задачу можно решить при помощи libvlc.
                    Интерено, что в моей камере тоже была эта LIVE555.
                    Похоже они все ее используют.
                      0
                      Тоже хочу заняться h.264 и MPEG4. Буду ffmpeg использовать для декодирования.
                      +1
                      Большое спасибо за статью. Пришлась очень вовремя.
                      Вот у меня есть немного оффтопиковый вопрос про камеру: вы не пробовали подключаться к ней с нескольких клиентов (тех же VLC)? Как работает? Не тормозит? Да и вообще, общие впечатления от камеры какие?
                        +1
                        Пожалуйста.
                        Только что подключил четыре VLC, ноут уже сильно торомзил. С одним клиентом задержка изображения составляла полторы секунды, с четырмя составила почти две с половиной секунды.
                        Камера мне очень нравится, шустрая, картинка хорошая, есть ночной режим. Много настроек. Может одновременно отдавать видео в нескольких форматах, может что-то записывать на microSD. Стоит такая порядка 15к, есть ещё подобная, купольная, она уже где-то 10к стоит.
                          0
                          Спасибо за быстрый ответ!
                          Присматриваюсь к камерам, надо чтобы было 720p и клиентов 5-6 могли к ней цепляться через интернет без значительных провалов в производительности. Поставлю и эту в список рассматриваемых.
                            0
                            Так можно же камеры прогнать через компьютер. У меня так на одном объекте 4 камеры работают, прога забирает с них изображения а далее раздаёт в интернет (в том числе и на сайт), можно регулировать частоту кадров и размер изображения. Раздаёт тоже MJPEG, но по HTTP. Если больше никакой специфики не нужно, то должны справится и другие проги такие как flumotion или ffserver.
                          0
                          Нет, рассчитывайте на то, что камеры наблюдения непригодны для подключения более одного клиента.

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

                          Так что советую придерживаться четкого правила: одна камера — одно подключение. Хочется мультиплексировать — ставьте erlyvideo и раздавайте видео с камеры на тысячи клиентов.
                          0
                          Спасибо, очень актуально и очень интересно.

                          Скажите пожалуйста, сколько будет стоить готовое решение для просмотра видео в браузере с удаленной сетевой камеры?
                          (короче хочется с работы видеть, что происходит дома.)
                            0
                            Бесплатно. На домашнем роутере пробрасываете наружу порт для камеры, далее заходите на свою камеру из-вне и смотрите изображение.
                            Ну, может только роутер придётся купить.
                            0
                            только запрос OPTIONS аналогичен в HTTP не GET, а запросу (surprise!) OPTIONS.
                              0
                              Спасибо, подправил.
                              +1
                              Большое спасибо за статью и за сырец. Переписал на qt/c++ успешно работает на TRENDnet TV-IP512P.

                              PS
                              Мелкая очепятка в исходниках.
                              В main.py секция пароля 'password', а в файле rtsp_client.py 'pass'.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое