Захват видео с сетевых камер, часть 1
Самый простой способ передачи картинки — это MJPEG по HTTP. В этом случае кадры отдаются готовыми JPEG файлами через специальные разделители. Для таких случаев был разработан специальный MIME тип multipart. У него есть несколько подтипов, нас интересуют mixed и x-mixed-replace. Отличий между ними практически нет, мы их будем обрабатывать абсолютно одинаково. Отличие у них смысловое: mixed указывает просто на документ, состоящий из нескольких частей, эти части могут быть независимыми или могут объединяться; а тип x-mixed-replace прямо указывает, что каждая следующая часть должна заменять собой предыдущую и обрабатываться должны как обновление какого-то представления. Буква «x» вначале названия указывает на то, что этот тип является экспериментальным, но тем не менее он во всю используется.
В HTTP заголовке тип MIME указывается в Content-Type параметре:
У этих типов есть обязательный параметр boundary , который указывает каким текстом разделяются части документа. К разделителю также добавляются два дефиса в его начало. Важно чтобы этот разделитель не встречался в самом сообщении, если не указывается его размер в Content-Size .
Чтобы понять структуру передачи MJPEG по HTTP достаточно взглянуть на следующий пример:
Для анализа того, как ваша сетевая камера передаёт изображения, можно воспользоваться либо снифером (я пользуюсь WireShark), либо подключиться к камере через telnet, например так:
После строки GET нужно ещё отправить одну пустую строку для индикации того, что ваш заголовок закончился. А вместо "/jpeg" нужно написать тот запрос, по которому ваша камера отдаёт MJPEG.
В приведённом выше примере %Binary JPEG% соответствует интересующей нас информации — JPEG изображению. Его-то нам и нужно выделить из потока.
Как видно вначале идёт стандартный HTTP заголовок с описанием документа. Connection может быть как close , так и keep-alive , в нашем случае это не играет роли. Из заголовка нам всего навсего нужно две строчки: первая со статусом 200 OK, которая говорит нам, что всё хорошо, сейчас вылетит птичка; и Content-Type для определения параметра boundary .
После HTTP заголовка (после пустой строки) идёт тело передаваемого документа, состоящего из множества частей. Каждая часть начинается с разделителя, имеет свой собственный заголовок и своё тело документа после пустой строки. Content-Type: image/jpeg указывает нам, что мы действительно получаем JPEG изображения, Content-Length на размер текущего кадра в байтах (в первой части это 23950 байт), а в X-Timestamp может передаваться временная отметка текущего кадра, можно использовать для этих целей текущее время компьютера в момент приёма кадра, но X-Timestamp будет точнее, так как сеть может оказывать разное влияние на скорость передачи кадров.
Python MJPEG over HTTP ClientНесмотря на простой формат передачи изображений, их приём можно реализовать разными методами. Также играет роль TCP сегментация, а точнее подход к её обработке. Дело в том, что максимальный размер передаваемого сообщения (MTU) по Ethernet не может превышать 1500 байт и данные мы обрабатываем каждый раз когда такой пакет к нам приходит. Если анализировать информацию сразу по её приходу, то может случится так, что данные будут не полными и парсер не сможет справится со своей задачей. А если буферизировать поток входящих данных, то это не лучшим образом сказывается на производительности и ресурсоёмкости. Надежнее было бы воспользоваться буферизацией и приступать к анализу только когда уже накоплено достаточно информации (сначала нужно прочитать до '\r\n\r\n' , чтобы выделить заголовок, а потом либо до тех пор пока не встретятся два раза в потоке разделители, либо также читать до пустой строки, определять размер изображения и отсчитывать количество байт). Но я воспользовался методом обработки информации сразу по её приходу.
Код клиента состоит из двух файлов: main.py и http_mjpeg_client.py . В первом производится запуск приложения, а во втором реализована работа с камерой. Сразу их и приведу здесь. main.py
В главном файле мы определяем параметры подключения к камере в словаре config , запускаем реактор сетевого фрэймворка Twisted и обрабатываем получаемые изображения в функции processImage() . В этом примере каждый получаемый кадр просто записывается в текущую директорию с именем frame.jpg .
Работоспособность я проверял с помощью MJPEG стримера, запускал я его вот так: ./mjpg_streamer -i "./input_testpicture.so" -o "./output_http.so -w ./www" При этом request в конфигурации клиента надо указать равным /?action=stream . Передавать изображения с вебкамеры он у меня отказался.
Я старался хорошо документировать второй файл, чтобы читателю было легче понять как происходит процесс изъятия изображений из потока. На словах алгоритм можно описать следующим образом: при подсоединении к камере первым делом формируем HTTP заголовок с обращением к ней и отсылаем его, это функция connectionMade() . Функция dataReceived() вызывается всякий раз, когда к нам приходят новые данные. В ней мы проверяем установлена ли уже передача JPEG данных или нет. Если ещё нёт, то значит к нам должен прийти HTTP заголовок ответа камеры, мы его выделяем с помощью функции split('\r\n\r\n', 1) , затем разбираем его по полочкам, выделяя нужные параметры (статус и boundary ). В остальных случаях мы полученные данные сразу передаём функции findJPEG() .
В этой функции так же происходит ветвление в зависимости от того получили ли мы внутренний заголовок JPEG документа или ещё нет. Если не получили — ожидаем его и разбираем, если получили — значит данные это непосредственно JPEG изображение и складываем их в переменную self.img до тех пор пока не получим все self.next_img_size байт изображения, а когда получим — вызываем функцию, переданную нам через параметр конфигурации callback , и отдаём ей только что полученное изображение.
Параметр debug можно выставить в ноль для отключения отображения выводимой информации.
Загрузить исходный код можно по этой ссылке: Python MJPEG over HTTP Client.