Заняття присвячене потоковій передачі (streaming). Властивості, які пропонують додатки Flask, дозволяють давати великі відповіді, ефективно розділяючи їх на невеликі порції протягом тривалого періоду часу. Розглянемо, як побудувати сервер потокового відео в реальному часі.
Що таке потокове відео?
Потокове відео (streaming) є технологією, в якій сервер надає відповідь на запит шматками. Є декілька причин, чому це може бути корисно:
• Дуже великі відповіді. Намагання зібрати відгук у пам'яті, щоб надати його клієнту, може бути неефективним для дуже великих відповідей. Звісно, можна було б написати відповідь на диск, а потім повернути файл з flask.send_file(), але це додає I/O в суміш. Надання відповіді невеликими порціями є набагато кращим рішенням, припускаючи, що дані можуть бути отримані шматками.
• Дані в режимі реального часу. Для деяких додатків за запитом може знадобитися повернути дані, які надходять з джерела в реальному часі. Дуже хорошим прикладом цього є режим реального часу для відео або аудіо потоку. Багато камер безпеки використовують цей метод для передачі потокового відео на веб-браузери.
Реалізація потокової передачі даних з Flask
Flask забезпечує вбудовану підтримку потокової передачі відповідей за допомогою використання функцій генератора. Генератор є спеціальною функцією, яка може бути перервана і відновлена. Розглянемо наступну функцію:
def gen():
yield 1
yield 2
yield 3
Ця функція виконується в три етапи, кожен з яких повертає значення. Наступні сесії оболонки дадуть вам трохи уявлення про те, як використовуються генератори:
>>> x = gen()
>>> x
<generator object gen at 0x7f06f3059c30>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
В цьому простому прикладі бачите, що функція генератора може повертати декілька результатів в певній послідовності. Flask використовує цю характеристику функцій генератора для реалізації потокової передачі.
У наведеному нижче прикладі показано, як за допомогою потокової передачі можна генерувати велику таблицю даних, без необхідності збирання всієї таблиці в пам'яті:
from flask import Response, render_template
from app.models import Stock
def generate_stock_table():
yield render_template('stock_header.html')
for stock in Stock.query.all():
yield render_template('stock_row.html', stock=stock)
yield render_template('stock_footer.html')
@app.route('/stock-table')
def stock_table():
return Response(generate_stock_table())
У цьому прикладі ви можете побачити, як Flask працює з функціями генератора. Маршрут, який повертає потокову відповідь повинен повертати об'єкт Response, що ініціалізований за допомогою функції генератора. Flask потім піклується про виклик генератора і відправлення всіх часткових результатів клієнту у вигляді шматків.
Для цього конкретного прикладу, якщо візьмете Stock.query.all(), що інтерактивно повертає результат запиту до бази даних, то можете створити один рядок потенційно великої таблиці за один раз. Тому, незалежно від кількості елементів в запиті, використання пам'яті процесом Python не збільшуватиметься через те, що треба зібрати великий рядок відповіді.
Багатокомпонентні відповіді
В наведеному вище прикладі таблиця генерує традиційну сторінку невеликими порціями, причому, всі частини злиті в остаточний документ. Це хороший приклад того, як генерувати великі відповіді, але додаймо ще чогось трохи більш захоплюючого, щоб працювати з даними в реальному масштабі часу.
Цікаве використання потокової передачі повинно мати можливість заміняти кожен попередній шматок на сторінці, бо це дозволить потокам "грати" або робити анімацію у вікні браузера. За допомогою цього методу ви можете мати зображення як кожен шматок в потоці, а це надає вам чудовий відео-канал, який працює в браузері!
Секрет для реалізації оновлення на місці у використанні багатокомпонентної відповіді. Складові відповіді складаються з заголовка, який включає в себе один з багатокомпонентних типів контенту, а потім частин, розділених маркером межі, кожна з яких має власну частину типу заданого контенту.
Є кілька багатокомпонентні типів контенту для різних потреб. Для цілей, що мають потік, в якому кожна частина замінює попередню частину, повинен бути використаний тип контенту multipart/x-mixed-replace. Щоб допомогти отримати уявлення про те, як це виглядає, ось структура багатокомпонентного потоку відео:
HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame
--frame
Content-Type: image/jpeg
<jpeg data here>
--frame
Content-Type: image/jpeg
<jpeg data here>
...
Як бачите вище, структура досить проста. Основний заголовок Content-Type встановлюється в multipart/x-mixed-replace і визначений граничний рядок. Тоді кожній включеній частині передують дві риски і гранична частина рядка в своєму власному рядку. Частини мають свій власний заголовок Content-Type, і кожна частина може необов'язково включати заголовок Content-Length з довжиною в байтах частини корисного навантаження, але, принаймні, для зображень браузери здатні впоратися з потоком без довжини.
Створення серверу потокового відео в реальному часі
Досить теорії - настав час, щоб побудувати повний додаток, який реалізує потокове відео в реальному часі для веб-браузерів.
Є багато способів для перегляду потокового відео в браузерах і кожен метод має свої переваги і недоліки. Метод, який добре працює з потоковими параметрами Flask, є потік послідовності незалежних зображень JPEG. Він називається Motion JPEG і використовується багатьма IP-камерами безпеки. Цей метод має низьку латентність, але якість не найкраща, бо стиснення JPEG не дуже ефективне для відео руху.
Нижче можете побачити на диво простий, але повний веб-додаток, який може обслуговувати потік Motion JPEG:
#!/usr/bin/env python
from flask import Flask, render_template, Response
from camera import Camera
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
def gen(camera):
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/video_feed')
def video_feed():
return Response(gen(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
Цей додаток імпортує клас Camera, який відповідає за забезпечення послідовності кадрів. Введення частини управління камерою, як окремого модуля, в даному випадку є хорошою ідеєю - таким чином веб-додаток залишається чистим, простим і універсальним.
Додаток має два маршрути. Маршрут / обслуговує головну сторінку, яка визначена в шаблоні index.html. Нижче можете побачити вміст цього файлу шаблону:
<html>
<head>
<title>Video Streaming Demonstration</title>
</head>
<body>
<h1>Video Streaming Demonstration</h1>
<img src="{{ url_for('video_feed') }}">
</body>
</html>
Ця проста HTML-сторінка лише з заголовка і тега зображення. Зверніть увагу, що атрибут тега src зображення вказує на другий маршрут цього додатка, де і відбувається чарівництво.
Маршрут /video_feed повертає відповідь потокової передачі. Оскільки цей потік повертає зображення, які будуть відображатися на веб-сторінці, URL-адреса для цього маршруту вказана в атрибуті src тега зображення. Браузер буде автоматично зберігати оновлений елемент зображення шляхом відображення потоку зображень JPEG в ньому, так як багатокомпонентні відповіді підтримуються в більшості/всіма браузерами (дайте знати, якщо знайшли браузер, який цього не любить).
Функціональний генератор, використаний в маршруті /video_feed, називається gen(), і приймає як аргумент екземпляр класу Camera. Аргумент mimetype встановлюється, як показано вище, з типом контенту multipart/x-mixed-replace і межу встановлює в рядок "frame".
Функція gen()входить в цикл, в якому вона безперервно повертає кадри з камери, як шматки реагування. Функція запитує камеру, щоб забезпечити кадр, за допомогою виклику методу camera.get_frame(), а потім вона дає з цим кадром відформатований шматок відповіді з типом вмісту image/jpeg, як показано вище.
Отримання кадрів з відеокамери
Все, що залишилося зробити, це реалізувати клас Camera, який повинен буде підключатися до обладнання камери і завантажувати з неї відеокадри в реальному часі. Гарною річчю інкапсуляції апаратної залежної частини цього додатка в класі є те, що цей клас може мати різні реалізації для різних людей, при цьому інша частина програми залишається незмінною. Ви можете думати про цей клас як про драйвер пристрою, який забезпечує рівномірну реалізацію, незалежно від фактичного використовуваного обладнання.
Інша перевага відокремленого від іншої частини програми класу Camera є те, що додаток можна легко обдурити, щоб він думав, що є камера, коли в дійсності її не існує, тому клас камери може бути реалізований, щоб зробити емуляцію камери без реального апаратного забезпечення. Це буде найпростішим способом перевірки потокової передачі і не доведеться турбуватися про апаратне забезпечення, поки не буде працювати все інше. Нижче можете побачити просту емуляцію реалізації камери:
from time import time
class Camera(object):
def __init__(self):
self.frames = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]
def get_frame(self):
return self.frames[int(time()) % 3]
Дана реалізація читає три зображення з диска з назвами 1.jpg, 2.jpg і 3.jpg, а потім повертає їх один за одним кілька разів зі швидкістю один кадр в секунду. Метод get_frame використовує поточний час в секундах, щоб визначити, який із трьох кадрів повернути в будь-який даний момент. Досить просто, чи не так?
Так, це супер простий спосіб налаштування потокового відео на Raspberry Pi, щоб отримати доступ до відео в реальному часі з iOS, Android, Windows і Mac. Проект перетворює камеру вашого RPi в веб-камеру реального часу. І налаштування насправді займає менше 5 хвилин. Розглянемо його послідовність.
Налаштування потокового відео для Raspberry Pi
1. Дозвольте роботу камери RPi, якщо не зробили цього раніше:
sudo raspi-config
2. Якщо ви ще не встановили pip на своєму RPi, то використовуйте цю команду, щоб встановити його:
sudo apt-get install python-pip
3. Встановіть бібліотеку picamera, виконавши команду:
pip install picamera
4. Встановіть бібліотеку flask Python, виконавши команду:
sudo pip install flask
5. Завантажте проект потокового відео на Flask Мігеля, виконавши команду:
git clone https://github.com/miguelgrinberg/flask-video-streaming.git
6. В папці проекту відредагуйте файл app.py:
#!/usr/bin/env python
from flask import Flask, render_template, Response
# emulated camera
from camera import Camera
# Raspberry Pi camera module (requires picamera package)
# from camera_pi import Camera
app = Flask(__name__)
@app.route('/')
def index():
"""Video streaming home page."""
return render_template('index.html')
def gen(camera):
"""Video streaming generator function."""
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/video_feed')
def video_feed():
"""Video streaming route. Put this in the src attribute of an img tag."""
return Response(gen(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, threaded=True)
7. Закоментуйте рядок додаванням “#” на його початку:
#from camera import Camera
Розкоментуйте такий рядок:
from camera_pi import Camera
8. Збережіть файл.
9. Виконайте команду, щоб знайти локальну IP-адресу свого RPi:
Ifconfig
10. Ви побачите багато виведених рядків - шукайте щось подібне:
inet addr:192.168.0.107 Bcast:192.168.0.255 Mask:255.255.255.0
11. inet addr - ваша локальна IP-адреса. У даному випадку, 192.168.0.107.
12. Запустіть сервер Flask, виконавши команду:
python app.py
13. Ви побачите наведені нижче рядки, які означають, що сервер запущений на порту 5000 і готовий до роботи:
* Running on http://0.0.0.0:5000/
* Restarting with reloader
14. Відкрийте веб-браузер на своєму улюбленому пристрої і перейдіть за адресою http://192.168.0.107:5000, за винятком того, що замініть IP-адресу тією, з якою ваш RPi буде запущений.
15. Ви повинні бачити відео в реальному часі, яке захоплює ваш RPi.
Наголошуємо, що команди будуть працювати тільки з комп'ютера, підключеного до тієї ж локальної мережі, що і ваш RPi. Ви можете звернутися до цих вказівок і команд з іншого проекту, якщо хочете налаштувати RPi для віддаленого доступу.
Якщо коротко, то тепер ви надсилаєте клієнтам потокове відео в реальному часі, використовуючи Motion JPEG, який лише послідовно посилає JPEG-кадри.
Наведемо модифікований приклад коду для вмикання потокового відео з веб-камери за допомогою OpenCV. OpenCV використовує VideoCapture, щоб повернути байти необроблених зображень, які не є JPEG, тому вам потрібно зробити додатковий крок кодування байтів зображення в JPEG і все буде працювати.
# camera.py
import cv2
class VideoCamera(object):
def __init__(self):
# Using OpenCV to capture from device 0. If you have trouble capturing
# from a webcam, comment the line below out and use a video file
# instead.
self.video = cv2.VideoCapture(0)
# If you decide to use video.mp4, you must have this file in the folder
# as the main.py.
# self.video = cv2.VideoCapture('video.mp4')
def __del__(self):
self.video.release()
def get_frame(self):
success, image = self.video.read()
# We are using Motion JPEG, but OpenCV defaults to capture raw images,
# so we must encode it into JPEG in order to correctly display the
# video stream.
ret, jpeg = cv2.imencode('.jpg', image)
return jpeg.tobytes()
# main.py
from flask import Flask, render_template, Response
from camera import VideoCamera
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
def gen(camera):
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')
@app.route('/video_feed')
def video_feed():
return Response(gen(VideoCamera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
Повний код можете знайти на Github тут.
(За матеріалами: blog.miguelgrinberg.com, videos.cctvcamerapros.com, chioka.in)