- Оригинальный адрес:Flask Video Streaming Revisited
- Оригинальный автор:Miguel Grinberg
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:zhmhhu
- Корректор:
Около трех лет назад я был в этомVideo Streaming with FlaskЯ написал статью в своем блоге, в которой я придумал очень функциональный сервер потоковой передачи, который использует функцию просмотра Flask Builder дляMotion-JPEGПотоковая передача в веб-браузер. В этой статье я хотел показать простые и практичныеПотоковые ответы, что является неизвестной функцией Flask.
Эта статья была очень популярна не потому, что она научила читателей, как реализовать потоковый ответ, а потому, что многие люди хотели реализовать сервер потокового видео. К сожалению, когда я пишу свою статью, я не сосредотачиваюсь на создании надежного видеосервера, поэтому я часто получаю вопросы и просьбы о совете от читателей, которые хотят использовать видеосервер для реального приложения, но вскоре обнаруживают его ограничения.
Резюме: потоковое видео с помощью Flask
Я рекомендую вам прочитатьоригинальная статьяознакомиться с моим проектом. Короче говоря, это сервер Flask, который использует потоковый ответ для обслуживания потока видеокадров, захваченных с камеры в формате Motion JPEG. Этот формат очень прост, и хотя он не самый эффективный, он имеет то преимущество, что изначально поддерживается всеми браузерами без каких-либо сценариев на стороне клиента. По этой причине это довольно распространенный формат, используемый камерами безопасности. Чтобы продемонстрировать сервер, я написал драйвер камеры для Raspberry Pi, используя модуль камеры. Для тех из вас, у кого нет raspberry pi, а есть только ручная камера, я также написал эмулированный драйвер камеры, который передает серию изображений в формате jpeg, хранящихся на диске.
Запускайте камеру только тогда, когда есть зритель
Одна из причин, по которой людям не нравятся необработанные потоковые серверы, заключается в том, что фоновый поток, который захватывает видеокадры с камеры Raspberry Pi, запускается, когда первый клиент подключается к потоку, но после этого он никогда не останавливается. Более эффективный способ обработки этого фонового потока — запускать его только при наличии зрителя, чтобы камеру можно было отключить, когда никто не подключен.
Я только что реализовал это улучшение. Идея состоит в том, что каждый раз, когда клиент обращается к видеокадру, записывается текущее время этого доступа. Поток камеры проверяет эту метку времени и завершает работу, если обнаруживает, что она старше десяти секунд. С этим изменением, когда сервер работает в течение десяти секунд без каких-либо клиентов, он отключит свою камеру и прекратит всю фоновую активность. Как только клиент снова подключается, поток перезапускается.
Вот краткое описание этого улучшения:
class Camera(object):
# ...
last_access = 0 # 最后一个客户端访问相机的时间
# ...
def get_frame(self):
Camera.last_access = time.time()
# ...
@classmethod
def _thread(cls):
with picamera.PiCamera() as camera:
# ...
for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True):
# ...
# 如果没有任何客户端访问视屏帧
# 10 秒钟之后停止线程
if time.time() - cls.last_access > 10:
break
cls.thread = None
Упрощенный класс камеры
Распространенная проблема, о которой мне упоминали многие люди, — это сложность добавления поддержки других камер. Я реализовал это для raspberry pi.Camera
Класс довольно сложен, потому что он использует поток захвата фона для связи с оборудованием камеры.
Чтобы было проще, я решил вынести общий функционал для всей фоновой обработки кадров в базовый класс, оставив только задачу получения кадров с камеры реализовать в подклассе. модульbase_camera.py
новое вBaseCamera
Класс реализует этот базовый класс. Вот как выглядит эта общая нить:
class BaseCamera(object):
thread = None # 从摄像机读取帧的后台线程
frame = None # 后台线程将当前帧存储在此
last_access = 0 # 最后一个客户端访问摄像机的时间
# ...
@staticmethod
def frames():
"""Generator that returns frames from the camera."""
raise RuntimeError('Must be implemented by subclasses.')
@classmethod
def _thread(cls):
"""Camera background thread."""
print('Starting camera thread.')
frames_iterator = cls.frames()
for frame in frames_iterator:
BaseCamera.frame = frame
# 如果没有任何客户端访问视屏帧
# 10 秒钟之后停止线程
if time.time() - BaseCamera.last_access > 10:
frames_iterator.close()
print('Stopping camera thread due to inactivity.')
break
BaseCamera.thread = None
Эта новая версия потока камеры Raspberry Pi сделана универсальной с использованием другого генератора. ожидание потокаframes()
Метод (который является статическим методом) становится генератором, реализованным в определенном подклассе различных камер. Каждый элемент, возвращаемый итератором, должен быть видеокадром в формате jpeg.
Вот как аналоговая камера, возвращающая неподвижное изображение, вписывается в этот базовый класс:
class Camera(BaseCamera):
"""模拟相机的实现过程,将
文件1.jpg,2.jpg和3.jpg形成的重复序列以每秒一帧的速度以流式文件的形式传输。"""
imgs = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]
@staticmethod
def frames():
while True:
time.sleep(1)
yield Camera.imgs[int(time.time()) % 3]
Обратите внимание, что в этой версииframes()
Как генератор формирует скорость один кадр в секунду, просто засыпая между кадрами.
Подкласс камеры Raspberry Pi также был упрощен за счет редизайна:
import io
import picamera
from base_camera import BaseCamera
class Camera(BaseCamera):
@staticmethod
def frames():
with picamera.PiCamera() as camera:
# let camera warm up
time.sleep(2)
stream = io.BytesIO()
for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True):
# return current frame
stream.seek(0)
yield stream.read()
# reset stream for next frame
stream.seek(0)
stream.truncate()
Драйвер камеры OpenCV
Многие пользователи жаловались, что они не могут получить доступ к Raspberry Pi с модулем камеры, поэтому они не могут попробовать этот сервер, кроме как для имитации камеры. Теперь намного проще добавить драйвер камеры, я хочу, чтобы он был основан наOpenCV, который поддерживает большинство веб-камер USB и камер ноутбуков. Вот простой драйвер камеры:
import cv2
from base_camera import BaseCamera
class Camera(BaseCamera):
@staticmethod
def frames():
camera = cv2.VideoCapture(0)
if not camera.isOpened():
raise RuntimeError('Could not start camera.')
while True:
# 读取当前帧
_, img = camera.read()
# 编码成一个 jpeg 图片并且返回
yield cv2.imencode('.jpg', img)[1].tobytes()
С этим классом будет использоваться первая камера, обнаруженная вашей системой. Если вы используете ноутбук, это может быть ваша встроенная камера. Если вы хотите использовать этот драйвер, вам необходимо установить привязки OpenCV для Python:
$ pip install opencv-python
выбор камеры
Теперь проект поддерживает три разных драйвера камеры: Analog, Raspberry Pi и OpenCV. Чтобы упростить выбор используемого драйвера без необходимости редактирования кода, сервер Flask находитCAMERA
переменные среды, чтобы знать, какие классы импортировать. Эта переменная может быть установлена вpi
илиopencv
, если не задано, по умолчанию используется аналоговая камера.
Способ его реализации очень общий. несмотря ни на чтоCAMERA
Каково значение переменной среды, сервер ожидает, что драйвер будетcamera_ $ CAMERA.py
в модуле. Сервер импортирует модуль, а затем заглянет в него.Camera
своего рода. Логика на самом деле очень проста:
from importlib import import_module
import os
# import camera driver
if os.environ.get('CAMERA'):
Camera = import_module('camera_' + os.environ['CAMERA']).Camera
else:
from camera import Camera
Например, чтобы запустить сеанс OpenCV из bash, вы можете сделать:
$ CAMERA=opencv python app.py
Используя командную строку Windows, вы можете сделать следующее:
$ set CAMERA=opencv
$ python app.py
оптимизация производительности
В нескольких других наблюдениях мы обнаружили, что сервер потребляет много ресурсов ЦП. Причина этого в том, что нет синхронизации между фоновым потоком, захватывающим кадры, и генератором, который возвращает эти кадры обратно клиенту. Оба бегут как можно быстрее независимо от скорости другого.
В общем случае имеет смысл запускать фоновый поток как можно быстрее, поскольку вы хотите, чтобы частота кадров для каждого клиента была как можно выше. Но вы определенно не хотите, чтобы генератор, который передает кадры клиенту, работал быстрее, чем камера, производящая кадры, потому что это означает отправку дубликатов кадров клиенту. Хотя эти дубликаты не вызывают никаких проблем, они не приносят никакой пользы, кроме увеличения нагрузки на ЦП и сеть.
Таким образом, необходим механизм, с помощью которого генератор доставляет клиенту только сырые кадры, и если цикл доставки внутри генератора быстрее, чем частота кадров потока камеры, генератор должен ждать, пока новый кадр будет доступен, поэтому он должен его настроить. себя, чтобы соответствовать скорости камеры. С другой стороны, если цикл доставки работает медленнее, чем поток камеры, он никогда не должен отставать в обработке кадров, а должен пропускать определенные кадры, чтобы всегда доставлять последний кадр. Звучит сложно, верно?
Решение, которое я хочу, состоит в том, чтобы поток камеры сигнализировал генератору о запуске, когда доступен новый кадр. Затем генераторы могут заблокироваться в ожидании сигнала перед передачей следующего кадра. При просмотре блока синхронизации я обнаружилthreading.Eventэто функция, которая соответствует этому поведению. Таким образом, в основном каждый генератор должен иметь объект события, а затем поток камеры должен сигнализировать всем активным объектам событий, чтобы уведомить все работающие генераторы, когда доступен новый кадр. Генератор пропускает кадры и сбрасывает свои объекты событий, затем ждет, пока они снова перейдут к следующему кадру.
Чтобы избежать добавления логики обработки событий в генератор, я решил реализовать собственный класс событий, который использует идентификатор потока вызывающего объекта для автоматического создания и управления отдельными событиями для каждого клиентского потока. Честно говоря, это немного сложно, но идея исходит из того, как реализованы локальные переменные контекста Flask. Новый класс событий называетсяCameraEvent
, и имеетwait()
,set()
иclear()
метод. С поддержкой этого класса можно добавить механизм управления скоростью.BaseCamera
своего рода:
class CameraEvent(object):
# ...
class BaseCamera(object):
# ...
event = CameraEvent()
# ...
def get_frame(self):
"""返回相机的当前帧."""
BaseCamera.last_access = time.time()
# wait for a signal from the camera thread
BaseCamera.event.wait()
BaseCamera.event.clear()
return BaseCamera.frame
@classmethod
def _thread(cls):
# ...
for frame in frames_iterator:
BaseCamera.frame = frame
BaseCamera.event.set() # send signal to clients
# ...
существуетCameraEvent
Волшебство, реализованное в классе, позволяет нескольким клиентам по отдельности ожидать новых кадров.wait()
Метод выделяет отдельный объект события для каждого клиента, используя идентификатор текущего потока, и ожидает его.clear()
Метод сбрасывает событие, связанное с идентификатором потока вызывающей стороны, чтобы каждый поток генератора мог работать со своей собственной скоростью. Вызывается потоком камерыset()
Метод отправляет сигнал объекту события, назначенному всем клиентам, а также удаляет все события, которые не были обслужены, поскольку это означает, что клиент, связанный с этими событиями, закрыт, а сам клиент больше не существует. ты сможешьРепозиторий GitHubвидеть вCameraEvent
реализация класса.
Чтобы дать вам представление о степени улучшения производительности, взгляните на драйвер аналоговой камеры, который до этого изменения потреблял около 96% ресурсов ЦП, потому что он всегда отправлял повторяющиеся кадры со скоростью, намного превышающей скорость генерации одного. кадров в секунду. После этих изменений тот же поток потребляет около 3% CPU. В обоих случаях видеопоток просматривает только один клиент. Драйвер OpenCV уменьшил нагрузку ЦП с ~45% до 12% для одного клиента и увеличил на ~3% для каждого нового клиента.
Развернуть веб-сервер
В конце концов, я думаю, что если вы действительно собираетесь использовать этот сервер, вам следует использовать более мощный веб-сервер, чем тот, который поставляется с Flask. Хороший вариант — использовать Gunicorn:
$ pip install gunicorn
С Gunicorn вы можете запустить сервер следующим образом (не забудьте сначала поставитьCAMERA
переменная окружения установлена на выбранный драйвер камеры):
$ gunicorn --threads 5 --workers 1 --bind 0.0.0.0:5000 app:app
--threads 5
Опция указывает Gunicorn обрабатывать до пяти одновременных запросов. Это означает, что с этим значением у вас может быть до пяти клиентов, которые одновременно смотрят видеопоток.--workers 1
возможность ограничить сервер одним процессом. Это необходимо, поскольку только один процесс может подключаться к камере для захвата кадров.
Вы можете немного увеличить количество потоков, но если вы обнаружите, что вам нужно много потоков, может быть более эффективным использовать асинхронную структуру, чем потоки. Gunicorn можно настроить для использования двух совместимых с Flask фреймворков: gevent и eventlet. Чтобы сервер потоковой передачи видео мог использовать эти фреймворки, в фоновом потоке камеры есть небольшое дополнение:
class BaseCamera(object):
# ...
@classmethod
def _thread(cls):
# ...
for frame in frames_iterator:
BaseCamera.frame = frame
BaseCamera.event.set() # send signal to clients
time.sleep(0)
# ...
Единственное изменение здесь заключается в том, что в петлю захвата камеры добавленаsleep(0)
. Это требуется как для eventlet, так и для gevent ß, поскольку они используют совместную многозадачность. Способ, которым эти платформы достигают параллелизма, заключается в том, чтобы каждая задача освобождала ЦП, вызывая функцию, которая выполняет сетевой ввод-вывод, или выполняя это явно. Так как здесь нет ввода-вывода, функция сна выполняется для освобождения ЦП.
Теперь вы можете запустить Gunicorn с помощью gevent или eventlet worker следующим образом:
$ CAMERA=opencv gunicorn --worker-class gevent --workers 1 --bind 0.0.0.0:5000 app:app
здесь--worker-class gevent
опция настраивает Gunicorn на использование gevent framework (вы должны использоватьpip install gevent
установить его). Вы также можете использовать--worker-class eventlet
. Как указано выше,--workers 1
Ограничено одним процессом. Gunicorn in Eventlet и Gevent работников, назначенные по умолчанию тысячи параллельных клиентов, поэтому это должно быть больше, чем количество клиентов, которые могут поддерживать сервер.
в заключении
Все вышеперечисленные изменения включены вРепозиторий GitHubсередина. Надеюсь, вы получите больше удовольствия от этих улучшений.
Прежде чем закончить, я хотел бы дать краткие ответы на другие вопросы об этом сервере:
-
Как настроить сервер для работы с фиксированной частотой кадров? Настройте свою камеру для доставки кадров с этой скоростью, а затем спите достаточно времени во время каждой итерации цикла доставки камеры, чтобы работать с этой скоростью.
-
Как увеличить частоту кадров? Сервер, который я здесь описываю, обслуживает видеокадры с максимально возможной скоростью. Если вам нужна более высокая частота кадров, вы можете попробовать настроить камеру для меньших кадров видео.
Как добавить звук? Это действительно тяжело. Аудио не поддерживается в формате Motion JPEG. Вам нужно будет использовать отдельный поток для аудио, а затем добавить аудиоплеер на HTML-страницу. Даже если вам удастся все сделать, синхронизация между аудио и видео не будет очень точной.
Как сохранить поток на диск на сервере? Просто сохраните последовательность файлов JPEG в потоке камеры. С этой целью вы можете удалить автоматический механизм завершения фоновых потоков, когда нет зрителей.
Как добавить элементы управления воспроизведением в видеоплеер? Motion JPEG не допускает взаимодействия с пользователем, но если вам нужна эта функция, элементы управления воспроизведением немного сложны. Если на сервере хранятся все изображения в формате jpeg, пауза может быть достигнута за счет повторной доставки сервером одних и тех же кадров. Когда пользователь возобновит воспроизведение, серверу придется обслуживать «старое» изображение, загруженное с диска, потому что теперь пользователь находится в режиме DVR, а не смотрит поток в прямом эфире. Это может быть очень интересный проект!
Это все для этой статьи. Дайте нам знать, если у вас есть другие вопросы!
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллекти другие поля, если вы хотите видеть больше качественных переводов, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.